Skip to content

[PRD][Accessories] Accessory protocol PoC #475

Description

@KacperKozak

Problem Statement

Riders need custom hardware to work with Vescape while riding with the phone locked. The current sensor spike shows readings, but has no reusable accessory discovery contract, durable enrollment, ground-clearance tilt binding, or brake-light output. Firmware builders need a small protocol that separates hardware implementation from app-owned behavior.

Solution

Introduce saved Accessories with stable, typed capabilities and a versioned JSON/BLE protocol. The PoC supports a ground-clearance sensor and a separate brake-light accessory. The rider adds an accessory once, configures its behavior on its detail screen, and it operates automatically with the currently connected Board. Native owns connections, settings, and binding execution; firmware measures sensors and renders light states.

User Stories

  1. As a rider, I want to discover compatible nearby accessories, so that I can add my hardware without knowing BLE identifiers.
  2. As a rider, I want to explicitly add an accessory, so that another rider's hardware does not activate my Board controls.
  3. As a rider, I want saved accessories to reconnect when Vescape starts, so that setup is not repeated each ride.
  4. As a rider, I want accessories to work while the phone is locked or backgrounded, so that I can ride with the phone in my pocket.
  5. As a rider, I want saved identity and settings to survive accessory reboot and app restart, so that normal power cycles need no recalibration.
  6. As a rider, I want incompatible firmware and unsupported capabilities explained, so that I understand why an accessory cannot operate.
  7. As a rider, I want live clearance in centimetres on the accessory screen, so that I can calibrate its actual mounting.
  8. As a rider, I want near/far distances, correction direction, and strength, so that correction matches my Board and mounting position.
  9. As a rider, I want valid calibration edits saved automatically, so that there is no Save or Apply workflow.
  10. As a rider, I want an initially unconfigured sensor to wait for calibration, so that mounting assumptions do not command tilt.
  11. As a rider, I want less ground clearance to produce stronger correction, so that the nose or tail is lifted as configured.
  12. As a rider, I want sensor-driven tilt only while riding, so that parking or carrying the Board does not generate input.
  13. As a rider, I want sensor measurements to stop while parked but BLE to stay connected, so that the accessory saves power and can resume promptly.
  14. As a rider, I want opening the sensor screen to resume readings while parked, so that calibration does not require riding.
  15. As a rider, I want missing, erroneous, or stale readings to release tilt smoothly, so that the last correction is not held indefinitely.
  16. As a rider, I want the existing tilt pad to display the commanded input without accepting competing gestures, so that I can see the sensor's action.
  17. As a rider, I want Board Move to remain available while not riding, so that the accessory does not remove an existing control.
  18. As a rider, I want bindings to follow the current Board, so that the PoC needs no per-Board assignment workflow.
  19. As a rider, I want calibration to explain when remounting requires adjustment, so that I do not assume one calibration fits every Board.
  20. As a rider, I want a separate light to receive riding, braking, and hard-braking states, so that it can indicate slowdown.
  21. As a rider, I want brake sensitivity to change slowdown thresholds in either travel direction, so that the light responds appropriately.
  22. As a rider, I want an off/glow preference while not riding, so that the light can remain visible when parked.
  23. As a rider, I want to preview the light states while parked, so that I can inspect behavior without taking a ride.
  24. As a firmware builder, I want explicit telemetry-unavailable state and local command expiry, so that firmware decides how connection problems look.
  25. As a rider, I want fresh-session recovery after a disconnect, so that old commands do not replay after power returns.
  26. As a firmware builder, I want capability IDs, declared ranges/rates, explicit statuses, and acknowledged commands, so that integrations do not depend on sensor models or device names.
  27. As a firmware builder, I want framing, compatibility, and retry rules, so that both implementations agree on behavior under partial delivery and reconnects.
  28. As a firmware builder, I want future capability types to have separate handlers, so that extension does not require a general scripting engine in v1.

Implementation Decisions

  • The existing Board selector is the entry point for accessory management. Show separate Boards and Accessories sections, preserve the current Board selection/add flow, and add an Add accessory action. Accessory rows show connection status and open their configuration screen. Accessories are not nested under individual Boards and continue to target the currently connected Board. Replace the spike's Settings → Sensors entry with this flow.

  • Four modules form the implementation: accessory protocol/session handling; native accessory registry and lifecycle; ground-clearance binding; brake-light binding. Capability handlers expose a small typed interface for configuration, inputs, outputs, and expiry. UI uses native snapshots and sends intents.

  • Use protocol v1's custom service, newline-delimited JSON, manifest handshake, applied-value acknowledgements, explicit set commands, per-session identity, and expiring runtime commands. Its numeric timing defaults are proposed and must be validated, not presented as established hardware performance.

  • Support only ground-clearance and brake-light capability types. Keep unknown types isolated. No generic sensor editor, arbitrary action graph, or firmware-generated Board calibration.

  • Persist enrollment and per-capability settings natively using the existing durable storage architecture, including migration and backup contracts when those stores are affected. Do not store high-rate samples durably for this feature.

  • Bind to the current Board. Calibration is per accessory capability, not per Board. Save only complete valid calibration; preserve the last valid saved value while an editor contains an incomplete value.

  • Reuse established native background lifecycle and Board state/trust checks on both platforms. Do not gate accessory work on React mounts or reuse recording Idle Pause as a substitute for current riding state without verifying semantics.

  • Ground-clearance firmware keeps only VL53L0X support and exposes readings in centimetres. The driver remains independent of protocol and application calibration.

  • Map near clearance to configured maximum input, far clearance to neutral, and interpolate linearly with clamping. Apply explicit direction. Sensor-driven commands never bypass firmware-dependent command trust.

  • Sensor reads while riding or while its detail screen needs readings. Only riding permits tilt. Invalid/stale readings cancel through existing smooth native behavior; loss of the Board link cannot guarantee delivery of neutral.

  • Keep the tilt pad visible as read-only commanded input while the configured accessory is connected. Board Move remains available while parked. Cancel pending sensor return ownership before handing the shared remote-input transport to Board Move.

  • Derive brake states from decreasing speed magnitude. Smooth speed-derived deceleration and use hysteresis. Select and document initial thresholds within the braking slice, then validate on hardware. Constant-speed downhill riding is not braking under this agreed definition.

  • Firmware receives semantic light states and owns animations. App owns sensitivity and parked off/glow preference. Parked preview uses the same expiring command path and restores actual state on exit.

  • Final front/rear hardware is anticipated through capability identity, but arbitration is deferred. The PoC must not silently allow two competing sensor bindings to write tilt concurrently; show an unsupported combination instead.

  • Reuse the spike as prior art, not as proof that the new protocol or locked-screen integration works. Its code is in an open PR, not the current checkout.

Testing Decisions

Test externally observable contracts in all four modules, with shared protocol fixtures across firmware/Kotlin/Swift where practical. Avoid tests for labels, trivial predicates, or duplicated implementation formulas.

  • Protocol/session tests: fragmented and concatenated JSON, limits, version handling, unknown types, duplicate requests, applied rates, expiry, and stale-session callbacks.
  • Registry/lifecycle tests: save/reload settings, launch reconnect, actual measurement standby, preview demand, and current-Board changes. Extend shared persistence contracts if durable schema/operations change.
  • Ground-clearance tests: calibration boundaries, direction, fresh riding/trust gates, smooth release, and Board Move handoff without a late sensor write.
  • Brake tests: forward/reverse speed traces, noise/hysteresis, telemetry gaps, parked preference, preview exit, duplicate state renewal, and light fallback on lease expiry.
  • Prior art includes existing Remote Tilt controller tests on both platforms, TS command/presentation ordering tests, native auto-connect gate tests, and shared persistence migration/reliability tests.
  • Keep firmware output checks possible with a fake light driver until physical hardware exists. That proves state delivery, not real LED behavior. Final hardware validation is a separate human-assisted slice.

Out of Scope

  • Generic sensors, arbitrary mappings, scripting, joysticks, HUDs, powerbank monitoring, and direct accessory-to-accessory links.
  • Per-Board assignment/configuration, competing front/rear tilt arbitration, cloud sync, and durable sensor history.
  • Ultrasonic support and an app sensor-selection UI.
  • Choosing an LED bus/model now. Physical light-driver integration waits for selected hardware.
  • Production authentication guarantees. Explicit enrollment and IDs alone do not authenticate a device; the protocol draft names this remaining limitation.

Further Notes

The complete protocol draft and design are embedded below so this issue is self-contained before repository docs are committed. Implementation prior art: #441 at commit 91ebeaf0fdf3daa8a8151314048de9e663b9149c. Hardware repository: https://github.com/vescape-app/vescape-accessories.

The approved implementation slices are AFK with deterministic validation. The last slice is HITL because the light hardware is not selected and real concurrent BLE behavior must be measured. All cross-platform native changes carry parity links and tests appropriate to the changed contract.

Approved implementation order

  1. Discover compatible accessories
  2. Save and reconnect accessories (after 1)
  3. Calibrate live ground clearance (after 2)
  4. Apply ground-clearance tilt (after 3)
  5. Drive brake-light states (after 2)
  6. Validate connected riding (HITL; after 4, 5 and firmware foundation [Accessories] 7 - Separate firmware device profiles #483)
  7. Separate firmware device profiles ([Accessories] 7 - Separate firmware device profiles #483; implement before brake-light task [Accessories] 5 - Drive brake-light states #480). This follow-up extracts shared C++ runtime, sensor driver/capability and explicit clearance/rear-light builds.

Protocol v1 snapshot

Read the complete protocol v1 snapshot

Vescape Accessory Protocol v1

Status: implementation draft for the PoC. Product behavior is in accessories.md (included in this PRD). This protocol is not implemented yet. Timing, rate, and size limits below are proposed PoC defaults, not measured reliability guarantees.

Ownership

Firmware declares hardware capabilities, measures sensors, and renders outputs. Vescape owns saved calibration, current-Board selection, riding detection, tilt mapping, and speed-based braking detection. Native runs this work while the screen is locked. The wire protocol contains no Board IDs, tilt calibration, raw VESC commands, or LED frames.

V1 recognizes ground_clearance and brake_light. Dispatch by capability type and address by capability ID; do not infer behavior from accessory names or sensor models. Future capability types get their own schemas and handlers.

BLE transport

Assign this custom GATT service and characteristic set for the draft:

UUID Purpose Properties
8d53dc10-1db7-4cd3-868b-8a527460aa84 Vescape Accessory service Advertised
8d53dc11-1db7-4cd3-868b-8a527460aa84 App to accessory Write with response
8d53dc12-1db7-4cd3-868b-8a527460aa84 Accessory to app Notify

These are project-assigned UUIDs, not Bluetooth SIG assigned services. Replace the spike's Nordic UART service on both sides together; that generic service also identifies other hardware and is not accessory identity.

Both directions carry UTF-8 newline-delimited JSON. Each message is one compact object followed by LF. Examples below represent complete lines; send the trailing LF. Split outgoing bytes to fit the negotiated ATT payload. Reassemble bytes before decoding UTF-8 or JSON. Handle partial lines and multiple lines per received chunk. Serialize all chunks of one outgoing message before the next message.

Maximum line length is 4096 bytes excluding LF. An oversized line, invalid UTF-8, or malformed JSON ends the protocol session and disconnects BLE. Clear receive and transmit buffers on disconnect. No plain-text logs or echo replies on these characteristics; use serial for diagnostics.

BLE write completion confirms transport delivery, not command application. Application acknowledgements are defined below.

Identity, compatibility, and enrollment

accessoryId is a factory-provisioned or once-generated persistent UUID that survives reboot and ordinary firmware updates. A display name or BLE address is not this identity. Capability IDs are unique within an accessory and stable across firmware updates. Saved settings key on accessory ID plus capability ID.

An advertised service makes a device discoverable. The rider explicitly adds it before automatic operation. Only saved accessories auto-connect. Read the manifest on every connection and validate identity, version, and recognized capability schemas before using saved settings. If measurement limits change and saved calibration no longer fits, show setup required and keep that binding inactive.

The bootstrap hello and manifest envelope remains readable across versions. Operational v1 schemas ignore unknown optional fields. Unknown capability types can be shown as unsupported while recognized types work. Unsupported protocol versions allow an incompatibility explanation but no operational commands. New optional fields must have omission semantics that preserve existing behavior; changing existing meanings requires a version bump.

Stable IDs are identifiers, not authentication. This PoC enrollment does not claim protection from an accessory impersonating an enrolled ID. Authenticated enrollment is an explicit remaining protocol concern before use beyond controlled prototypes.

Session handshake

  1. Connect and subscribe to notifications.
  2. App sends hello with a fresh random session UUID and supported versions.
  3. Firmware resets volatile runtime state, selects v1 if supported, and returns its manifest.
  4. App validates the manifest, then sends current configuration/state for enrolled capabilities.
  5. Measurements begin only after an enabled configuration is applied. Light output begins when a state command is applied.
{"type":"hello","requestId":1,"sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","supportedVersions":[1]}

Example manifest for an illustrative sensor build. Ranges and rates must describe the actual firmware's supported operation; these example numbers are not VL53L0X guarantees.

{"type":"manifest","requestId":1,"sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","protocolVersion":1,"accessoryId":"b36ed5bd-1d24-460c-8034-aaeaefc5d016","name":"Clearance sensor","firmwareVersion":"0.1.0","capabilities":[{"id":"clearance","type":"ground_clearance","unit":"cm","range":{"min":3,"max":100},"ratesHz":[10,20,30]}]}

A light advertises a capability object such as:

{"id":"rear_light","type":"brake_light"}

If there is no common version, return the same manifest envelope with protocolVersion: null and supportedVersions, then accept no operational commands. If a repeated hello carries the same session ID and request ID, resend the manifest without resetting runtime. A new session ID resets runtime and invalidates previous commands.

Every post-hello message carries the agreed session ID. Ignore messages from other sessions; they never renew a timeout. A disconnect invalidates the session. An app re-handshake also clears its old queues and callback ownership before sending the new hello.

Requests and acknowledgements

Request IDs are strictly increasing integers within a session. App sends at most one outstanding request per accessory, with request chunks serialized. Coalesce unsent state updates to the latest desired state. Fresh readings use a separate notification stream; they do not need acknowledgements.

An acknowledgement means the command was validated and applied, not merely queued. It does not prove a physical LED illuminated. Unsupported values and invalid fields return errors without applying partial changes.

Commands always set desired values. They never toggle or cycle. Retrying the same request ID must not restart an animation or reapply a transition. Firmware retains the latest request's response for duplicate replies, rejects older IDs as stale_request, and rejects reuse of an ID with a different body as request_id_reused. Duplicate retries do not extend command leases; a deliberate renewal uses a new request ID.

Example error:

{"type":"error","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","requestId":2,"code":"invalid_argument","message":"rateHz must be positive"}

V1 error codes: invalid_argument, unknown_capability, unsupported_message, not_ready, hardware_error, stale_request, request_id_reused. A well-formed unknown request returns unsupported_message; an unknown unsolicited message is ignored without renewing any timeout.

Ground-clearance configuration

{"type":"configure","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","requestId":2,"capabilityId":"clearance","enabled":true,"rateHz":20}
{"type":"ack","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","requestId":2,"capabilityId":"clearance","applied":{"enabled":true,"rateHz":20},"leaseMs":2000}

Select the nearest supported rate, choosing the lower rate on a tie, and acknowledge the actual rate. enabled: false stops actual measurement, including a sensor's continuous measurement mode, while preserving BLE connectivity. Require rateHz in both forms to keep commands complete and replayable. Capability configuration is volatile; reboot starts disabled.

Vescape renews the complete configuration while it is needed. Measure while riding or while the sensor screen is open. Only fresh Board riding state, complete valid calibration, and fresh valid sensor samples permit sensor-driven tilt. Screen preview alone never permits tilt. Losing Board telemetry stops tilt and disables measurements unless the screen still needs them.

Readings identify the capability and contain a per-capability sequence number and sample time in milliseconds since the current protocol session began:

{"type":"reading","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","capabilityId":"clearance","seq":1,"sampleTimeMs":125,"status":"ok","value":12.4}
{"type":"reading","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","capabilityId":"clearance","seq":2,"sampleTimeMs":175,"status":"out_of_range"}
{"type":"reading","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","capabilityId":"clearance","seq":3,"sampleTimeMs":225,"status":"error"}

ok requires a finite numeric value in the declared range. Other statuses omit value. A driver unable to distinguish missing hardware from no target must report error, rather than inventing a valid distance. Emit status samples at the configured cadence while enabled, including persistent error/out-of-range states.

Sequence numbers start at 1 and increase across measurement pauses within a session. Drop duplicate or older samples. Sample timestamps use the accessory's monotonic clock; do not subtract them directly from phone timestamps. The app uses local monotonic receipt time for the missing-stream timeout and sequence/timestamp progress to reject regressions. This is a PoC freshness mechanism, not a claim of synchronized clocks or bounded end-to-end latency.

Keep only the latest unsent reading per capability to avoid replaying a backlog. Once transmission of a line has started, finish that line before sending another. Out-of-range/error readings release tilt immediately through existing smooth cancellation; valid readings that stop arriving release it on the stale timeout. Acknowledgements do not refresh sensor freshness.

Brake-light state

{"type":"state","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","requestId":3,"capabilityId":"rear_light","telemetry":"available","mode":"braking","parked":"glow"}
{"type":"ack","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","requestId":3,"capabilityId":"rear_light","applied":{"telemetry":"available","mode":"braking","parked":"glow"},"leaseMs":2000}

mode is riding, braking, hard_braking, or not_riding. parked is off or glow, and affects output only in not_riding. Firmware interprets these semantic states and owns brightness, color, and timing. Renewing an unchanged state does not restart blink phase.

For absent/stale Board telemetry, use this complete replacement state; omit mode:

{"type":"state","sessionId":"b06b9d76-6c73-4d70-a763-d933b294c45b","requestId":4,"capabilityId":"rear_light","telemetry":"unavailable","parked":"glow"}

Firmware chooses how telemetry-unavailable looks. Connection loss and expired app commands are detected locally and may have different firmware-defined behavior. There is no outgoing “disconnected” command.

Parked preview sends the same state schema with preview: true. This optional field defaults to false and labels simulated state; it makes no claim that Board telemetry exists. Permit telemetry: "unavailable" plus a mode only when preview is true. Vescape sends the actual current state immediately on closing preview. Firmware's lease also ends preview if the app disappears.

PoC timing and failure defaults

Setting Proposed default
Preferred sensor rate 20 Hz, resolved against manifest and confirmed by ack
Missing sensor stream 300 ms, starting at enabled ack or latest accepted sample
Runtime command lease 2000 ms per capability
App renewal interval 500 ms, send state changes immediately
Request response timeout 500 ms; retry once with the same ID
Handshake response timeout 3000 ms; disconnect and use normal reconnect policy

For a selected rate below 10 Hz, use max(300 ms, 3 * sample period) for missing-stream detection. These values need validation under concurrent Board and accessory BLE traffic on Android and iOS.

Any second request timeout marks the accessory unavailable, cancels its active sensor binding, and disconnects it for a fresh handshake. A malformed response follows the same failure path. Firmware never renews a lease on malformed, rejected, duplicate, or wrong-session commands. On lease expiry or disconnect it stops measurements and hands light output to its local unavailable behavior.

If the Board link is itself gone, the app cannot promise to deliver a neutral command. Clear native input ownership and pending writes; receiver-side Board timeout remains the final fallback. If only the accessory dies and the Board link remains usable, use the existing smooth Remote Tilt cancellation.

Implementation checks

  • Split messages at every byte boundary, including UTF-8 characters; also accept concatenated lines.
  • Reject oversized and malformed messages without an unbounded receive buffer.
  • Exercise version rejection, unknown capabilities, and changed capability limits after reconnect.
  • Drop an ack, retry, and confirm that a light animation does not restart or a lease extend twice.
  • Stop app renewals with BLE connected; observe sensor standby and firmware-owned light fallback.
  • Stop readings with configuration acks still arriving; observe tilt cancellation.
  • Reconnect with queued old commands; confirm they cannot affect the new session.
  • Validate measurement standby and locked-screen operation with current development builds on both platforms.

Remaining implementation work

Inspect and reuse the existing native riding-state predicate and Board telemetry freshness rules. Define braking smoothing and sensitivity thresholds in the app, outside this wire protocol. Choose the real sensor manifest limits from driver configuration and measurements. Authenticated enrollment and simultaneous front/rear tilt arbitration remain outside this PoC draft.

Product design snapshot

Read the complete product design snapshot

Accessories

Design in progress. These are agreed requirements, not implemented behavior.

Initial scope

  • A board-mounted distance sensor controls Remote Tilt from ground clearance.
  • A separate light accessory responds to Board braking telemetry.
  • Enrolled accessories auto-connect when the app starts and operate through the native runtime while the screen is locked or the app is backgrounded.
  • In v1, every Board-related binding targets the currently connected Board. There is no Board selector or per-Board binding configuration.
  • Sensor calibration is saved once per binding. Moving the sensor to another Board or mounting position requires manual recalibration; explain this beside the calibration controls.

Discovery and enrollment

  • The existing Board selector is the entry point for accessory management. Show separate Boards and Accessories sections, preserve the current Board selection/add flow, and add an Add accessory action. Accessory rows show connection status and open their configuration screen. Accessories are not nested under individual Boards and continue to target the currently connected Board. Replace the spike's Settings → Sensors entry with this flow.

  • Compatible accessories advertise a shared Vescape Accessory BLE service UUID, independent of their display names. The draft UUID is specified in protocol v1 (included in this PRD).

  • After connecting, Vescape reads a manifest containing a stable accessory ID, display name, protocol version, firmware version, and capabilities.

  • Each capability has a stable local ID and a recognized type. Ground-clearance inputs declare centimetres as their unit; brake-light outputs receive semantic states.

  • Measurement capabilities declare supported measurement rates and numeric measurement ranges in the manifest. Vescape uses these hardware limits to validate requests; they are distinct from rider-selected near/far calibration distances.

  • One accessory may expose multiple capabilities. Unknown types appear as unsupported without blocking recognized capabilities.

  • Nearby accessories are added explicitly by the rider. Only saved accessories auto-connect; discovery alone never authorizes a tilt input.

  • V1 supports exactly two capability types: ground_clearance and brake_light. Generic sensor support and arbitrary binding editors are out of scope.

  • Recognized types provide predefined behavior and suggested settings that the rider can adjust. Keep capability-specific setup and runtime handling separate so future types can be added without redesigning discovery.

  • Capability types and stable local IDs are separate: adding another type does not change existing capability identities. Protocol evolution must preserve recognized capabilities when an accessory also advertises unknown types.

Ground-clearance sensor

  • Use only the VL53L0X time-of-flight sensor for this PoC. Remove the spike's ultrasonic support when implementing the firmware changes; there is no sensor-selection UI.
  • Firmware exposes one ground_clearance capability with readings in centimetres, independent of the underlying sensor driver.
  • Calibration sets near and far distances in centimetres, correction direction, and maximum Remote Tilt input.
  • Less ground clearance produces stronger correction. Mounting at the nose or tail determines the appropriate correction direction.
  • Missing or stale readings and accessory connection loss use the existing smooth Remote Tilt cancellation behavior while the Board connection remains available.
  • Disable sensor measurements and sensor-driven tilt while not riding, to save accessory power and prevent unwanted input. Keep BLE connected in standby so Vescape can resume measurements when riding starts. Reuse the existing native riding-state predicate after checking its implementation; standby commands are specified in protocol v1.
  • Reuse existing app controls and native Remote Tilt behavior.
  • The sensor accessory screen shows live distance in centimetres and lets the rider configure near/far distances, correction direction, and strength. Measurements run while this screen is open even when not riding; sensor-driven tilt remains disabled while not riding.
  • The rider supplies Board-specific calibration during initial setup; firmware does not supply an assumed mounting calibration. Once configured, the binding operates automatically during riding with no separate arming step.
  • Calibration edits save automatically when complete and valid, with near distance strictly below far distance. There is no Save or Apply step; a complete valid calibration activates the binding automatically for riding.
  • The eventual hardware includes front and rear sensors. This PoC focuses on capability types and defers arbitration between competing tilt inputs; stable capability IDs leave room for both later.
  • While a configured ground-clearance accessory is connected, the Remote Tilt pad remains visible as a read-only indicator of commanded tilt, with manual input disabled. It displays commanded input, not measured Board pitch.
  • Board Move remains available while not riding, when sensor-driven tilt is inactive. Both use the existing native remote-input controller; transitions must prevent a pending sensor return from overriding Board Move.

Wire protocol

  • Use a small versioned JSON protocol for the PoC, covering the manifest, sensor readings, and app commands. Protocol v1 (included in this PRD) defines the implementation draft, including schemas and proposed defaults.
  • Frame messages as newline-delimited JSON: one compact JSON object followed by \n. Both receivers buffer BLE chunks until a complete line arrives; BLE packet boundaries are not message boundaries.
  • Enforce the protocol's fixed maximum message size and disconnect on malformed or oversized messages.
  • Receivers ignore unknown optional fields within a supported protocol version. Changes to existing field meanings require a new protocol version.
  • Unsupported protocol versions block operational commands and bindings. Vescape may show the discovered accessory with an incompatibility explanation, but does not activate its controls.
  • Configuration requests carry a request ID. Firmware responds with a matching acknowledgement containing the settings actually applied, including the accepted measurement rate, or an explicit error. Vescape does not treat a successful BLE write as configuration acceptance.
  • Sensor readings are unacknowledged streams; individual samples do not require a round trip.
  • Commands set explicit desired values or states, such as measurement enabled/disabled or braking state. Do not use toggle/cycle commands: repeating a request must preserve the same result without restarting an unchanged light animation.
  • Runtime commands expire unless renewed by Vescape. On expiry, sensors return to measurement standby and lights use their firmware-defined unavailable behavior. Protocol v1 proposes timeout values for PoC validation.
  • Detect loss in both directions without requiring a final disconnect message: accessories detect expired app commands, and Vescape detects missing expected sensor readings. If accessory power dies, stale sensor input releases tilt through the existing smooth cancellation while the Board connection remains available.
  • Keep unavailable Board telemetry distinct from an expired app command: Vescape can remain responsive while reporting that Board telemetry is unavailable.
  • Every reconnect starts a fresh protocol session: read and validate the manifest again, then resend the current applicable configuration and state. Discard old queued commands and ignore acknowledgements or callbacks belonging to the previous session.
  • Sensor reading messages identify their capability and carry an explicit status: ok with a numeric value, out_of_range, or error. Ground-clearance values are in centimetres.
  • Out-of-range readings, sensor errors, and stale or missing readings release sensor-driven tilt through the existing smooth return behavior. A missing value never represents a valid maximum-distance sample.

Brake light

  • Vescape sends semantic states: riding, braking, hard braking, and not riding. Accessory firmware owns brightness, colors, and blink patterns. The initial light's intended behavior is dim red while riding, brighter red when braking, and blinking red under hard braking.
  • Detect braking from decreasing Board speed magnitude over time, in either travel direction. Constant-speed riding does not activate braking, including downhill riding.
  • One sensitivity control adjusts the deceleration thresholds for brighter red and hard-braking blinking. Smoothing and threshold values remain to be resolved.
  • While not riding, the rider can choose between light off and a steady red glow, for example while leaving the Board outside a shop.
  • Non-riding light behavior is independent of the sensor's measurement standby.
  • Accessory firmware owns the visual behavior when disconnected or when Board telemetry is unavailable; Vescape does not prescribe a loading pattern or fallback color.
  • Vescape owns braking detection and sensitivity; it does not stream individual LED frames.
  • The light settings screen offers a parked preview of riding, braking, and hard-braking states on the actual accessory. Closing the preview restores automatic behavior.
  • The accessory detects its own connection loss. While connected, Vescape explicitly reports Board telemetry unavailability. Missing app updates must also be detectable without receiving a final message.

Remaining work

  • Implement and validate the protocol draft across firmware and native app runtimes.
  • Reuse native riding detection and Board telemetry freshness rules.
  • Tune brake detection smoothing and sensitivity thresholds using ride data.
  • Validate protocol timing defaults under concurrent Board and accessory traffic.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessoriesAccessory discovery, protocol, configuration, and Board bindingsenhancementNew feature or requestready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions