lowtech

lowtech

A minimal port of the useful core of Solaar for Logitech HID++ devices — no UI, just the things you actually want from a terminal: see your devices, read DPI/battery, watch button presses, and reassign mouse buttons. Single self-contained binary, system libraries statically linked.

Built and verified against a Logitech PRO X SUPERLIGHT 2 on a Logitech Bolt receiver, on macOS (Apple Silicon). Also builds on Linux (hidraw backend) — see Build.

Features

A bit of lore

Logitech G HUB just didn’t work for me, so I figured I’d port Solaar — the excellent Linux tool for Logitech devices — into something I could actually run. Then it turned out Solaar didn’t really support the mouse I had at the time, the LIGHTSPEED “2c” (PRO X SUPERLIGHT 2): its onboard profiles use a format newer than Solaar handles, so it couldn’t remap the buttons. So this became its own thing — deliberately minimal, just the handful of features I actually use, talking HID++ straight to the device.

It runs on macOS and Linux, but feature parity with Solaar or G HUB isn’t the goal — it’s built for setting up a minimal onboard mapping once and forgetting about it: the config lives on the mouse, so nothing has to keep running.

Huge thanks to Solaar: the HID++ feature list, control IDs, and the initial hardware/button mappings used here were derived from its work.

Why not just use Solaar?

Solaar is a large Python + GTK application. On macOS:

lowtech talks HID++ directly over the receiver’s vendor HID interface (usage page 0xFF00), which needs no special permission on macOS.

What it does

