aboutsummaryrefslogblamecommitdiff
path: root/ctrl.mjs
blob: 1773cfbcbb38113f3390cd9bc49f775947a85888 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                   
 
                                      
 
                                                   
 



                                             


                                                              




                            

                                           
 


                         
 
                           
                                                            

  
                                       






                                   
                                                  



                                           

                                 
                          

                          
                                           

                                         

                                
     

                          















                                                                         

                                                                            
                                       

                                                                             
                                         
       



                                                    


                                                                




                                              
                                                       

                                

                                            






                                          





                                     

                                                           

                                                                                
                                           




                                                                         


                                                               
       




                                             
               
     

                                  




                                   
                         







                                                                            
     

                                 
 
                                 

                                                           
               
     

                                                                              









                                                                   




















                                                               

                                       


                                                                        







                                                              
                                                          


                                            
               

                            

                                                    


                                       
 
                                                         
                                     
   
 
                                                        


                                            
               

                               

                                                                    
 
                            
                               


                       
#!/usr/bin/env node

import { registerFont } from "canvas";

registerFont("./ocr-a-ext.ttf", { family: "ocr" });

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"));

// 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 takeAction = (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) => {
        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 x_padding = 8;
            const y_padding = 3;
            const bg = hl ? light : "black";
            const fg = hl ? "black" : light;
            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.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;
    }
    drawSideKnobs("left", left);
    drawSideKnobs("right", right);
    for (let i = 0; i < 12; i++) {
        drawKey(i, keys[i], false);
    }
};

// Observe connect events
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]);
});

const handleKnobEvent = (id) => {
    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);
    }
    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),
    );
    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);
    takeAction(key, "pressed");
    device.vibrate(HAPTIC.REV_FASTEST);
});

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(() => {
        process.exit();
    });
});