Skip to content

feat(sinowealth): support the Cosmic Byte Kilonova 3335IC through its receiver - #249

Draft
m4milaad wants to merge 4 commits into
OpenMouse-Project:mainfrom
m4milaad:feat/sinowealth-kilonova
Draft

m4milaad wants to merge 4 commits into
OpenMouse-Project:mainfrom
m4milaad:feat/sinowealth-kilonova

Conversation

@m4milaad

@m4milaad m4milaad commented Oct 23, 2026 •

Copy link
Copy Markdown

Device

Cosmic Byte Kilonova 3335IC, through its 2.4 GHz dongle, which enumerates as SINOWEALTH 258a:002f, the M690 PRO receiver's product id. Windows 11. Everything here was verified read-only against that one receiver; the wired path of this mouse has not been seen yet.

Today the registry claims the id by USB shape, and then the settings-block check refuses the device with "not the Redragon M690 PRO's": the Kilonova answers the same SinoWealth bank framing (libratbag's driver-sinowealth.c) with its own footprints. Identify replies ASCII "2315" ("2945" on the M690 PRO) and byte 9 of the settings bank is 0x11 (0x13). Rather than losing the receiver, this PR gives the M690 PRO client a second, read-only profile.

What the PR does

  • Adds src/sinowealth/index.ts, a framing-family module (the src/compx/ pattern): the shared report/length/command constants, the two known profiles (M690 PRO "2945"/0x13, Kilonova 3335IC "2315"/0x11), and encode/check/decode helpers for identify, link and status. Its duplicated numbers are pinned to @openmouse/protocol/redragon by src/sinowealth/index.test.ts. Exported as a ./sinowealth package subpath and from the package root.
  • RedragonM690ProHidClient resolves its settings block against those profiles: the M690 PRO behaves exactly as before; a Kilonova is recognized and read; a model byte on this shared receiver id that matches no profile is still refused with the same NotM690ProError message as before.
  • Kilonova status: brand "Cosmic Byte", name, battery and link decoded from report 5 when the transport can read it (OpenMouse Bridge or node-hid; null otherwise), "2.4 GHz receiver" / "mouse off or asleep" connection detail, settings grid hidden, and every write path refused with a read-only note before any write is attempted.
  • MouseStatus["brand"] gains "Cosmic Byte". The registry entry keeps its "Redragon" label, with a comment pointing at the GearHub precedent for a device wearing another brand's label until its own status arrives.
  • Evidence: captures/cosmic-byte-kilonova-3335ic/ (the read-only node-hid session) and docs/kilonova-3335ic.md (captures, observed facts vs hypotheses, what would unlock the rest).

What was observed on real hardware (read-only, mouse awake and linked, 70 % battery)

  • identify: [05 01 32 33 31 35 00 00] -> "2315"
  • link: [05 80 01 01 00 00 00 00] -> linked
  • status: [05 90 11 46 00 00 00 00] -> wireless, battery 0x46 = 70 %
  • settings bank 05 21 -> 154 bytes, model byte 0x11 at 9, a5 00 marker at 152-153
  • button bank 05 22 -> 88 bytes, payload all zeros
  • the settings bank also read through Chrome and reached the app's model-byte check, which is how the need for this PR was noticed. The report-5 STALL behavior for Chrome on this unit is inferred from the family (the M690 PRO's capture chrome-report5-stall.hex), not captured here.
  • No write was ever tried on hardware.

Not decoded, and left alone

The Kilonova's bank interior. Its stage-table bytes (observed 00 07 00 0f 00 1f 00 3f 00 9f after the stage count at byte 11) do not name the M690 PRO's DPI positions 1-24, so OpenMouse hides the settings grid rather than mapping the M690 PRO's layout onto another device's memory. Byte 10 is 0x04 (1000 Hz on the M690 PRO map), but the rate was not measured, so nothing decodes it. The zero-payload button bank keeps its slot grid as a hypothesis only.

Consequences for the application

The brand map already keys by string, so the device type is accepted as-is; but two MouseUiHints this driver relies on (statusNote, forceShowBattery) were not rendered by the app, so with the current shell the Kilonova's page shows the device and nothing else. The matching shell change is OpenMouse-Project/openmouse#1201 (draft): it renders the status note on the connected status line and keeps a – battery column until OpenMouse Bridge can read the percent. I verified both sides locally: npm run check in this repository (build, 2677 tests, npm pack --dry-run), then npm run build, npm test and npm run size in OpenMouse with the patched shell, all pass. A matching openmouse-landing-page supported-devices entry for the Kilonova will follow in that repository; src/features/hardware-test/supported-devices-data.ts stays untouched until it lands.

CI note: first-time contributor, so the workflows need maintainer approval to run.

… receiver

Its dongle enumerates as SinoWealth 258a:002f, the M690 PRO receiver's
product id, and answers the same bank framing with its own footprints
(identify "2315", settings-bank model byte 0x11). RedragonM690ProHidClient
now resolves a profile from the settings block: the M690 PRO keeps its full
behavior, the Kilonova 3335IC is recognized and read for identity, link and
battery, and every write path is refused. Nothing past the model byte of the
Kilonova's bank is decoded, and no write has been tried on hardware.

Evidence: captures/cosmic-byte-kilonova-3335ic/ (node-hid, read-only) and
docs/kilonova-3335ic.md. The framing numbers shared with the M690 PRO are
pinned together by src/sinowealth/index.test.ts.
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added type: enhancement PR title category: enhancement area: protocol Protocol codecs and shared types area: drivers Device drivers and transport integration area: tests Protocol and driver tests area: captures Protocol captures and device evidence area: documentation Documentation area: dependencies Dependency manifests and lockfiles area: ci Automation and build configuration labels Oct 23, 2026
The Kilonova 3335IC's settings bank is still not decoded enough to write,
but its DPI table is now understood and can be reported as read-only data:

- The stage table lives at 0x0d in the settings bank: six 16-bit
  little-endian values where the DPI is (index + 1) * 100. The active stage
  is the high nibble of byte 0x0b (1-based) and the stage count is the low
  nibble, the same semantics as the M690 PRO.
- Evidence: the mouse's own DPI button reports each stage change on input
  report 7 ([11 04 stage byte ..]) through OpenMouse Bridge; pressing
  through all six stages reproduced the bank's exact values, and a
  power-cycle confirmed the mouse boots to the saved active stage.

The driver now reports dpi, dpiStages and activeDpiStage with
valuesVerified: true while settingsReady stays false, so the app shows the
DPI read-out but still refuses every write. Adds a
sinowealthDecodeKilonovaConfig unit test and updates the recognition test.
Comment thread src/drivers/redragon/m690-pro-hid.ts
/** Snaps to the nearest vendor DPI value, writes it, and returns that value. */
async setDpiStageValue(stage: number, dpi: number): Promise<number> {
if (this.kilonova) {
const label = redragonM690ProNearestDpi(dpi);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High redragon/m690-pro-hid.ts:368

Kilonova DPI writes throw for supported values such as 400, 1600, and 6400: redragonM690ProNearestDpi snaps them to 500, 1500, and 6500, which sinowealthWithKilonovaStageDpi rejects. Snap to the Kilonova's supported DPI values instead.

Suggested change
const label = redragonM690ProNearestDpi(dpi);
const label = [400, 800, 1600, 3200, 6400, 16000].reduce((nearest, value) => Math.abs(value - dpi) < Math.abs(nearest - dpi) ? value : nearest);
🤖 Copy this AI Prompt to have your agent fix this:
In file @src/drivers/redragon/m690-pro-hid.ts around line 368:

Kilonova DPI writes throw for supported values such as `400`, `1600`, and `6400`: `redragonM690ProNearestDpi` snaps them to `500`, `1500`, and `6500`, which `sinowealthWithKilonovaStageDpi` rejects. Snap to the Kilonova's supported DPI values instead.

Comment on lines +367 to 374
if (this.kilonova) {
const label = redragonM690ProNearestDpi(dpi);
await this.writeBlocks((block) => sinowealthWithKilonovaStageDpi(block, stage, label), (block) => block);
return label;
}
const label = redragonM690ProNearestDpi(dpi);
await this.writeConfig((block) => redragonM690ProWithStageDpi(block, stage, label));
return label;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High redragon/m690-pro-hid.ts:367

On a fresh client, setDpiStageValue(0, 800) writes the Kilonova’s stage as 400 DPI but reports success, silently changing the onboard value. The branch selects redragonM690ProWithStageDpi before writeBlocks queues and completes device identification, so the Kilonova byte whitelist accepts the wrong encoding; choose the encoder inside the callback, after identification.

-    if (this.kilonova) {
-      const label = redragonM690ProNearestDpi(dpi);
-      await this.writeBlocks((block) => sinowealthWithKilonovaStageDpi(block, stage, label), (block) => block);
-      return label;
-    }
     const label = redragonM690ProNearestDpi(dpi);
-    await this.writeConfig((block) => redragonM690ProWithStageDpi(block, stage, label));
+    await this.writeBlocks(
+      (block) => this.kilonova
+        ? sinowealthWithKilonovaStageDpi(block, stage, label)
+        : redragonM690ProWithStageDpi(block, stage, label),
+      (block) => block,
+    );
     return label;
🤖 Copy this AI Prompt to have your agent fix this:
In file @src/drivers/redragon/m690-pro-hid.ts around lines 367-374:

On a fresh client, `setDpiStageValue(0, 800)` writes the Kilonova’s stage as 400 DPI but reports success, silently changing the onboard value. The branch selects `redragonM690ProWithStageDpi` before `writeBlocks` queues and completes device identification, so the Kilonova byte whitelist accepts the wrong encoding; choose the encoder inside the callback, after identification.

…ites to known bytes

Replace the hardcoded Kilonova DPI index map with the (dpi/100)-1 formula
so any 100-step value 400-16000 can be written. Detect the Kilonova in
writeBlocks and refuse any change that touches bytes beyond the known
DPI/polling fields (0x0a low nibble, 0x0b, DPI stages 0x0d-0x18), so
button/lighting/layout bytes are never guessed at. Expose Kilonova
polling rates and the full DPI option range, and add the dpiStageEditor
UI hint. setButtonMapping throws 'read-only' for Kilonova.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: captures Protocol captures and device evidence area: ci Automation and build configuration area: dependencies Dependency manifests and lockfiles area: documentation Documentation area: drivers Device drivers and transport integration area: protocol Protocol codecs and shared types area: tests Protocol and driver tests type: enhancement PR title category: enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant