Skip to content

bitmouse: support the ATK ZERO on wired and receiver transports - #50

Open
itsnttt wants to merge 2 commits into
OpenMouse-Project:mainfrom
itsnttt:bitmouse-atk-zero
Open

bitmouse: support the ATK ZERO on wired and receiver transports#50
itsnttt wants to merge 2 commits into
OpenMouse-Project:mainfrom
itsnttt:bitmouse-atk-zero

Conversation

@itsnttt

@itsnttt itsnttt commented Sep 5, 2026

Copy link
Copy Markdown

Closes #48.

What this adds

ATK's current mice speak a configuration protocol this repository does not
implement. Their vendor software calls the family BITMOUSE; it shares
nothing with the 16-byte EEPROM protocol in src/atk/ but the vendor id.

SUPPORTED_HID_FILTERS only asks ATK for { 0x373b, usagePage 0xff02, usage 2 },
and an ATK ZERO exposes no 0xff02 at all — so it never reaches the browser's
device picker. Vendor id alone was never enough to conclude this family was
covered.

This adds the codec under src/bitmouse/, a driver at
src/drivers/atk/bitmouse-hid.ts, and PID-scoped discovery filters.

Devices

USB id Firmware Path
ATK ZERO, wired 0x373b:0x1154 mouse 3.0.3 direct, target = 0
ATK ZERO, 8K receiver 0x373b:0x1155 mouse 3.0.3, receiver 3.0.2 relayed, target = 1

Both answer cid,mid = 1,1. Sensor is a PAW3950 Ultra (10–42 000 DPI, stepping
10 / 50 / 100).

Only these two products are claimed. The same protocol drives much of the
current ATK and VXE range, but each model wants its own hardware check first —
the table in src/bitmouse/index.ts is where one goes.

Framing

63-byte frames on report 0x08, over the vendor collection at usage page
0xff05 / usage 0x0001.

request  [0] checksum = Σ bytes 1..62 & 0xff
         [1] 0x72   [2] paramLen   [3] cmdSn   [4] target
         [5] commandId              [6] cmdLen  [7..] payload

reply    no checksum byte, so every field sits one earlier:
         [0] 0x72   [1] status (0xff = error)  [2] cmdSn  [3] target
         [4] commandId              [5] payload length    [6..] payload

A receiver answers its own commands on target 0 and relays mouse commands on
target 1.

Three firmware behaviours worth knowing

Replies are not zero-padded. Bytes past the reported length are whatever the
previous exchange left behind:

GetDeviceType -> 72 00 3a 00 1a 01 | 03 | ff ff 00 00 00 01 01 01 08 07 04
                                     ^^   leftovers from GetCurrentMouseConfig

bitmouseDecodeReply trims to the reported length, and a test pins this capture.

The DPI table's count byte is the table size, not the enabled count. It reads
8 on a mouse holding two configured stages; unused slots read 0 DPI while keeping
stale colour bytes.

enable on a DPI write is a command bit, not a stored flag. This one cost a
round trip. A stage record is eight bytes and a write prefixes it with the stage
index, so the write byte that lines up with the record's last byte is a literal
zero at +8 — and enable, at +9, has no counterpart in the table at all. An
early version of this driver read that last byte back as an enable flag and fed
it to the next write, so every DPI write sent enable=0. The mouse stored the
value, answered the read-back with it, and never moved the sensor. Read-back
verification passed the whole time; only using the mouse showed it. enable is
true when the stage being written is the active one, and the byte the table does
store is named reserved so it is not mistaken for a flag again.

Lift-off is a range, not three stops

The level is not the config block's silentHeight byte, which reads zero: the
vendor reads it as offsetCalibration + 1 and writes it as
{ height: 0, offsetCalibration: level - 1 }. The level is the same register
scale the A9 family already uses in src/atk/index.ts — tenths of a millimetre
offset by six — so codes 1 to 11 are 0.7 mm to 1.7 mm, which is the slider
the vendor software shows.

MouseStatus.liftOffDistance carries three stops, which would throw away eight
of the eleven settings, so the driver reports both: the nearest stop for the
shared control, and the range in a new optional liftOffScale for a control
that can use it. Drivers that set no scale are unaffected.

Verified on hardware

An ATK ZERO, over its receiver and wired. Every write is confirmed by reading
the value back, and each was restored afterwards.

Setting Command Change How it was checked
DPI 2 800 → 1600 pointer speed
Polling rate 1 2000 → 1000 Hz read-back
Lift-off 3 0.7 → 1.7 mm lifting the mouse; tracking stops noticeably higher
Angle snapping 11 off → on slow horizontal drags track straight
Debounce 22 4 → 8 ms read-back
Sleep 21 1800 → 600 s read-back
Motion sync 13 on → off read-back
Ripple control 12 on → off read-back
Sensor sampling mode 31 Eco / High / Ultra read-back from address 74
Ultra Long Range 27 off → on read-back from address 75

Reads exercised on both transports: device version, cid/mid, device type,
battery level and charging state, the config block, and the DPI stage table.

Deliberately not decoded

  • GetChipId (71) and GetMouseOnlineStatus (35) answer zero on
    hardware that is plainly online, so neither is used.
  • cmdSn is a constant 0x3a in every reply captured. Not validated against.
  • Sensor angle (command 33, address 2692) is located but not implemented.
  • The config block's DPI field, which reads zero and whose offsets overlap the
    lift-off byte in the vendor's own accessors. DPI comes from the address sweep
    the vendor software uses.

Evidence

Framing, command ids, field offsets and request lengths were read out of the
vendor's own WebHID configurator (ATK HUB v3.2.21, served publicly from
hub.atk.pro), and every value used here was then confirmed against the device.
No vendor code is copied; the implementation and its naming are this
repository's own.

The mouseCidMid command (74) is the same identification route as #44,
approached from this protocol's side.

Checks

  • npm run check here: build clean, 848 tests, npm pack fine.
  • openmouse built against this package with
    npm install --no-save --package-lock=false ../mouse-protocol:
    npm run check clean, 112 tests, and the app was driven against the mouse
    for the table above.
  • registry.test.ts needed 0xff05 in USAGE_PAGES, exactly as its own
    assertion message instructs, so the new driver is covered by the overlap
    matrix.

Matching application PR

The app needs changes to reach this driver at all, and to render the lift-off
range — see OpenMouse-Project/openmouse#159.

itsnttt and others added 2 commits September 5, 2026 07:03
ATK's newer mice use a different configuration channel from the A9
family already in atk/hid.ts: 63-byte frames on report 0x08 over a
vendor collection at usage page 0xff05 / usage 0x0001, rather than
16-byte EEPROM commands on 0xff02 / 0x0002. The existing ATK request
filter matches only the older collection, so an ATK ZERO never reaches
the browser picker at all.

Add the framing as a codec under src/bitmouse -- the name the vendor
software gives this protocol family -- and a driver for the two ATK ZERO
USB ids, wired and behind its 8K receiver. Only those two products are
claimed; the same protocol drives many more models, but each needs its
own hardware check first.

Verified on an ATK ZERO, mouse firmware 3.0.3, receiver firmware 3.0.2,
over both transports:

  reads   device version, cid/mid (1,1), device type, battery level and
          charging state, the config block, the DPI stage table
  writes  each confirmed by reading back, then restored: polling rate
          2000 -> 1000 Hz, DPI 800 -> 1600, debounce 4 -> 8 ms, sleep
          1800 -> 600 s, motion sync and ripple control on -> off

Two firmware behaviours the tests pin down: a reply leaves the previous
exchange's bytes past its own length, so payloads are trimmed to the
length the device reports; and the DPI table's count byte reads 8 while
only two stages hold a value, so the usable stages are the leading run
with a non-zero DPI.

Left unreported rather than guessed at: lift-off distance, where the
config byte reads zero on both transports and the vendor's own field map
overlaps a 16-bit DPI value with it; angle snapping, which has a command
but was not exercised; chip id and the online-status byte, which both
answer zero on hardware that is plainly online.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ong range

Driving the ATK ZERO from the OpenMouse app rather than a unit test turned
up a defect the read-back verification could not catch, plus four settings
the first pass either mis-read or left out.

The DPI enable bit. A stage record is eight bytes and a write prefixes it
with the stage index, so the write byte lining up with the record's last
byte is a literal zero at +8, and `enable` at +9 has no counterpart in the
stored table at all. Reading that last byte back as an enable flag and
feeding it to the next write meant every DPI write sent enable=0: the mouse
stored the value, answered the read-back with it, and never moved the sensor
onto it. `enable` is a command bit, set when the stage being written is the
active one, so it is computed that way now. The byte the table does store is
renamed `reserved` so it is not mistaken for a flag again.

Lift-off. The level is not in the config block's silentHeight byte, which
reads zero: the vendor reads it as offsetCalibration + 1 and writes it as
{ height: 0, offsetCalibration: level - 1 }. The level is the A9 register
scale, tenths of a millimetre offset by six, so codes 1 to 11 are 0.7 mm to
1.7 mm — a range, not the three stops first assumed. Both are reported: the
nearest stop for the shared control, and the range in `liftOffScale` for a
mouse-specific slider.

Straight line correction is angle snapping under the vendor's name, so it is
reported and written through the shared control instead of hidden.

Sensor sampling mode and Ultra Long Range are read with getAddressData at
addresses 74 and 75 and written with commands 31 and 27. Long range needs a
field of its own, so `longRangeMode` joins the shared status.

`setDpiStageValue` implements the stage editor the driver already advertised
through `ui.dpiStageEditor` but could not service.

Verified on an ATK ZERO over its receiver: DPI 800 -> 1600, polling
2000 -> 1000 Hz, debounce 4 -> 8 ms, sleep 1800 -> 600 s, motion sync and
ripple on -> off, each read back and restored. Lift-off, angle snapping,
sensor mode and long range are decoded from the vendor software and build
clean, but have not yet been exercised on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ATK ZERO (0x373b:0x1154 / 0x1155): a second ATK protocol the 0xff02 filter cannot reach

1 participant