octokeyz

Client Libraries

The octokeyz firmware communicates over a custom USB HID protocol using vendor-specific usage pages (0xFF00-0xFF03). Interacting with the device from userspace requires a client library that understands this protocol. Currently, a Go library is available.

Go

Package: rafaelmartins.com/p/octokeyz

The Go client library provides device discovery, button event handling, LED control, and display output for both hardware variants.

Installation

go get rafaelmartins.com/p/octokeyz

Device Lifecycle

Enumerate() returns a list of all connected octokeyz devices. GetDevice() returns a specific device by serial number, or auto-detects if exactly one device is connected when called with an empty string.

Once you have a *Device, call Open() before interacting with it and Close() when done.

dev, err := octokeyz.GetDevice("")
if err != nil {
    log.Fatal(err)
}

if err := dev.Open(); err != nil {
    log.Fatal(err)
}
defer dev.Close()

fmt.Println("connected to:", dev.SerialNumber())

If multiple devices are connected, use Enumerate() to list them, or GetDevice() with a specific serial number:

devices, err := octokeyz.Enumerate()
if err != nil {
    log.Fatal(err)
}
for _, d := range devices {
    fmt.Println(d.SerialNumber())
}

dev, err := octokeyz.GetDevice("200014000A43304D45363820")

Handling Button Events

Register callbacks for individual buttons with AddHandler(), then call Listen() to start the blocking event loop.

Handlers receive a *Button argument. Inside a handler, WaitForRelease() blocks until the button is released and returns the press duration. GetID() returns the ButtonID.

dev.AddHandler(octokeyz.BUTTON_1, func(b *octokeyz.Button) error {
    fmt.Printf("button %s pressed\n", b)
    duration := b.WaitForRelease()
    fmt.Printf("released after %s\n", duration)
    return nil
})

errCh := make(chan error, 1)
if err := dev.Listen(errCh); err != nil {
    log.Fatal(err)
}

Button constants are BUTTON_1 through BUTTON_8.

Listen() blocks indefinitely, dispatching events to registered handlers. If a handler returns an error, it is wrapped in a ButtonHandlerError and sent to the errCh channel without stopping the event loop. Pass nil for errCh to have errors sent to the standard logger.

Modifier Buttons

The Modifier type implements shift/modifier-like functionality. Register its Handler method on a button, then check Pressed() from other handlers to branch on modifier state.

var modifier octokeyz.Modifier

dev.AddHandler(octokeyz.BUTTON_8, modifier.Handler)

dev.AddHandler(octokeyz.BUTTON_1, func(b *octokeyz.Button) error {
    b.WaitForRelease()
    if modifier.Pressed() {
        fmt.Println("button 1 + modifier")
    } else {
        fmt.Println("button 1")
    }
    return nil
})

LED Control

Led() sets the indicator LED state. Five states are available:

State Behavior
LedOn Steady on
LedFlash Single 50ms pulse
LedSlowBlink 250ms period
LedFastBlink 100ms period
LedOff Off

LedFlash triggers a single short pulse on the firmware side. To create a visible flash pattern, call it multiple times with sleep intervals:

for i := 0; i < 3; i++ {
    dev.Led(octokeyz.LedFlash)
    time.Sleep(100 * time.Millisecond)
}

Display Control

Display functions are only operational on octokeyz-mega. On the basic variant, the firmware reports no display capability by returning an ErrDeviceDisplayNotSupported error whenever a function that requires a display is called.

The display has 8 lines (DisplayLine1 through DisplayLine8) with 21 characters per line. GetDisplayCharsPerLine() returns this value at runtime, or 0 if the device has no display.

DisplayLine() writes a string to a specific line with alignment:

dev.DisplayLine(octokeyz.DisplayLine1, "octokeyz", octokeyz.DisplayLineAlignCenter)
dev.DisplayLine(octokeyz.DisplayLine3, "left",     octokeyz.DisplayLineAlignLeft)
dev.DisplayLine(octokeyz.DisplayLine4, "right",    octokeyz.DisplayLineAlignRight)

Alignment options are DisplayLineAlignLeft, DisplayLineAlignRight, and DisplayLineAlignCenter.

DisplayClearLine() clears a single line. DisplayClear() clears the entire display immediately. DisplayClearWithDelay() clears the display after a firmware-side delay (millisecond resolution, up to 65535ms) -- useful for showing transient information without needing a client-side timer:

dev.DisplayLine(octokeyz.DisplayLine1, "done!", octokeyz.DisplayLineAlignCenter)
dev.DisplayClearWithDelay(2 * time.Second)

Complete Example

package main

import (
    "fmt"
    "log"
    "time"

    "rafaelmartins.com/p/octokeyz"
)

func main() {
    dev, err := octokeyz.GetDevice("")
    if err != nil {
        log.Fatal(err)
    }

    if err := dev.Open(); err != nil {
        log.Fatal(err)
    }
    defer dev.Close()

    for i := 0; i < 3; i++ {
        dev.Led(octokeyz.LedFlash)
        time.Sleep(100 * time.Millisecond)
    }

    dev.AddHandler(octokeyz.BUTTON_1, func(b *octokeyz.Button) error {
        fmt.Println("pressed")
        duration := b.WaitForRelease()
        fmt.Printf("released. pressed for %s\n", duration)
        return nil
    })

    if err := dev.Listen(nil); err != nil {
        log.Fatal(err)
    }
}

API Reference

For the full API surface, type details, and additional examples, see the package documentation on pkg.go.dev.

Source code is available at github.com/rafaelmartins/go-octokeyz.