Skip to content

feat(api): fix touch velocity, name buttons, close stalled subscribers - #14

Open
hyperb1iss wants to merge 4 commits into
mainfrom
nova/input-events
Open

hyperb1iss wants to merge 4 commits into
mainfrom
nova/input-events

Conversation

@hyperb1iss

Copy link
Copy Markdown
Owner

Hypercolor is about to consume the blocksd event stream so Lightpad touches and LUMI key presses can drive effects. Three problems in that stream would have leaked straight into the integration. Touch velocity decoded with the wrong sign and scale, button events could not say which button was pressed, and a subscriber that fell behind silently stopped receiving events while its connection stayed open. This change fixes all three and corrects the protocol docs that described touch and button payloads wrongly.

🛠️ What changes

Touch velocity is signed. The firmware sends vx, vy, and vz as two's complement bytes, which the ROLI SDK converts with IntegerWithBitSize::toBipolarFloat. blocksd treated them as offset binary around 128, so a small positive velocity of 1 decoded as -1.0 and the most negative value decoded as 0.0. Velocities now match the SDK exactly: raw values map to [-1, 127/128] and zero stays zero.

Buttons say which button. Button events now carry the protocol button_id, its SDK function name as button, and the device timestamp. The SDK reports a button as an index into each model's button list, so the registry holds those lists in SDK order and the device group resolves the name from the block type.

Device button_id → button
LUMI Keys 0 mode, 1 down, 2 up
Lightpad, Seaboard 0 mode
Loop, Live, Touch, Developer SDK order, ending down, up

An index a model does not define reports "button": null with the raw ID intact. Touch events also gain timestamp, the device clock in milliseconds, so a consumer can space out a burst of touches delivered together. Every change is additive; existing fields keep their meaning apart from the velocity fix.

Overflowing subscribers are disconnected. Each subscriber still has a 1024-event queue. When it fills, the broadcaster now shuts the queue down and remembers it. The transport's event writer sees the shutdown, sends {"type": "error", "message": "event queue overflowed; reconnect and subscribe again"}, and closes the connection (WebSocket close status 1008). Clients learn they missed events and can reconnect, resubscribe, and rediscover instead of holding a healthy-looking socket that never delivers another event. Other subscribers are unaffected.

Docs. The message tables described touch and button payloads as starting with a 7-bit device index. The device index lives in the SysEx header; the payload starts with a 5-bit timestamp offset, which is how both the SDK decoder and blocksd read it. The device reference no longer claims button IDs are missing.

🎯 Invariant: broadcasts never raise into device callbacks

Event broadcasts run inside DeviceGroup touch and button callbacks, so an exception there would tear down the USB group for every client. The overflow path keeps that property in two ways. A subscribe on a connection whose queue was shut down is refused. A shut-down queue that is still subscribed is dropped on the next broadcast instead of raising QueueShutDown.

🧪 Validation

  • just check on 1dd88d2 passes: 670 Python tests, Ruff, ty, dashboard checks, docs build, and the fresh installed-wheel check.
  • The velocity decoder matches the SDK formula for all 256 inputs, checked by an independent script that reproduces toBipolarFloat with int32 and float32 semantics.
  • Button packets fed through a real DeviceGroup and the Unix socket produce the documented wire output for LUMI, Lightpad M, and Loop serials.
  • Socket-level tests on both transports overflow a subscriber and assert the error, the close (status 1008 on WebSocket), and cleanup. A repro in which a stalled client overflows and then subscribes again leaves the device group running and a healthy subscriber still receiving events.
  • An independent verifier reviewed the branch twice against the SDK sources; its first pass caught a re-subscribe crash in an earlier draft of the overflow change, which the invariant above now covers.

Physical touch and button events were not captured for this change. The protocol mapping comes from the SDK and the decoder tests, not from live hardware.

🔍 Review focus

Start with _broadcast in src/blocksd/api/events.py and the two event writers in src/blocksd/api/server.py. One boundary is deliberate: a client that stops reading entirely keeps its socket until its pending writes drain, because the writer is parked in drain() and never sees the shutdown. The subscription is already gone at that point, so the cost is one idle connection, and a client that resumes reading receives the error and the close. The docs describe it that way rather than promising an immediate close.

📌 Follow-ups

The Hypercolor consumer is in progress on nova/device-input (spec 82): a read-only event connection per blocksd instance, device-attributed touch and button input, and routing into Hypercolor's interaction pipeline. How LUMI touch x maps onto the 24 key LEDs still needs a hardware capture.

🤖 Generated with Claude Code

hyperb1iss and others added 4 commits September 28, 2026 20:22
Touch velocity fields are two's complement bytes. The ROLI SDK converts
them with IntegerWithBitSize::toBipolarFloat, which sign-extends the raw
value and divides by 128. blocksd treated them as offset binary around
128, so a small positive velocity of 1 decoded as -1.0 and the most
negative value decoded as 0.0.

Golden vectors from the SDK formula now cover zero, both signs, and both
extremes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Button events only said press or release, so clients could not tell the
three LUMI Keys buttons apart. They now carry the protocol button index,
its ROLI SDK function name, and the device timestamp. The SDK reports a
button as an index into each model's button list, so the registry holds
those lists in SDK order and the device group resolves the name from the
block type.

Touch events also carry the device timestamp, which lets consumers space
out a burst of touches delivered together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a subscriber's bounded queue filled, the broadcaster removed the
subscription but left the connection open. The client kept a healthy
socket that would never deliver another event, with no way to notice.
A long-lived consumer such as a lighting bridge would go quiet until it
restarted.

The broadcaster now shuts down the overflowed queue and remembers it.
Each transport's event writer sees the shutdown, sends an error that
explains the overflow, and closes the connection (WebSocket status
1008), so clients reconnect, resubscribe, and rebuild state. Other
subscribers are unaffected.

Broadcasts run inside device callbacks, so they must never raise. A
subscribe on an overflowed connection is refused, and a shut-down queue
that somehow remains subscribed is dropped instead of raising
QueueShutDown into the device group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The message tables described touch and control button payloads as
starting with a 7-bit device index. The device index lives in the SysEx
header; the payload starts with a 5-bit timestamp offset, as the SDK's
HostPacketDecoder and blocksd's decoder both read it.

The device reference also still said button events omit the protocol
button ID, which is no longer true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant