RUNXN BROWSER BRIDGE v1 — FIRMWARE CONTRACT This is a supported connection format, not evidence that an existing board is already configured for it. Match your firmware to this contract or set its Bluetooth service/characteristic UUIDs in Rig setup before pairing. The page does not auto-detect undocumented vendor packet formats. TRANSPORTS Bluetooth Low Energy: Custom service: d4a82aa0-8b74-4f36-9f12-91a60b2e7c10 Readings (notify): d4a82aa1-8b74-4f36-9f12-91a60b2e7c10 Palm command (write): d4a82aa2-8b74-4f36-9f12-91a60b2e7c10 The browser requests permission for the custom service as well as standard battery_service and generic_access. Those standard services do not provide stride telemetry. Pairing is initiated by a user click, never proximity alone. Supply notify/indicate for readings and write or write-without-response for palm commands. A pacer-only unit can leave the reading ID empty in Rig setup. A sensor-only unit can leave the palm command ID empty. USB Serial: navigator.serial.requestPort(), 115200 baud, newline-delimited UTF-8 JSON/CSV. Do not assume the board sends this format merely because a port opens. Only text mode is supported over USB Serial. Direct WebUSB fallback: Used when Web Serial is unavailable, or selected in Rig setup. Requires a unique accessible vendor-specific (class 0xFF) bulk-IN interface. Optional bulk-OUT carries one-byte palm commands. Set the interface number if the device has multiple matching interfaces. Ambiguous alternates or configurations are rejected instead of guessed. No unsupported class-driver emulation or automatic driver replacement is attempted. This is a USB bulk transport, not a serial port; baud rate is not applicable. Text mode only. Windows/other OS driver requirements depend on the firmware and device. TEXT FRAMES One full JSON object per newline, or a CSV header followed by newline-delimited rows. BLE also accepts a complete JSON object in one notification. Text can be fragmented across notifications/reads; fragments are buffered. CRLF is accepted. Example JSON (invented values, for firmware integration only): {"t_ms":1000,"landing_cm":8,"braking_ms":30,"ground_ms":220,"pace_s_km":240,"phase_ms":0,"piezo_mv":42,"haptic":1,"session_id":"RT-402","fork_dive_mm":0,"braking_distance_m":0} Required fields: t_ms Device timestamp in milliseconds, 0..4294967295; strictly increasing per connection. Reset/reconnect on a clock restart. landing_cm Signed foot landing relative to hips: -20..40 cm. Positive ahead. braking_ms Detected braking/check interval: 0..300 ms, no longer than ground_ms. ground_ms Ground contact: 80..550 ms. pace_s_km Pace in seconds per kilometre: 90..1800. Optional fields: phase_ms Footstrike phase, 0..600 ms; defaults to landing (0). Supply this per sample to animate the leg from actual rig timing. Without it the pose shows the incoming landing position, not a fake run. piezo_mv -5000..5000 mV; displayed as supplied, not converted to brake force. haptic 0/none, 1/brake, 2/pull, 3/rhythm. Defaults to 0. Incoming cues only animate the page; they are never echoed back to hardware. session_id 1..80 ASCII characters, starting with an alphanumeric character; subsequent characters: alphanumeric, underscore, dot, colon, hyphen. fork_dive_mm 0..300 mm, shown in the hardware readout when supplied. braking_distance_m -100..100 m, shown in the hardware readout when supplied. JSON numeric fields must be actual finite numbers, not strings or nulls. The rig computes calibrated measurements; raw piezo voltage alone is insufficient to infer landing position or brake force. The page does not invent missing fields. CSV example: t_ms,landing_cm,braking_ms,ground_ms,pace_s_km,phase_ms,piezo_mv,haptic 1000,8,30,220,240,0,42,1 1020,8,30,220,240,20,18,0 Headerless CSV uses the same eight-column order. A header may reorder columns and include the optional session_id, fork_dive_mm and braking_distance_m fields. Use simple unquoted fields; commas and newlines inside IDs are not supported. Maximum text record: 4096 characters. Oversized/bad records, out-of-range data, duplicate/backwards timestamps and incomplete required fields are rejected. BINARY BLUETOOTH MODE Select "8 Float32 values / Bluetooth only" in Rig setup. Each notification must contain EXACTLY 32 bytes: eight IEEE754 Float32 little-endian values in this order: t_ms, landing_cm, braking_ms, ground_ms, pace_s_km, phase_ms, piezo_mv, haptic The haptic code is represented as a Float32 integer 0..3. All fields are required. Do not fragment a binary frame across BLE notifications. Negotiate an ATT MTU large enough for a 32-byte notification payload, or use fragmented text instead. Float32 timestamps lose fine precision at large uptime values; use text with integer timestamps for long sessions. Binary mode has no session ID field. PALM OUTPUT The user must enable "Send palm cues to my rig" and click a feeling button. One byte is written to the configured GATT characteristic, serial writer or WebUSB bulk-OUT endpoint. No proprietary commands are guessed. 0x00 Stop / silence 0x01 Two short brake taps 0x02 Forward cue 0x03 Rhythm at 180 steps per minute, up to three seconds The firmware defines bounded actuator intensity and MUST stop every command within three seconds. Stop on loss of the connection or its own watchdog. This page does not control treadmills, resistance motors, brakes or propulsion. Output is rate-limited and serialized. Stop is best effort when the page is hidden, disconnected or the user presses Stop. Browser closure cannot guarantee delivery of a final stop byte; the firmware watchdog is mandatory. A successful write means bytes were sent, not that a physical sensation was verified. NFC TAP IN Tap In explicitly requests Web NFC permission on supported Android Chrome. Supported NDEF records: text ID, JSON text/application-json with session_id or tag_id, or HTTPS RunXn/RipTags/RippleXn URL with session_id or tag_id query field. Example: https://riptags.com/?session_id=RT-402 The reader stops after a valid tag. No NFC writes, URL navigation or remote fetches. The tag ID is displayed locally; no authentication or calibrated spatial lock is inferred. If the rig reports a different session_id, the mismatch is shown. LIVE / PREVIEW BEHAVIOR Only a validated full reading switches to live mode. Mere connection or battery notifications never do. Three seconds without valid new data returns the visual stage to its eight-run preview while keeping the connection status explicit. Selecting a numbered run, replaying or scrubbing the preview disconnects the rig. Leaving/hiding the page closes the connection; reconnect explicitly to resume. The renderer consumes the newest valid frame once per browser animation frame. Metrics show supplied readings. The stride pose and normalized force curves remain illustrations, not reconstructed video or measured force waveforms. Peak visual amplitudes are bounded; measured numeric values are not replaced with sample runs. No 0.1 mm accuracy or calibration is certified by Bluetooth, Serial, USB or NFC. Data/configuration remain in the current tab; no uploads or persistence. BRING-UP CHECKLIST 1. Test the firmware protocol on a stationary rig, with bounded outputs. 2. Serve/open https://runxn.com; choose the correct profile and pair/plug in. 3. Confirm WAITING before valid data, LIVE only after valid data. 4. Verify live numeric values and phase against an independent capture. 5. Stop data for >3 seconds; verify preview fallback and explicit stale status. 6. Unplug/disconnect, reconnect, cancel each picker, and test denied permission. 7. Enable palm output, verify 0x01/0x02/0x03 and physical duration/intensity; test stop, tab hiding and firmware watchdog independently. 8. Tap a real NDEF RipTag; verify local ID and a deliberate rig-ID mismatch. 9. Confirm the eight original runs, ghost toggle, replay and legal footer remain. This browser implementation has automated transport/parser tests. A physical rig, actuator and NFC phone must still pass this bring-up before a hardware pilot.