OpenSnek is intentionally structured so new device support can land without rewriting the app shell. The main rule is simple: if you change protocol behavior, update the protocol docs and tests in the same change.
Read these first:
- docs/protocol/PROTOCOL.md
- docs/protocol/USB_PROTOCOL.md
- docs/protocol/BLE_PROTOCOL.md
- docs/protocol/PARITY.md
- OpenSnek/README.md
OpenSnek/Sources/OpenSnekCore- shared device models, device profiles, button layouts, persistence keys
OpenSnek/Sources/OpenSnekProtocols- shared BLE vendor framing and USB HID report helpers
OpenSnek/Sources/OpenSnekHardware- shared transport/session code
OpenSnek/Sources/OpenSnek- app bridge, app services, SwiftUI
OpenSnek/Sources/OpenSnekProbe- fast CLI/probe workflows for protocol validation
captures/- raw protocol captures and reverse-engineering reference data
The preferred workflow is:
-
Identify the device precisely.
- Record USB vendor/product IDs.
- Record Bluetooth vendor/product IDs if BLE is supported.
- Record marketing name, serial behavior, and any firmware/build strings the device reports.
-
Capture baseline behavior before writing code.
- Capture untouched/default state.
- Capture one setting change at a time.
- Capture both write traffic and the readback/refresh that follows.
- Keep separate captures for USB and BLE if both transports exist.
-
Add or extend the device profile in
OpenSnek/Sources/OpenSnekCore/DeviceSupport.swift.- Add a
DeviceProfileID. - Register identities for each supported transport.
- Define button layout and visible/writable slots.
- Define capability flags honestly. Do not expose UI controls unless the protocol path is proven.
- Add a
-
Reuse shared transport/protocol layers before adding new ones.
- USB report framing belongs in
OpenSnekProtocolsorOpenSnekHardware. - BLE vendor exchange sequencing belongs in
OpenSnekHardware. - Device-specific orchestration belongs in the profile/bridge layer.
- Do not duplicate transport helpers in
OpenSnekProbeor app code.
- USB report framing belongs in
-
Only add new UI when the device truly exposes a new feature family.
- If the device is just another mouse with the same capabilities, the app should work from profile metadata alone.
- If you find yourself branching the UI on raw PID/transport strings, stop and move that logic into profile capabilities instead.
-
Add tests with the code change.
- Add pure parsing/building tests in
OpenSnekTests. - Add profile-resolution tests for new identities/capabilities.
- Add transport/protocol tests for newly decoded payloads.
- If hardware is available, run the hardware-gated tests and report the outcome.
- Add pure parsing/building tests in
-
Update docs in the same change.
- Update protocol docs if bytes/commands/interpretation changed.
- Update CHANGELOG.md for user-visible behavior.
- Update this guide if the onboarding workflow changes.
The fastest way to decode a new command path is controlled comparison.
- Change exactly one setting per capture segment.
- Keep a default read, a write, and a readback close together.
- Label captures with what changed and what transport was used.
- Prefer official-app captures or captures taken from a known-good control path.
Look for:
- HID feature report request/response pairs
- command class and command ID bytes
- transaction ID behavior
- status byte and checksum behavior
- payload bytes that change when the UI changes one setting
Useful questions:
- Is this a true read/write path or just telemetry?
- Does the response echo the request before returning the real payload?
- Are stage IDs stable tokens or UI indices?
- Does the device require a follow-up read to settle?
When adding USB support, keep framing logic in OpenSnek/Sources/OpenSnekProtocols/USBHIDProtocol.swift or shared USB session files, not inline in app/probe code.
Look for:
- ordered write/notify exchanges on the vendor characteristic
- request IDs or sequence bytes
- key-family bytes that distinguish feature groups
- payload length changes and selector bytes
- the readback notification that proves what the device accepted
Useful questions:
- Is the operation serialized, or are multiple writes being interleaved?
- Is a byte a slot ID, stage ID, effect selector, or a value count?
- Does the readback use the same payload shape as the write?
- Is the effect native on-device, or only software-driven frame streaming?
When adding BLE support, keep operations sequential per connection. Do not introduce parallel writes that race the vendor session.
Use this loop:
- Decode the bytes in docs or notes.
- Add the pure payload builder/parser first.
- Write a unit test with the captured bytes.
- Reuse the shared transport/session layer.
- Add bridge/profile integration.
- Validate with
OpenSnekProbebefore relying on the full app UI. - Validate readback, not just write ACKs.
Install the local pre-push hook once per checkout so formatting, lint, and unit tests run before
git push:
./OpenSnek/scripts/install_git_hooks.shRun the same guard manually before publishing a branch:
./OpenSnek/scripts/pre_push_checks.shCore package tests:
swift test --package-path OpenSnekApp/probe builds:
swift build --package-path OpenSnek
./OpenSnek/scripts/xcodebuild_generated.sh -scheme OpenSnek -destination 'platform=macOS' build
./OpenSnek/scripts/xcodebuild_generated.sh -scheme OpenSnekProbe -destination 'platform=macOS' buildBLE probe iteration:
swift run --package-path OpenSnek OpenSnekProbe dpi-read
swift run --package-path OpenSnek OpenSnekProbe dpi-set --values 1600,6400 --active 2Hardware-gated reliability checks:
OPEN_SNEK_HW=1 swift test --package-path OpenSnek --filter HardwareDpiReliabilityTests
OPEN_SNEK_HW=1 swift test --package-path OpenSnek --filter HardwareUSBButtonRemapTests- Keep changes scoped by behavior.
- Include protocol docs and tests with protocol changes.
- Call out capture files used for validation.
- State whether hardware validation was
pass,fail, orskipped.