Notes for February 5-11

A rainy, soggy, somewhat intense week. I almost enjoyed it.

Monday, 2024-02-05

Pretty quiet day, managed to get a lot done at work.

  • Got a few Arduino Pro Micros in the mail and piled them up in a project box while I printed the rest of the BOM for that project. I like this chip because it has a lot of easy to use analog inputs and you can change the USB HID descriptions to just about anything you want, which comes in handy more often than you’d think.
  • Upgraded the kernel and Intel microcode on , since apparently there are improvements in scheduling across both P-cores and E-cores in newer CPUs (you can get intel-microcode from the non-free repositories).

Tuesday, 2024-02-06

Didn’t really get anything fun done until the evening.

  • Printed out the parts for a clever joystick-based 6DOF controller and spent a while investigating SpaceMouse USB HID reporting. Turns out someone figured most of it out already–except, apparently, all the buttons, but those are documented in this library.
  • Spent a little while poking at in hopes of building a small desktop app to automate a few things. The UI box model is a bit weird, so my control layout is currently all over the place.
  • Recoded my Proxmox VM to HomeKit via Tasmota bridge in asyncio, making it a lot simpler and more responsive while using only distribution packages:
#!/usr/bin/env python3

# Requirements: apt install python3-asyncio-mqtt

from asyncio import run, create_task, sleep, create_subprocess_shell
from asyncio.subprocess import PIPE, STDOUT 
from asyncio_mqtt import Client
from datetime import datetime
from json import dumps, loads
from logging import basicConfig, INFO, DEBUG, WARNING, getLogger

basicConfig(level=INFO, format='%(asctime)s %(levelname)s %(funcName)s:%(lineno)s %(message)s')
log = getLogger(__name__)
cluster_status = {}
vm_sets = {
    "gpu_exclusive_set": ['106','108'],
    "media": ['100'],
    "windows": ['300','301','303']
}
monitored_vms = list(set([item for sublist in vm_sets.values() for item in sublist]))

async def setup_entities(c: Client) -> None:
    global monitored_vms
    for vm in monitored_vms:
        await c.publish(f"homeassistant/switch/proxmox_{vm}/config", dumps({
            "name": f"Proxmox VM {vm}",
            "stat_t": f"proxmox_{vm}/tele/STATE",
            "avty_t": f"proxmox_{vm}/tele/LWT",
            "pl_avail": "Online",
            "pl_not_avail": "Offline",
            "cmd_t": f"proxmox_{vm}/cmnd/POWER",
            "val_tpl": "{{value_json.POWER}}",
            "pl_off": "OFF",
            "pl_on": "ON",
            "uniq_id": f"proxmox_{vm}",
            "dev": {
                "ids": [f"proxmox_{vm}"]
            }
        }))
        await c.subscribe(f"proxmox_{vm}/#")

async def pvesh(path: str, verb: str="get") -> dict: 
    proc = await create_subprocess_shell(f"pvesh {verb} {path} --output-format json", stdout=PIPE)
    log.debug(proc)
    stdout, _ = await proc.communicate()
    log.debug(stdout)
    try:
        return loads(stdout.decode())
    except Exception as e:
        log.warning(e)
        return None

async def monitor_cluster_status(c: Client) -> None:
    while(True):
        await update_cluster_status(c)
        await sleep(45)

async def update_cluster_status(c: Client) -> None:
    global cluster_status, monitored_vms
    now = datetime.now().isoformat()
    for i in await pvesh("/cluster/resources --type vm"):
        log.debug(i)
        vm = i['id'].split("/")[1]
        cluster_status[vm] = {
            "id": i['id'],
            "node": i['node'],
            "name": i['name'], 
            "status": i['status']
        }
        if vm in monitored_vms:
            log.debug(f"{vm} {i['status']}")
            await c.publish(f"proxmox_{vm}/tele/HASS_STATE", dumps({"Version":"0.1","Module":"Proxmox VE"}))
            await c.publish(f"proxmox_{vm}/tele/STATE", dumps({"Time":f"{now}","POWER": "ON" if i['status']=="running" else "OFF" }))

async def queue_action(c: Client, path: str) -> None:
    await pvesh(path, "create")
    await sleep(30)
    await update_cluster_status(c)

async def main() -> None:
    async with Client("mosquitto.local") as c:
        create_task(monitor_cluster_status(c))
        await setup_entities(c)
        async with c.messages() as messages:
            async for message in messages:
                for vm in monitored_vms:
                    if message.topic.matches(f"proxmox_{vm}/cmnd/POWER"):
                        log.info(f"{message.topic} {message.payload}")
                        type_id = cluster_status[vm]["id"] 
                        node = cluster_status[vm]["node"]
                        action = message.payload.decode("utf-8").lower()
                        status = "start" if action=="on" else "shutdown"
                        create_task(queue_action(c, f"/nodes/{node}/{type_id}/status/{status}"))

if __name__ == "__main__":
    run(main())

To keep it running, I just tacked this unit file on and called it a night:

[Unit]
Description=Tasmota VM bridge
After=network.target

[Service]
ExecStart=/root/scripts/tasmota.py
Restart=always
Type=simple

[Install]
WantedBy=network.target

Wednesday, 2024-02-07

Had a little time in the early morning and a few scattered minutes throughout the day.

  • Upgraded Klipper on my and tweaked the config to dial back speed a bit since I am still getting the occasional Y layer shift.
  • Threw away a couple of CAD designs and started new ones, because the old ones sucked (and were around a year old anyway).
  • Poked at some LLM stuff. 64GB of RAM might not be enough for what I want to do, so I gave my sandbox 96GB of RAM temporarily. borg is getting a bit tight (and CPU inference is still much slower than I’d like, but fortunately ollama can split models between GPU and system RAM).
  • Assembled my prototype 6DOF controller and spent an entertaining hour in the evening getting the Arduino to spoof the USB IDs of a legitimate SpaceMouse:
Surprisingly functional
Yes, this had a bad layer shift, but as a prototype, it's fine.

I had to tweak the Arduino sketch a fair bit to make it work “right” for me, but the 3Dconnexion preference pane adopted it as one of its own:

#include "HID.h"

#define DOF 6
// Analog inputs from joysticks
int port[DOF] = {A0, A2, A6, A1, A3, A7};
int origin[DOF]; // initial sensor values

// conversion matrix from sensor input to rigid motion
int coeff[DOF][DOF] = {
  { 0,  0,  0, 10, 10, -20}, // TX
  { -3, -3, -3,  0,  0,  0}, // TZ
  { 0,  0,  0, -17, 17,  0}, // TY
  { 3,  3, -6,  0,  0,  0}, // RX
  { 0,  0,  0,  3,  3,  3}, // RZ
  {6,  -6,  0,  0,  0,  0}, // RY
};

int maxValues[2][DOF] = {
  {3000, 4000, 4000, 4000, 3000, 4500},//Positive Direction
  {3000, 4000, 1500, 4000, 3000, 4500} //Negative Direction
};

static const uint8_t _hidReportDescriptor[] PROGMEM = {
  0x05, 0x01,           //  Usage Page (Generic Desktop)
  0x09, 0x08,           //  0x08: Usage (Multi-Axis)
  0xa1, 0x01,           // Collection (Application)
  0xa1, 0x00,           // Collection (Physical)
  0x85, 0x01,           //  Report ID
  0x16, 0x00, 0x80,     //logical minimum (-500)
  0x26, 0xff, 0x7f,     //logical maximum (500)
  0x36, 0x00, 0x80,     //Physical Minimum (-32768)
  0x46, 0xff, 0x7f,     //Physical Maximum (32767)
  0x09, 0x30,           //    Usage (X)
  0x09, 0x31,           //    Usage (Y)
  0x09, 0x32,           //    Usage (Z)
  0x75, 0x10,           //    Report Size (16)
  0x95, 0x03,           //    Report Count (3)
  0x81, 0x02,           //    Input (variable,absolute)
  0xC0,                 //  End Collection
  0xa1, 0x00,           // Collection (Physical)
  0x85, 0x02,           //  Report ID
  0x16, 0x00, 0x80,     //logical minimum (-500)
  0x26, 0xff, 0x7f,     //logical maximum (500)
  0x36, 0x00, 0x80,     //Physical Minimum (-32768)
  0x46, 0xff, 0x7f,     //Physical Maximum (32767)
  0x09, 0x33,           //    Usage (RX)
  0x09, 0x34,           //    Usage (RY)
  0x09, 0x35,           //    Usage (RZ)
  0x75, 0x10,           //    Report Size (16)
  0x95, 0x03,           //    Report Count (3)
  0x81, 0x02,           //    Input (variable,absolute)
  0xC0,                 //  End Collection
  0xa1, 0x00,           // Collection (Physical)
  0x85, 0x03,           //  Report ID
  0x15, 0x00,           //   Logical Minimum (0)
  0x25, 0x01,           //    Logical Maximum (1)
  0x75, 0x01,           //    Report Size (1)
  0x95, 32,             //    Report Count (24)
  0x05, 0x09,           //    Usage Page (Button)
  0x19, 1,              //    Usage Minimum (Button #1)
  0x29, 32,             //    Usage Maximum (Button #24)
  0x81, 0x02,           //    Input (variable,absolute)
  0xC0,                 //  End Collection
  0xC0                  // End Collection
};

void setup() {
  static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor));
  HID().AppendDescriptor(&node);
  delay(300);
  for (int i = 0; i < DOF; i++) {
    origin[i] = analogRead(port[i]);
  }
}

void send_command(int16_t x, int16_t y, int16_t z, int16_t rx, int16_t ry, int16_t rz) {
  uint8_t trans[6] = { x & 0xFF, x >> 8, y & 0xFF, y >> 8, z & 0xFF, z >> 8 };
  HID().SendReport(1, trans, 6);
  uint8_t rot[6] = { rx & 0xFF, rx >> 8, ry & 0xFF, ry >> 8, rz & 0xFF, rz >> 8 };
  HID().SendReport(2, rot, 6);
}

void loop() {
  int sv[DOF]; // sensor value
  int mv[DOF]; // motion vector
  int moveFlag = false;

  for (int i = 0; i < DOF; i++) {
    sv[i] = analogRead(port[i]) - origin[i];
  }

  for (int i = 0; i < DOF; i++) {
    mv[i] = 0;
    for (int j = 0; j < DOF; j++) {
      mv[i] += coeff[i][j] * sv[j];
    }

    //Rescale to max value range
    if (mv[i] > 0) {
      mv[i] = (mv[i]) / (maxValues[0][i]/127);
    }
    else {
      mv[i] = (mv[i]) / (maxValues[1][i]/127);
    }
    if(mv[i] > 127) {
      mv[i] = 127;
    }
    else if(mv[i] < -128) {
      mv[i] = -128;
    }
  }
  send_command(-mv[0], mv[2], mv[1], mv[3], mv[5], -mv[4]);
}

It works surprisingly well, but after a few days of testing I really need to re-design some parts that are a bit too loose.

Thursday, 2024-02-08

Weekly late night meeting day, with a pretty great surprise.

  • Fiddled with USB device and vendor IDs just after breakfast, like an engineer ought to (for the record: vid=0x256f, pid=0xc635, SpaceMouse Compact).
  • Whipped together a little Node-RED service to take iOS Health data sent by Shortcuts via HTTP POST and have an LLM provide some commentary on it. Shortcuts remains every bit as inconsistent and irritating as
  • Received a Two Trees SK1 to test, which serendipitously is a very snug fit atop an IKEA KALLAX (not a millimetre to spare). Very impressed with the build quality.
This thing is just the right size
And then there were three. I think I can still cram in a few more...
  • After a few weeks of using Agenda I realized that it was getting in the way of both taking quick notes and polishing and finishing drafts, and thus I decided to give Heynote a chance. However, having yet another app with yet another Electron version on my system didn’t jibe, and I decided going back to iA Writer for maintaining multiple drafts would be easier.
  • Everything I had “scheduled” in Agenda moved over to Reminders, which is now surprisingly usable with its multiple Kanban-like columns and sub-tasks.

Now I know why there are a zillion notes apps.

Friday, 2024-02-09

Ran a few end-of-week errands, had a decent evening tinkering.

  • Received a few more electronics parts from AliExpress (including a couple of wrong ones, so I dread trying to sort this out).
  • Started testing the Two Trees SK1 with a few functional prints. The built-in Klipper macros are organized a bit differently from what I’m used to, so I made a few changes. Even without calibration, the print quality at speed is pretty amazing.
  • Had a stab at enabling IPv6 for docker containers on my Synology:
# sudo cat /var/packages/ContainerManager/etc/dockerd.json
{
    "data-root":"/var/packages/ContainerManager/var/docker",
    "log-driver":"db",
    "registry-mirrors":[],
    "storage-driver":"btrfs",
    "ipv6": true,
    "fixed-cidr-v6": "2001:db8:1::/64"
}
# replace the CIDR above with a proper one! I picked fdde:adbe:ef42:cafe::/64
# sudo systemctl restart pkg-ContainerManager-dockerd

However, Synology removes the "ipv6": true upon restart, which is… sub-optimal. Parked this one for the moment.

Saturday, 2024-02-10

Rainy day, which was a good excuse to stay indoors and tinker.

  • Read the Economist and generally caught up on news.
  • Personal inbox zero and some advisory work during the afternoon.
  • Adjusted the Two Trees SK1 configuration a bit more to my liking and got it to print a number of functional parts.
  • Did some I2C tinkering with an accelerometer. Plain can be remarkably refreshing sometimes.
  • Cleaned up and posted , which led to another bonus I2C hack.
  • Set up a neat little app for the that I’ll be trying out and reviewing.

Sunday, 2024-02-11

Lazy, leisurely day.

  • Played a few games, watched some morning TV.
  • Finally figured out an ancient, but working solution to have the send motion inputs to Bazzite.
  • Tried (somewhat in vain) to clean up my office, but the sheer number of electronics and spare parts is becoming an issue.
  • Disassembled the Two Trees SK1‘s toolhead to have a look at what kind of hotend and nozzles it uses. Surprisingly, it seems to use a Bambu Labs hotend clone with multiple improvements, like a reinforced heatbreak and removable nozzles. Fished around on AliExpress for a few spares to try out.
  • Flashed CYD-Klipper onto one of the 2.4” “cheap yellow displays” I recently got and had it monitor the Two Trees SK1 while it printed:
Not bad for a 700mm/s print
This thing is surprisingly good--and the screen is only at 50% brightness.

I can’t quite get over the fact that we have stupefyingly high speed, REST-enabled 3D printers these days.

This page is referenced in: