macotron.hid
Talk to keyboards, mice, macro pads, status lights, and other HID devices with raw reports, the way hidapi and hidapitester do.
// Turn a blink(1) status light red: report 1, fade command "c", RGB, fade time.
const dev = macotron.hid.open("27b8/1ed");
if (dev) {
macotron.hid.sendFeature(dev.id, [1, 0x63, 255, 0, 0, 0, 10], { length: 9 });
macotron.hid.close(dev.id);
}
Finding devices
A device is described by HIDDeviceInfo:
| Field | Type | Meaning |
|---|---|---|
name | string | Product name, or "HID device" when it reports none. |
vendor | string | Manufacturer name, or "". |
vendorID, productID | number | USB vendor and product IDs. |
usagePage, usage | number | The primary HID usage, for example page 1, usage 6 for a keyboard. |
serial | string | Serial number, or "". |
path | string | The IORegistry path. It tells apart two interfaces of the same device. |
maxInput, maxOutput, maxFeature | number | Largest report of each kind in bytes. 64 when the device does not say. |
battery | number? | Charge from 0 to 100, only for the few devices that publish one. Bluetooth mice and keyboards report charge through network.bluetooth() instead. |
A HIDFilter narrows the match. Every field is optional, and a device must match all the fields you give:
| Field | Type | Matches |
|---|---|---|
vendorID, productID | number | USB IDs. |
usagePage, usage | number | Primary usage. |
serial | string | Exact serial number. |
path | string | Exact IORegistry path. |
vidpid | string | Hex IDs like "27b8/1ed". vendorID and productID override it. |
Anywhere a filter goes, you can pass the vidpid string by itself: "27b8/1ed", "27b8:01ed", or just a vendor such as "05ac".
list(filter?: HIDFilter | string): HIDDeviceInfo[]
Every HID device that matches. With no filter, every HID device on the Mac, including the built-in keyboard and trackpad.
open(filter?: HIDFilter | string): (HIDDeviceInfo & { id: string }) | null
Opens the first matching device and returns its info plus an id for the other calls. Returns null when nothing matches or macOS will not open the device. Each call opens a new session with a new id.
macOS opens keyboards only for apps that have Input Monitoring permission. Many devices expose several interfaces under one vendor and product ID; add usagePage or path to the filter to pick the right one.
close(id: string): void
Closes the session. Pending readInput calls reject. Macotron closes every session when plugins reload.
Sending reports
data is an array of bytes (0–255) or a string such as "1,99,0,255". In a string, bytes are separated by commas, spaces, or colons, and a byte is hex when it starts with 0x or holds a letter ("ff"), and decimal otherwise.
The first byte is the report ID, as in hidapi. Use 0 when the device does not number its reports; Macotron does not send that leading 0. A non-zero report ID stays in the data that is sent.
sendOutput(id: string, data: number[] | string, opts?: { length?: number }): { ok, written?, error? }
Sends an output report. length pads the data with zeros up to that many bytes, counting the report ID. On success, written is the number of bytes sent. On failure, error is "not open", "bad data", "empty report", or an IOKit error code such as "0xe00002ed".
sendFeature(id: string, data: number[] | string, opts?: { length?: number }): { ok, written?, error? }
Sends a feature report. It works the same as sendOutput.
Reading reports
readInput(id: string, opts?: { timeout?: number }): Promise<{ id, reportId, data } | null>
Waits for the next input report from the device, like hid_read. This is how most devices answer a command. timeout is milliseconds and defaults to 1000. Resolves null when nothing arrives in time, and rejects when the session is not open.
Reports queue from the moment open returns, so a reply that arrives before you call readInput is not lost. The queue holds the newest 64 reports.
const dev = macotron.hid.open({ vendorID: 0x1234, productID: 0x5678, usagePage: 0xff00 });
if (dev) {
macotron.hid.sendOutput(dev.id, [0, 0x01], { length: 65 });
const reply = await macotron.hid.readInput(dev.id, { timeout: 500 });
if (reply) console.log(reply.data);
}
readInputReport(id: string, opts?: { reportId?: number; length?: number }): number[] | null
Asks for an input report with a control transfer (GetReport). Most devices never answer it; use readInput unless you know the device does. Returns the bytes with the report ID first, or null on failure. length defaults to the device's largest input report plus one.
readFeature(id: string, reportId: number, opts?: { length?: number }): number[] | null
Reads a feature report. Returns the bytes with the report ID first, or null on failure. length defaults to the device's largest feature report plus one.
reportDescriptor(id: string): number[] | null
The device's raw HID report descriptor, which lists its reports and their sizes. Returns null when the session is not open or the device does not publish one.
Streaming input
listen(id: string): { ok: boolean; error?: string }
Sends every input report from the device as a hid:input event. While a session listens, reports go to the event instead of the readInput queue, and the queue is emptied. A readInput call that is already waiting still gets the next report. Returns { ok: false, error: "not open" } for an unknown id.
unlisten(id: string): void
Stops the events. Reports queue for readInput again.
Events
hid:input
Fires for each input report from a session that is listening. The payload is { id, reportId, data }, where id is the session and data is an array of bytes.
const pad = macotron.hid.open({ vidpid: "1d50/615e" });
if (pad) macotron.hid.listen(pad.id);
macotron.on("hid:input", ({ id, data }) => {
if (pad && id === pad.id) console.log("keys", data);
});
See also
- macotron.usb to watch any USB device come and go.
- macotron.event to read or post ordinary key and mouse events.
- The
hid.jsplugin.