#!/usr/bin/env node
import { registerFont } from 'canvas';
registerFont('./ocr-a-ext.ttf', { family: 'ocr' });
import { discover } from 'loupedeck'
import { readFile } from 'fs/promises';
import { parse } from 'yaml'
import { sendCommand } from './xplane.mjs';
import { isNumberObject } from 'util/types';
const pages = parse(await readFile("./profile.yaml", "utf8"));
// state of the controller
let currentPage;
let pressed = new Set();
let highlighted = new Set();
// Detects and opens first connected device
const device = await discover()
const isNumber = (x) => { return !isNaN(x); }
const isObject = (obj) => {
return obj != null && obj.constructor.name === "Object"
};
const doAction = (labeled, type) => {
if (!isObject(labeled)) {
return;
}
let actionSpec = labeled[type];
if (actionSpec === undefined) {
return;
}
if (actionSpec.hasOwnProperty('xplane_cmd')) {
sendCommand(actionSpec.xplane_cmd);
}
};
const rectifyLabel = (label) => {
let text;
let font = "22px ocr";
if (isObject(label)) {
text = label.text;
if (label.hasOwnProperty('size')) {
font = `${label.size}px ocr`;
}
} else {
text = label.toString();
}
return { text, font }
}
const drawKey = (key, label, down) => {
device.drawKey(key, (c) => {
const { text, font } = rectifyLabel(label);
const padding = 10;
const bg = down ? "white" : "black";
const fg = down ? "black" : "white";
const w = c.canvas.width;
const h = c.canvas.height;
c.fillStyle = bg;
c.fillRect(0, 0, w, h);
c.fillStyle = fg;
c.lineWidth = 2;
c.strokeStyle = fg;
c.strokeRect(padding, padding, w - padding * 2, h - padding * 2);
c.font = font;
const { width, actualBoundingBoxAscent, actualBoundingBoxDescent } = c.measureText(text);
const x_axis = (w - width) / 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) => {
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 x_padding = 8;
const y_padding = 3;
const bg = hl ? "white" : "black";
const fg = hl ? "black" : "white";
const w = c.canvas.width;
const h = c.canvas.height / 3;
c.fillStyle = bg;
c.fillRect(0, y_offset, w, h);
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);
const { text, font } = rectifyLabel(labels[i]);
c.font = font;
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);
c.fillText(text, x_axis + y_offset, -(w - y_axis));
c.resetTransform();
}
})
};
const loadPage = (page) => {
const { left, right, keys } = page || {};
if (!left) {
return
}
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');
}
})
// React to knob turns
device.on('rotate', ({ id, delta }) => {
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], delta > 0 ? 'inc' : 'dec');
})
const clearStaleButton = (touches) => {
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);
pressed.delete(key);
}
}
};
device.on('touchstart', ({ changedTouches, touches }) => {
clearStaleButton(changedTouches);
const target = changedTouches[0].target;
if (target.key === undefined) {
return
}
pressed.add(target.key);
const key = pages[currentPage].keys[target.key];
drawKey(target.key, key, true);
doAction(key, 'pressed');
//device.vibrate()
})
device.on('touchmove', ({ changedTouches, touches }) => {
clearStaleButton(changedTouches);
})
device.on('touchend', ({ changedTouches, touches }) => {
clearStaleButton(changedTouches);
const target = changedTouches[0].target;
if (target.key === undefined) {
return
}
pressed.delete(target.key);
drawKey(target.key, pages[currentPage].keys[target.key], false)
})
process.on('SIGINT', () => {
device.close().then(() => {
console.info("shutdown")
process.exit()
})
})