lowtech list                      enumerate receivers and reachable devices
lowtech show                      name, type, HID++ version, battery, DPI, feature list
lowtech dpi [VALUE]               read DPI (or set it to VALUE — see note)
lowtech buttons                   list the onboard-profile button assignments
lowtech assign <BUTTON> <TARGET>  remap a button (writes the mouse's flash)
lowtech restore                   restore the profile sector from the local backup
lowtech watch [--raw]             print live mouse button presses

The CLI uses clap: lowtech --help, lowtech <cmd> --help, and lowtech --version all work.

Use -s/--slot <SLOT> (global) to pick a device — a receiver device number 1..6, or ff for a directly-connected device. Omitted, it uses the first reachable device (e.g. lowtech show, lowtech --slot 2 buttons).

watch reads the Logitech mouse’s HID input report and prints button presses (move the mouse briefly so it can classify the motion bytes, then press buttons). --raw dumps the raw HID reports for debugging. Buttons decode with the standard HID order (bit0=Left, 1=Right, 2=Middle, 3=Back, 4=Forward, …).

Examples

$ lowtech list
Logitech HID++ interfaces: 1
  [receiver] Logitech receiver  (HID++ 1.0)
  [slot 1] PRO X SUPERLIGHT 2c  (HID++ 4.2, 33 features)

$ lowtech show
Device [slot 1]
  Name:     PRO X SUPERLIGHT 2c
  Type:     Mouse (3)
  HID++:    4.2
  Battery:  49% (discharging)
  DPI:      800  (LOD=2)
  Features: 33
    ...

$ lowtech buttons
Onboard profile (sector 1, 5 buttons), CRC OK:
  button 1: Left click   [80 01 00 01]
  button 2: Right click  [80 01 00 02]
  button 3: Middle click [80 01 00 04]
  button 4: Back         [80 01 00 08]
  button 5: Forward      [80 01 00 10]

# remap the "forward" button to middle-click
$ lowtech assign 5 middle
backed up original sector to ./lowtech-backup-sector1.bin
button 5: Forward -> Middle click
verified: readback matches (Middle click)

# put it back
$ lowtech assign 5 forward

assign targets:

Because lowtech writes the raw HID++ button spec, you’re not limited to the presets a vendor app exposes. G HUB only lets you pick from its own list; here a button can be any standard HID action — any mouse button, any keyboard key + modifiers (key:MM:KK), or any consumer/media usage (consumer:XXXX) — written straight into the onboard profile.

Optional: make the side buttons send browser Back/Forward

By default the side buttons send raw HID mouse buttons 4 and 5. Most browsers treat those as Back/Forward, but if yours doesn’t you can remap them in the onboard profile to send explicit navigation events — no driver needed:

# button 4 = back side button, button 5 = forward side button
$ lowtech assign 4 ac-back       # HID consumer "AC Back"  (macOS/Windows/Linux)
$ lowtech assign 5 ac-forward    # HID consumer "AC Forward"
# or, for the macOS browser keyboard shortcut instead:
$ lowtech assign 4 nav-back      # Cmd+[
$ lowtech assign 5 nav-forward   # Cmd+]

ac-back/ac-forward are the HID-standard, OS-agnostic codes for browser navigation. The change is written to onboard memory and persists with no driver running; lowtech restore reverts it.

How button assignment works (and why it’s safe)

This mouse has no 0x1B04 (Reprogrammable Controls). Buttons live in the onboard profile (0x8100), as 4-byte specs inside a 255-byte profile sector that ends with a CRC-16/CCITT-FALSE checksum. In format 7 the button array sits at offset 48 (format ≤5 used offset 32; that 16-byte shift is why Solaar’s parser misreads this mouse).

assign is conservative:

  1. Reads the active profile sector and validates its CRC — refuses to touch it if the format isn’t understood.
  2. Writes a backup of the original bytes to ./lowtech-backup-sector<N>.bin.
  3. Patches only the 4 bytes of the target button, recomputes the CRC, writes the sector (startWrite / writeData / endWrite).
  4. Reads back and verifies both the CRC and the changed button.

If anything looks wrong it tells you to run lowtech restore.

Note: dpi <value> (the host-side 0x2202 write) is rejected while the mouse is in onboard mode (0x05 logitech internal) — on this mouse DPI is governed by the onboard profile, same as buttons.

Build

Same command on either OS — the hidapi backend is selected per platform in Cargo.toml (macos-shared-device on macOS, linux-static-hidraw on Linux):

cargo build --release
./target/release/lowtech list

Or drive it with the Makefile: make build, make install (via cargo), make deps (shows the dynamic-library deps), and on Linux make install-udev — the working counterpart to Solaar’s broken make install_udev. make help lists all targets.

macOS

The bundled C hidapi is statically compiled in — no libhidapi.dylib runtime dependency. otool -L shows only Apple’s own system frameworks (IOKit / CoreFoundation / AppKit) and libSystem, which cannot be statically linked on macOS by design:

$ otool -L target/release/lowtech
    /System/Library/Frameworks/IOKit.framework/...
    /System/Library/Frameworks/CoreFoundation.framework/...
    /System/Library/Frameworks/AppKit.framework/...
    /usr/lib/libiconv.2.dylib
    /usr/lib/libSystem.B.dylib

watch (reading the mouse input report) may require Input Monitoring permission: System Settings → Privacy & Security → Input Monitoring → enable your terminal, then quit & reopen it. The HID++ commands don’t need it.

Linux

hidapi is compiled in statically using the hidraw backend (the same one Solaar uses). libudev remains a dynamic dependency for device enumeration:

$ ldd target/release/lowtech
    libudev.so.1 => ...
    libc.so.6 => ...

For a fully self-contained binary, build against musl with a static libudev/libusb (e.g. cargo build --release --target x86_64-unknown-linux-musl).

Permissions: by default hidraw nodes are root-only. Install the bundled udev rule so your user can talk to Logitech devices without sudo (this is the piece Solaar’s broken make install_udev was trying to do):

sudo cp linux/42-logitech-hidpp.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger

Then replug the receiver.

Layout

Status / limitations

Credits

Built on the shoulders of Solaar (GPL-2.0-or-later). The HID++ feature ids, control ids, and onboard-profile / button-spec encodings used here were derived from Solaar’s implementation — this project would not exist without it. lowtech is an independent, deliberately minimal reimplementation; any mistakes are mine (I mean Claude’s).

License

GPL-2.0-or-later — see LICENSE. The HID++ feature/control ids and onboard-profile / button-spec encodings are derived from Solaar (GPL-2.0-or-later), so lowtech carries the same license.