Skip to content

feat(lamzu): add the Atlantis generation under CompX vendor id 0x3554 (resolves #85) - #91

Merged
snekxs merged 10 commits into
mainfrom
resolve/pr-85
Sep 11, 2026
Merged

feat(lamzu): add the Atlantis generation under CompX vendor id 0x3554 (resolves #85)#91
snekxs merged 10 commits into
mainfrom
resolve/pr-85

Conversation

@snekxs

@snekxs snekxs commented Sep 11, 2026

Copy link
Copy Markdown
Member

Resolves conflicts in #85, supersedes it.

Original PR by the contributor added a new Lamzu Atlantis driver under CompX's shared vendor id 0x3554 (also used by Teevolution and VGN). The driver's isSupported and the touched src/drivers/pulsar/pulsar-hid.ts correctly exclude Atlantis product IDs by subtraction (LAMZU_ATLANTIS_PRODUCTS.keys()), following the same safe per-vendorId-scoping pattern used elsewhere in this repo (and specifically the pattern that fixes the historical Pulsar/VGN codec-overwrite bug) — no shared codec object is mutated or reused across vendor branches. The only conflict was an additive change to src/drivers/registry.ts from other PRs merged in the meantime (Incott, HyperX, MCHOSE V3). Resolved by combining both sides' additions; no logic was altered.

Verified: npm run check (build + full test suite + pack dry-run) passes, 1516/1516 tests green.

Claude-Session: https://claude.ai/code/session_01JfTcrhgBuFh8QiLvbtdJCX

bpavlina and others added 8 commits September 9, 2026 00:34
The Atlantis family speaks CompX's report-8 interrupt protocol, not the
feature-report page/command protocol the 0x373e and 0x37b0 Lamzu models
use — the same protocol this package already implements for Pulsar's 4K
receiver, so the framing, checksum, command ids and 50-step DPI encoding
are imported from there rather than restated.

Verified on a Lamzu Atlantis Mini 4K, firmware 1.24, wired 0x3554:0xf50f.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
- A late reply to a timed-out attempt could satisfy the next request:
  every flash access shares command 0x08/0x07, so a read of one address
  could return another's bytes and a setter could verify against the
  wrong field. Flash replies are now matched on the echoed address.
- open() is memoized; two concurrent reads attached two listeners, of
  which close() removed one.
- setDpiStageCount shrinks the cached stage list with the device.
- setProfile validates against the four profiles the mouse accepts
  (0-3 write and read back; 4 and above are rejected with status 1).
- A corrupt DPI stage read throws instead of reporting 50 DPI.
- Dropped the redundant per-product picker filters: 0x3554 is already
  requested vendor-wide for the Pulsar 4K receiver.
- Discovery tests, and the flash-field evidence table now says which
  fields the vendor UI confirmed and which were only round-tripped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
The Atlantis config collection carries only report 8, the request/reply
channel, so startNotifications answers false and the app keeps polling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
isSupported gates on an output report 8 on the 0xff02/usage-2
collection, and Chrome's collection shape is not the platform's — so
what navigator.hid actually reports is now in the notes rather than
assumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
setDpiStageColor scaled an unchecked stage index straight into a flash
address: stage -8 landed on the DPI stages at 12 and stage 13 on the
button actions at 96, with the colour reading back cleanly from wherever
it wrote. Every stage index is now bounded before it becomes an address.

Also:
- Setters hold the queue across write and read-back, so two concurrent
  calls for one field can no longer verify against each other's writes.
- close() cancels: it settles the waiting exchange and stops queued work
  from reopening the device.
- Switching profiles or changing the stage count drops the cached
  status, which described the old profile or the wrong stage list.
- DPI stages decode per axis, so a stage Lamzu's app set to separate x
  and y no longer reads as corrupt.
- The polling-rate family is declared per product instead of inferred
  from the rate ceiling, which had quietly decided it for the 1K
  receiver nobody has tested.
- Timers come from globalThis, so the client is testable off-browser.
- A fake HID device with real flash state now covers the request
  lifecycle, the setters, and each of the above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
The reentrancy flag it replaced could not tell a nested call from an
unrelated one: a second caller arriving while an operation awaited a
reply read the flag as nesting, ran inline, and overwrote the in-flight
exchange's reply callback. Public methods now take the lock exactly once
and work through unqueued helpers, so there is nothing to detect.

Also:
- open() and close() take turns. Overlapping them let a reopen memoize a
  resolved open while the close was still about to remove the listener,
  wedging every later open.
- setProfile and setDpiStageCount drop the cache before the write rather
  than after the verification read, which could otherwise fail and leave
  a cache describing a device that had already changed.
- dpiY follows dpi through a stage write and a stage switch instead of
  keeping the previous stage's value.
- The concurrency and close tests now reach the failure modes they name:
  the second caller starts only once the device has a report in hand and
  its reply is withheld. Reintroducing the old flag fails the first;
  restoring it passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWUM7JqBNz5bjMfs3ZVQKn
@snekxs
snekxs merged commit 27b9e60 into main Sep 11, 2026
2 checks passed
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.

2 participants