From de767f3bd2629181a5b49d9cccb1d7e494396e6e Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 15 Aug 2024 14:22:42 -0700 Subject: improve --- ctrl.mjs | 186 ++++++++++++++++++++++++++++-------------------------- package-lock.json | 88 ++++++++++++++++++++++++-- profile.yaml | 8 ++- xplane.mjs | 11 ++-- 4 files changed, 191 insertions(+), 102 deletions(-) diff --git a/ctrl.mjs b/ctrl.mjs index bcab1c5..1773cfb 100755 --- a/ctrl.mjs +++ b/ctrl.mjs @@ -1,14 +1,13 @@ #!/usr/bin/env node -import { registerFont } from 'canvas'; -registerFont('./ocr-a-ext.ttf', { family: 'ocr' }); +import { registerFont } from "canvas"; -import { discover } from 'loupedeck' +registerFont("./ocr-a-ext.ttf", { family: "ocr" }); -import { readFile } from 'fs/promises'; -import { parse } from 'yaml' -import { sendCommand } from './xplane.mjs'; -import { isNumberObject } from 'util/types'; +import { discover, HAPTIC } from "loupedeck"; +import { readFile } from "fs/promises"; +import { parse } from "yaml"; +import { sendCommand } from "./xplane.mjs"; const pages = parse(await readFile("./profile.yaml", "utf8")); @@ -17,16 +16,18 @@ let currentPage; let pressed = new Set(); let highlighted = new Set(); -// Detects and opens first connected device -const device = await discover() +// detects and opens first connected device +const device = await discover(); -const isNumber = (x) => { return !isNaN(x); } +const isNumber = (x) => { + return !isNaN(x); +}; const isObject = (obj) => { - return obj != null && obj.constructor.name === "Object" + return obj != null && obj.constructor.name === "Object"; }; -const doAction = (labeled, type) => { +const takeAction = (labeled, type) => { if (!isObject(labeled)) { return; } @@ -34,7 +35,7 @@ const doAction = (labeled, type) => { if (actionSpec === undefined) { return; } - if (actionSpec.hasOwnProperty('xplane_cmd')) { + if (actionSpec.hasOwnProperty("xplane_cmd")) { sendCommand(actionSpec.xplane_cmd); } }; @@ -44,14 +45,14 @@ const rectifyLabel = (label) => { let font = "22px ocr"; if (isObject(label)) { text = label.text; - if (label.hasOwnProperty('size')) { + if (label.hasOwnProperty("size")) { font = `${label.size}px ocr`; } } else { text = label.toString(); } - return { text, font } -} + return { text, font }; +}; const drawKey = (key, label, down) => { device.drawKey(key, (c) => { @@ -68,25 +69,30 @@ const drawKey = (key, label, down) => { c.strokeStyle = fg; c.strokeRect(padding, padding, w - padding * 2, h - padding * 2); c.font = font; - const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = c.measureText(text); + const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = + c.measureText(text); const x_axis = (w - width) / 2; - const y_axis = h / 2 + (actualBoundingBoxAscent - actualBoundingBoxDescent) / 2; + const y_axis = + h / 2 + (actualBoundingBoxAscent - actualBoundingBoxDescent) / 2; c.fillText(text, x_axis, y_axis); - }) + }); }; const drawSideKnobs = (side, labels, highlight) => { device.drawScreen(side, (c) => { + const light = pages[currentPage].hasOwnProperty("color") + ? pages[currentPage].color + : "white"; if (!highlight) { highlight = [false, false, false]; } for (let i = 0; i < 3; i++) { const hl = highlight[i]; - const y_offset = i * c.canvas.height / 3; + const y_offset = (i * c.canvas.height) / 3; const x_padding = 8; const y_padding = 3; - const bg = hl ? "white" : "black"; - const fg = hl ? "black" : "white"; + const bg = hl ? light : "black"; + const fg = hl ? "black" : light; const w = c.canvas.width; const h = c.canvas.height / 3; c.fillStyle = bg; @@ -94,76 +100,60 @@ const drawSideKnobs = (side, labels, highlight) => { c.fillStyle = fg; c.lineWidth = 2; c.strokeStyle = fg; - c.strokeRect(x_padding, y_padding + y_offset, w - x_padding * 2, h - y_padding * 2); + c.strokeRect( + x_padding, + y_padding + y_offset, + w - x_padding * 2, + h - y_padding * 2, + ); const { text, font } = rectifyLabel(labels[i]); c.font = font; - const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = c.measureText(text); + const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = + c.measureText(text); const x_axis = (h - width) / 2; - const y_axis = w / 2 + (actualBoundingBoxAscent - actualBoundingBoxDescent) / 2; - c.rotate(90 * Math.PI / 180); + const y_axis = + w / 2 + + (actualBoundingBoxAscent - actualBoundingBoxDescent) / 2; + c.rotate((90 * Math.PI) / 180); + c.fillStyle = hl ? 'black' : 'white'; c.fillText(text, x_axis + y_offset, -(w - y_axis)); c.resetTransform(); } - }) + }); }; const loadPage = (page) => { const { left, right, keys } = page || {}; if (!left) { - return + return; } - drawSideKnobs('left', left); - drawSideKnobs('right', right); + drawSideKnobs("left", left); + drawSideKnobs("right", right); for (let i = 0; i < 12; i++) { drawKey(i, keys[i], false); } }; // Observe connect events -device.on('connect', () => { - console.info('Connection successful!') - currentPage = 1; - loadPage(pages[currentPage]); -}) - -// React to button presses -device.on('down', ({ id }) => { - if (isNumber(id)) { - console.info(`switch to page: ${id}`) - if (id == 0) { - return - } - currentPage = id; - loadPage(pages[currentPage]); - } else { - const { left, right, keys } = pages[currentPage] || {}; - if (!left) { - return - } - let pos = {"T": 0, "C": 1, "B": 2}[id.substring(4, 5)]; - let side = {"L": ['left', left], "R": ['right', right]}[id.substring(5, 6)]; - let mask = [false, false, false]; - mask[pos] = true; - drawSideKnobs(side[0], side[1], mask); - if (!highlighted.has(id)) { - highlighted.add(id); - setTimeout(() => { - drawSideKnobs(side[0], side[1], [false, false, false]); - highlighted.delete(id); - }, 200); - } - doAction(side[1][pos], 'pressed'); +device.on("connect", async () => { + console.info("connected"); + currentPage = pages[0].hasOwnProperty('default') ? pages[0].default : 1; + for (let i = 0; i < pages.length; i++) { + const color = pages[i].hasOwnProperty("color") + ? pages[i].color + : "white"; + await device.setButtonColor({ id: i, color: pages[i].color }); } -}) + loadPage(pages[currentPage]); +}); -// React to knob turns -device.on('rotate', ({ id, delta }) => { +const handleKnobEvent = (id) => { const { left, right, keys } = pages[currentPage] || {}; if (!left) { - return + return; } - let pos = {"T": 0, "C": 1, "B": 2}[id.substring(4, 5)]; - let side = {"L": ['left', left], "R": ['right', right]}[id.substring(5, 6)]; + let pos = { T: 0, C: 1, B: 2 }[id.substring(4, 5)]; + let side = { L: ["left", left], R: ["right", right] }[id.substring(5, 6)]; let mask = [false, false, false]; mask[pos] = true; drawSideKnobs(side[0], side[1], mask); @@ -174,11 +164,32 @@ device.on('rotate', ({ id, delta }) => { highlighted.delete(id); }, 200); } - doAction(side[1][pos], delta > 0 ? 'inc' : 'dec'); -}) + return side[1][pos]; +}; + +// React to button presses +device.on("down", ({ id }) => { + if (isNumber(id)) { + console.info(`switch to page: ${id}`); + if (id == 0) { + return; + } + currentPage = id; + loadPage(pages[currentPage]); + } else { + takeAction(handleKnobEvent(id), "pressed"); + } +}); + +// React to knob turns +device.on("rotate", ({ id, delta }) => { + takeAction(handleKnobEvent(id), delta > 0 ? "inc" : "dec"); +}); const clearStaleButton = (touches) => { - const s = new Set(touches.map(o => o.target.key).filter(k => k !== undefined)); + const s = new Set( + touches.map((o) => o.target.key).filter((k) => k !== undefined), + ); for (const key of pressed.keys()) { if (!s.has(key)) { drawKey(key, pages[currentPage].keys[key], false); @@ -187,36 +198,35 @@ const clearStaleButton = (touches) => { } }; -device.on('touchstart', ({ changedTouches, touches }) => { +device.on("touchstart", ({ changedTouches, touches }) => { clearStaleButton(changedTouches); const target = changedTouches[0].target; if (target.key === undefined) { - return + return; } pressed.add(target.key); const key = pages[currentPage].keys[target.key]; drawKey(target.key, key, true); - doAction(key, 'pressed'); - //device.vibrate() -}) + takeAction(key, "pressed"); + device.vibrate(HAPTIC.REV_FASTEST); +}); -device.on('touchmove', ({ changedTouches, touches }) => { +device.on("touchmove", ({ changedTouches, touches }) => { clearStaleButton(changedTouches); -}) +}); -device.on('touchend', ({ changedTouches, touches }) => { +device.on("touchend", ({ changedTouches, touches }) => { clearStaleButton(changedTouches); const target = changedTouches[0].target; if (target.key === undefined) { - return + return; } pressed.delete(target.key); - drawKey(target.key, pages[currentPage].keys[target.key], false) -}) + drawKey(target.key, pages[currentPage].keys[target.key], false); +}); -process.on('SIGINT', () => { +process.on("SIGINT", () => { device.close().then(() => { - console.info("shutdown") - process.exit() - }) -}) + process.exit(); + }); +}); diff --git a/package-lock.json b/package-lock.json index 3295610..747831e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,6 +85,22 @@ "url": "https://opencollective.com/serialport/donate" } }, + "node_modules/@serialport/bindings-cpp/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@serialport/bindings-interface": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@serialport/bindings-interface/-/bindings-interface-1.2.2.tgz", @@ -218,6 +234,22 @@ "url": "https://opencollective.com/serialport/donate" } }, + "node_modules/@serialport/stream/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -345,9 +377,9 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -792,6 +824,22 @@ "url": "https://opencollective.com/serialport/donate" } }, + "node_modules/serialport/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -1008,6 +1056,14 @@ "requires": { "@serialport/parser-delimiter": "11.0.0" } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } } } }, @@ -1076,6 +1132,16 @@ "requires": { "@serialport/bindings-interface": "1.2.2", "debug": "4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } }, "abbrev": { @@ -1182,9 +1248,9 @@ "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "requires": { "ms": "2.1.2" } @@ -1483,6 +1549,16 @@ "@serialport/parser-spacepacket": "12.0.0", "@serialport/stream": "12.0.0", "debug": "4.3.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } }, "set-blocking": { diff --git a/profile.yaml b/profile.yaml index 08e80d6..109e771 100644 --- a/profile.yaml +++ b/profile.yaml @@ -1,5 +1,6 @@ --- -- {} +# for xplane_cmd, refer to /Resources/plugins/Commands.txt under your X-Plane installation +- default: 1 # page 1 - left: # left knob labels @@ -51,7 +52,7 @@ - text: MENU pressed: xplane_cmd: sim/GPS/g1000n1_menu - - text: Direct + - text: -D-> pressed: xplane_cmd: sim/GPS/g1000n3_direct - text: MENU @@ -81,6 +82,7 @@ - text: ENT pressed: xplane_cmd: sim/GPS/g1000n3_ent + color: 'yellow' # page 2 - left: @@ -145,6 +147,7 @@ - text: DN pressed: xplane_cmd: sim/GPS/g1000n1_nose_down + color: '#1a62fd' # page 3 - left: @@ -210,3 +213,4 @@ xplane_cmd: sim/audio_panel/monitor_audio_com2 - '' - '' + color: 'white' diff --git a/xplane.mjs b/xplane.mjs index 54190c9..7b82817 100644 --- a/xplane.mjs +++ b/xplane.mjs @@ -1,8 +1,8 @@ -import dgram from 'node:dgram'; +import dgram from "node:dgram"; const xplaneAddr = "localhost"; const xplanePort = 49000; -const socket = dgram.createSocket('udp4'); +const socket = dgram.createSocket("udp4"); //socket.on('message', (msg, rinfo) => { // console.log(msg, rinfo) @@ -10,7 +10,7 @@ const socket = dgram.createSocket('udp4'); // //socket.bind(10080); -const subscribeDataRef = async (dataRef) => { +export const subscribeDataRef = async (dataRef) => { //const dataRef = "sim/flightmodel/position/indicated_airspeed"; let buffer = Buffer.alloc(4 + 1 + 4 * 2 + 400); let off = buffer.write("RREF"); @@ -21,8 +21,7 @@ const subscribeDataRef = async (dataRef) => { off = buffer.writeUInt8(0, off); // null terminated console.log(Array.from(buffer)); await socket.send(buffer, 0, buffer.length, xplanePort, xplaneAddr); -} - +}; export const sendCommand = async (cmd) => { let buffer = Buffer.alloc(4 + 1 + cmd.length + 1); @@ -30,7 +29,7 @@ export const sendCommand = async (cmd) => { off = buffer.writeUInt8(0, off); // null terminated off += buffer.write(cmd, off); // command off = buffer.writeUInt8(0, off); // null terminated - console.info(`sent command: ${cmd}`) + console.info(`x-plane cmd: ${cmd}`); await socket.send(buffer, 0, buffer.length, xplanePort, xplaneAddr); }; -- cgit v1.2.3-70-g09d2