aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xctrl.mjs186
-rw-r--r--package-lock.json88
-rw-r--r--profile.yaml8
-rw-r--r--xplane.mjs11
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);
};