aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xapp.mjs224
-rw-r--r--package-lock.json7
-rw-r--r--package.json3
-rw-r--r--profile.yaml16
4 files changed, 182 insertions, 68 deletions
diff --git a/app.mjs b/app.mjs
index 15771e5..b2cac4f 100755
--- a/app.mjs
+++ b/app.mjs
@@ -14,6 +14,7 @@ if (process.platform == "linux") {
import { discover, HAPTIC } from "loupedeck";
import { readFile } from "fs/promises";
import { parse } from "yaml";
+import { queue } from "async";
import { XPlane } from "./xplane.mjs";
const defaultFont = "OCR A Extended";
@@ -32,13 +33,15 @@ const pages = parse(
);
const isNumber = (x) => {
- return !isNaN(x);
+ return x != null && !isNaN(x);
};
const isObject = (obj) => {
return obj != null && obj.constructor.name === "Object";
};
+const deg2Rad = (x) => (x / 180) * Math.PI;
+
// state of the controller
let currentPage =
isObject(pages[0]) && pages[0].default != null ? pages[0].default : 0;
@@ -47,6 +50,11 @@ let highlighted = new Set();
// detects and opens first connected device
let device;
+
+// Render related variables
+let renderStop = [];
+let renderTasks;
+
while (!device) {
try {
device = await discover();
@@ -110,12 +118,30 @@ const getLabels = (conf) => {
return text;
};
-const formatValues = (conf, values, n = 1) => {
+const transformValues = (conf, values) => {
+ const f = (exp, v) => Function("$d", `"use strict"; return(${exp});`)(v);
+ let last;
+ const exps = Array.isArray(conf.exp) ? conf.exp : [conf.exp];
+ let res = [];
+ for (let i = 0; i < values.length; i++) {
+ let exp = exps[i] || last;
+ if (exp) {
+ res[i] = f(exp, values[i]);
+ } else {
+ res[i] = values[i];
+ }
+ last = exp;
+ }
+ return res;
+};
+
+const formatValues = (conf, values_, n = 1) => {
+ const values = transformValues(conf, values_);
const f = (fmt) => {
if (fmt) {
return Function("$d", `"use strict"; return(\`${fmt}\`);`)(values);
}
- if (isNaN(values[0])) {
+ if (!isNumber(values[0])) {
return "X";
}
return values[0].toFixed(0).toString();
@@ -123,13 +149,13 @@ const formatValues = (conf, values, n = 1) => {
let last;
let text = [];
- const formatter = Array.isArray(conf.values) ? conf.values : [conf.values];
+ const formatter = Array.isArray(conf.fmt) ? conf.fmt : [conf.fmt];
for (let i = 0; i < n; i++) {
let fmt = formatter[i] || last;
text.push(f(fmt));
last = fmt;
}
- return text;
+ return { text, values };
};
const formatColors = (color_name, conf, values, n = 1) => {
@@ -288,7 +314,7 @@ const drawSideKnobs = async (side, confs, highlight) => {
});
};
-const renderTextGauge = (c, display, values) => {
+const renderTextGauge = (c, display, values_) => {
const bg = "black";
const w = c.canvas.width;
const h = c.canvas.height;
@@ -297,16 +323,12 @@ const renderTextGauge = (c, display, values) => {
c.fillStyle = bg;
c.fillRect(0, 0, w, h);
- const text = formatValues(display, values, display.values.length);
+ const { text, values } = formatValues(display, values_, display.fmt.length);
+
// TODO: cache this
const styles = getTextStyles({
size: display.size,
- color_fg: formatColors(
- "color_fg",
- display,
- values,
- display.values.length,
- ),
+ color_fg: formatColors("color_fg", display, values, values.length),
});
renderMultiLineText(c, 0, 0, w, h, text, styles, {});
};
@@ -323,8 +345,8 @@ const renderMeterGauge = (c, display, values) => {
return;
}
- let reading = (values[0] - min) / (max - min);
- if (isNaN(reading)) {
+ let reading = (Math.max(values[0], min) - min) / (max - min);
+ if (!isNumber(reading)) {
reading = min;
}
@@ -371,7 +393,7 @@ const renderMeterGauge = (c, display, values) => {
c.stroke();
// show the value text
- const text = formatValues(display, values);
+ const { text } = formatValues(display, values);
const { font } = getTextStyles(display);
c.font = font[0];
c.fillStyle = fg;
@@ -407,7 +429,8 @@ const renderAttitudeIndicator = (c, display, values) => {
c.translate(x0, y0);
c.save();
- c.rotate((-roll * Math.PI) / 180);
+ c.rotate(deg2Rad(-roll));
+ c.save();
c.translate(0, (pitch / 10) * longSep);
// draw horizon
@@ -439,9 +462,41 @@ const renderAttitudeIndicator = (c, display, values) => {
}
}
c.stroke();
+
+ // draw bank angle arc
c.restore();
+ c.lineWidth = 1;
+ c.strokeStyle = fg;
+ c.beginPath();
+ const bankR = 30;
+ const theta0 = deg2Rad(-30);
+ const t15 = deg2Rad(-15);
+ const t10 = deg2Rad(-10);
+ const bankTicks = [10, 5, 10, 5, 5, 5, 5, 5, 10, 5, 10];
+ const bankSteps = [t15, t15, t10, t10, t10, t10, t10, t10, t15, t15];
+ c.save();
+ c.rotate(theta0);
+ c.moveTo(bankR, 0);
+ c.arc(0, 0, bankR, 0, deg2Rad(-120), true);
+ for (let i = 0; i < bankTicks.length; i++) {
+ c.moveTo(30, 0);
+ c.lineTo(30 + bankTicks[i], 0);
+ if (i < bankSteps.length) {
+ c.rotate(bankSteps[i]);
+ }
+ }
+
+ c.restore();
+ c.stroke();
+ c.beginPath();
+ c.lineWidth = 2;
+ c.moveTo(-3, -(bankR + 8));
+ c.lineTo(0, -bankR);
+ c.lineTo(3, -(bankR + 8));
+ c.stroke();
// draw center mark
+ c.restore();
c.lineWidth = 2;
c.strokeStyle = "yellow";
c.beginPath();
@@ -453,6 +508,10 @@ const renderAttitudeIndicator = (c, display, values) => {
c.lineTo(10, 0);
c.lineTo(10, 8);
c.rect(-1, -1, 2, 2);
+
+ c.moveTo(-3, -(bankR - 9));
+ c.lineTo(0, -(bankR - 1));
+ c.lineTo(3, -(bankR - 9));
c.stroke();
// draw vertical deflection dots
@@ -568,7 +627,7 @@ const renderMechanicalDisplay = (
c.strokeStyle = fg;
c.fillStyle = fg;
- if (isNaN(value)) {
+ if (!isNumber(value)) {
c.beginPath();
const y0 = narrowWinY;
const y1 = narrowWinY + narrowWinH;
@@ -690,7 +749,6 @@ const renderHSI = (c, display, values) => {
const f2 = 0.9;
const cdiR = 0.4 * r;
const vdefR = 3;
- const deg2Rad = (x) => (x / 180) * Math.PI;
const hdg = deg2Rad(values[0]);
const hdgB = deg2Rad(values[1]);
@@ -700,7 +758,7 @@ const renderHSI = (c, display, values) => {
}
const crs = src ? deg2Rad(values[src.crs]) : null;
let def = src ? Math.min(Math.max(values[src.def], -3), 3) : null;
- if (isNaN(def)) {
+ if (!isNumber(def)) {
def = 0;
}
const received = src ? values[src.received] : null;
@@ -727,24 +785,7 @@ const renderHSI = (c, display, values) => {
c.fillStyle = fg;
c.font = `16px '${defaultFont}'`;
c.fillText("N", -5, -0.5 * r);
-
- if (isNumber(hdgB)) {
- const bugW = 4;
- const bugY1 = -(r - 3);
- const bugY0 = -(r - 8);
- c.stroke();
- c.rotate(hdgB);
- c.fillStyle = "cyan";
- c.beginPath();
- c.moveTo(0, bugY1);
- c.lineTo(-bugW, -(r + 1));
- c.lineTo(-bugW, bugY0);
- c.lineTo(bugW, bugY0);
- c.lineTo(bugW, -(r + 1));
- c.lineTo(0, bugY1);
- c.fill();
- c.rotate(-hdgB);
- }
+ c.stroke();
if (crs != null) {
c.rotate(crs);
@@ -780,11 +821,33 @@ const renderHSI = (c, display, values) => {
c.moveTo(0, r);
c.lineTo(0, cdiR + 1);
+
+ c.rotate(-crs);
+ }
+
+ if (isNumber(hdgB)) {
+ const bugW = 4;
+ const bugY1 = -(r - 5);
+ const bugY0 = -(r - 8);
+ c.stroke();
+ c.rotate(hdgB);
+ c.lineWidth = 1;
+ c.strokeStyle = "white";
+ c.fillStyle = "cyan";
+ c.beginPath();
+ c.moveTo(0, bugY1);
+ c.lineTo(-bugW, -(r + 1));
+ c.lineTo(-bugW, bugY0);
+ c.lineTo(bugW, bugY0);
+ c.lineTo(bugW, -(r + 1));
+ c.lineTo(0, bugY1);
+ c.fill();
}
+
c.stroke();
};
-const renderBarGauge = (c, display, values) => {
+const renderBarGauge = (c, display, values_) => {
const bg = "black";
const fg = "white";
const w = c.canvas.width;
@@ -798,17 +861,12 @@ const renderBarGauge = (c, display, values) => {
const slotHeight = 60;
const barWidth = slotWidth * 0.6;
- const text = formatValues(display, values, display.values.length);
+ const { text, values } = formatValues(display, values_, display.fmt.length);
const label = getLabels(display);
// TODO: cache this
const { font, color_fg } = getTextStyles({
size: display.size,
- color_fg: formatColors(
- "color_fg",
- display,
- values,
- display.values.length,
- ),
+ color_fg: formatColors("color_fg", display, values, values.length),
});
c.rotate(Math.PI / 2);
@@ -845,18 +903,34 @@ const drawGauge = async (key, label, values) => {
alt: renderAltimeter,
hsi: renderHSI,
};
- await device.drawKey(key, (c) => {
- const display = label.display;
- if (display.type == null) {
- return;
- }
- if (types[display.type]) {
- types[display.type](c, display, values);
- }
+ const display = label.display;
+ if (display.type == null) {
+ return;
+ }
+ if (types[display.type]) {
+ renderTasks.push({
+ key,
+ func: (c) => types[display.type](c, display, values),
+ });
+ }
+};
+
+const resetRendering = async () => {
+ for (let i = 0; i < renderStop.length; i++) {
+ renderStop[i]();
+ }
+ renderStop = [];
+ if (renderTasks) {
+ await renderTasks.pause();
+ }
+ renderTasks = queue(async (e) => {
+ const { key, func } = e;
+ await device.drawKey(key, func);
});
};
const loadPage = async (page) => {
+ await resetRendering();
// page is not null
const { left, right, keys } = page;
let pms = [];
@@ -866,7 +940,7 @@ const loadPage = async (page) => {
const conf = Array.isArray(keys) && keys.length > i ? keys[i] : null;
pms.push(drawKey(i, conf, false));
if (isObject(conf) && conf.display != null) {
- drawGauge(i, conf, []);
+ conf.renderStart();
}
}
await Promise.all(pms);
@@ -899,12 +973,42 @@ device.on("connect", async () => {
Array.isArray(conf.display.source)
) {
let values = [];
+ //conf.fps = 0;
for (let k = 0; k < conf.display.source.length; k++) {
- values.push(NaN);
+ values.push(null);
}
const freq = isNumber(conf.display.freq)
? conf.display.freq
: 1;
+
+ const msPerFrame = 1000 / freq;
+ conf.renderStart = () => {
+ let enabled = true;
+ let startTime = new Date();
+ let timeout;
+ function draw() {
+ if (!enabled) {
+ return;
+ }
+ drawGauge(j, conf, values);
+ //conf.fps++;
+ let frameTime = msPerFrame;
+ const elapsedTime = new Date() - startTime;
+ if (elapsedTime > 1000) {
+ startTime = new Date();
+ conf.fps = 0;
+ } else if (elapsedTime + frameTime > 1000) {
+ frameTime = 1000 - elapsedTime;
+ }
+ timeout = setTimeout(draw, frameTime);
+ }
+ draw();
+ renderStop.push(() => {
+ enabled = false;
+ clearTimeout(timeout);
+ });
+ };
+
for (let k = 0; k < conf.display.source.length; k++) {
const source = conf.display.source[k];
const xplane_dataref = source.xplane_dataref;
@@ -912,12 +1016,7 @@ device.on("connect", async () => {
await xplane.subscribeDataRef(
xplane_dataref,
freq,
- async (v) => {
- values[k] = v;
- if (currentPage == i) {
- await drawGauge(j, conf, values);
- }
- },
+ async (v) => (values[k] = v),
);
}
}
@@ -1029,6 +1128,7 @@ device.on("touchend", async ({ changedTouches, touches }) => {
});
process.on("SIGINT", async () => {
+ await resetRendering();
await device.close();
await xplane.close();
process.exit();
diff --git a/package-lock.json b/package-lock.json
index 0ee9a5f..ff5f407 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "loupe-flightdeck",
"version": "0.0.4",
"dependencies": {
+ "async": "^3.2.6",
"canvas": "^2.11.2",
"loupedeck": "^6.0.1",
"yaml": "^2.5.0"
@@ -546,6 +547,12 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "license": "MIT"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
diff --git a/package.json b/package.json
index 8048f93..a7861e5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "loupe-flightdeck",
- "version": "0.0.4",
+ "version": "0.0.5",
"description": "Turn your stream deck into a flight deck!",
"repository": {
"type": "git",
@@ -10,6 +10,7 @@
"node": ">=16.0.0"
},
"dependencies": {
+ "async": "^3.2.6",
"canvas": "^2.11.2",
"loupedeck": "^6.0.1",
"yaml": "^2.5.0"
diff --git a/profile.yaml b/profile.yaml
index 63567cf..691b6ad 100644
--- a/profile.yaml
+++ b/profile.yaml
@@ -73,10 +73,16 @@
- 12
- 12
- 12
+ exp:
+ - '$d'
+ - '1 - $d'
+ - '$d'
+ - '$d'
+
# value expression written in a JS template literal (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)
# for each bar, should result in [0, 1]. Note $d is the array that
- # contains the values from "source"
- values:
+ # contains the fmt from "source"
+ fmt:
- '${$d[0] != null ? ($d[0] * 100).toFixed(0) : "X"}%'
- '${$d[1] != null ? ($d[1] * 100).toFixed(0) : "X"}%'
- '${$d[2] != null ? ($d[2] * 100).toFixed(0) : "X"}%'
@@ -95,7 +101,7 @@
- xplane_dataref: sim/cockpit2/engine/indicators/MPR_in_hg[0]
min: 10
max: 30
- values: '${$d[0] != null ? $d[0].toFixed(1) : "X"}'
+ fmt: '${$d[0] != null ? $d[0].toFixed(1) : "X"}'
# stops define the segmented arcs that the needle can point to at different angles
stops:
- color: 'green'
@@ -165,7 +171,7 @@
source:
- xplane_dataref: sim/cockpit/autopilot/autopilot_state
- xplane_dataref: sim/cockpit2/radios/actuators/HSI_source_select_pilot
- values:
+ fmt:
- '${($d[0] & 2) ? "HDG" : (($d[0] & 0x200) ? "NAV" : "ROL")} ${($d[0] & 0x10) ? "VS" : (($d[0] & 0x40) ? "FLC" : (($d[0] & 0x4000) ? "ALT" : "PIT"))}'
- '${$d[1] == 0 ? "NAV1" : ($d[1] == 1 ? "NAV2" : ($d[1] == 2 ? "GPS" : "X"))}'
size:
@@ -209,7 +215,7 @@
- xplane_dataref: sim/cockpit/radios/gps_course_degtm
- xplane_dataref: sim/cockpit2/radios/actuators/nav1_obs_deg_mag_pilot
- xplane_dataref: sim/cockpit2/radios/actuators/nav2_obs_deg_mag_pilot
- values:
+ fmt:
- '${$d[0] != null ? $d[0].toFixed(0).padStart(3, "0") : "000"}'
- 'HDG ${$d[1] != null ? $d[1].toFixed(0).padStart(3, "0") : "000"}'
- 'CRS ${($d[2] == 2 ? $d[3] : ($d[2] == 0 ? $d[4] : ($d[2] == 1 ? $d[5] : 0))).toFixed(0).padStart(3, "0")}'