Repository navigation
Release Notes v0.8.0
Released: 2026-11-14
blocksd gains custom LittleFoot program loading with a bundled, offline compiler, plus preview-level Windows support for ROLI Piano M and other Blocks hardware. Touch velocity, button identity, and battery reporting are corrected against the ROLI SDK, and distribution moves from pure-Python wheels to platform wheels that carry the native compiler.
🌟 Highlights
✨ Load custom LittleFoot programs onto devices
A new blocksd program command group compiles .littlefoot source and uploads it to a connected device. blocksd program load FILE resolves #include directives from the source directory or --include arguments, blocksd program status reports the active program and upload state, and blocksd program reset restores the device's saved program by cycling API mode. Implementation lives in src/blocksd/littlefoot/compiler.py, source.py, and upload.py, with CLI wiring in src/blocksd/cli/program.py.
🔧 Bundled offline compiler, no filesystem access
The compiler binary is built from vendored JUCE 7.0.12 juce_core and the ROLI SDK (both ISC licensed) by native/littlefoot/CMakeLists.txt and native/littlefoot/main.cpp. The build patches resolveIncludePath() to a stub, so the compiler can never read the daemon's filesystem: all includes are supplied as a virtual file map by Python. Source archives are checksummed in native/littlefoot/README.md for reproducible builds.
🪟 Windows preview support
blocksd now runs on Windows in foreground mode. src/blocksd/topology/detector.py recognizes ROLI Piano M and LUMI alongside BLOCK, and strips the per-direction enumeration index that WinMM appends to input and output port names so pairs resolve correctly. CLI commands reach the daemon over the local WebSocket endpoint (ws://127.0.0.1:9010/ws) instead of a Unix socket.
🐛 Correct touch velocity, battery, and button identity
Velocity fields now decode as two's complement signed 8-bit via _bipolar8() in src/blocksd/device/models.py, matching the SDK's IntegerWithBitSize::toBipolarFloat. Battery is scaled from the 5-bit topology field to a real percentage in src/blocksd/topology/device_group.py, so a full battery reports 100 instead of 31. Button events carry button_id and a human-readable button function name resolved from a per-block-type table in the new src/blocksd/device/registry.py.
👷 Platform wheels with the native compiler
hatch_build.py runs CMake during wheel builds and installs the compiler into src/blocksd/littlefoot/_native/, marking the wheel non-pure with a py3-none-{platform} tag. .github/workflows/wheels.yml builds cp313 wheels across Linux x86_64/ARM, macOS Intel/Apple Silicon, and Windows x86_64 with cibuildwheel 4.3.5.
🤖 LittleFoot Programs
- Six bundled library files ship under
src/blocksd/littlefoot/library/:AppleControlSurface.littlefoot,NoteGridBase.littlefoot,ClusterSync.littlefoot,ConfigIds.littlefoot,RPNHandler.littlefoot, andScale.littlefoot. They resolve by basename without being passed explicitly. - New API messages
program_load,program_status, andprogram_resetinsrc/blocksd/api/programs.pyandsrc/blocksd/api/commands.py.program_loadaccepts eithersourceplus anincludesmap or a base64codepayload, and runs compilation on a thread so MIDI keepalive is never delayed. - Program state is reported as
idle,compiling,uploading,unresponsive,uploaded, orready, withcode_sizeandheap_sizeso clients can distinguish transport acknowledgement from renderer readiness. - Memory budgets are validated before upload: program code, heap, and global stack use must fit the device's reported capacity.
- Source is capped at 256 KiB combined, and compilation times out after 15 seconds.
- While a custom program owns a device, blocksd's LED calls (
set_led_data,set_key_led_data) are rejected rather than silently fighting the program for the display. - Dashboard metadata defaults accept
int,float,bool, andmidiNoteonly; other declared types are rejected by the compiler. - User guide at
docs/guide/programs.md.
🌐 Dashboard
- New
ProgramPanel.tsxwith a.littlefootfile picker, an extra-includes section, a live state badge, transfer size details, and a restore button that appears only when a custom program is loaded. Markup usesaria-liveandaria-describedbyso status transitions are announced. useDeviceProgram.tspollsprogram_statusonce per second while connected and tracks a revision counter so responses from a previous device or connection are discarded.web/src/lib/program.tsvalidates file extension, rejects duplicate filenames, and enforces the same 256 KiB limit as the daemon before anything is sent.useDeviceSettings.tslistens for the newconfig_resetevent and clears cached values, schema, pending writes, and errors, so stale controls do not linger after a device starts a new configuration list.- Decorative taglines, eyebrow labels, and the instrument tip box were removed from
Studio.tsx, with the matching.instrument-tiprules dropped fromweb/src/theme/hardware.css. - Settings filtering falls back to the
allgroup when the previously active group disappears after a reset, and the empty state explains why no settings are shown.
🔌 API and Protocol
touchevents gain atimestampfield (device clock in milliseconds, comparable only within a single device's stream).buttonevents gainbutton_id,button(SDK function name such asmode,up,play_or_pause), andtimestamp. Function tables cover LUMI Keys, Lightpad, Seaboard, Loop, Live, Dev Controller, and Touch blocks.config_getaccepts an optionalrefresh: trueflag that triggers a factory-sync read before responding, backed by the newbuild_config_request_factory_sync()insrc/blocksd/protocol/builder.py. This requests current settings and ranges; it is not a factory reset.- New
config_resetevent broadcasts when a device begins a new active configuration list, telling clients their cached settings are invalid. - Stalled event subscribers are now disconnected instead of being silently unsubscribed. When a bounded queue overflows,
src/blocksd/api/events.pyshuts the queue down and records it in a weak set; the transport sends{"type": "error", "message": "event queue overflowed; reconnect and subscribe again"}and closes the connection (WebSocket clients get RFC 6455 close status 1008). Resubscribe attempts on a shut-down queue are rejected. - When
_handle_config()insrc/blocksd/protocol/decoder.pyencounters an unknown config command, it now returnsFalseanddecode_packet()breaks out of the decode loop, abandoning the remainder of the packet. Unknown config bodies have no length prefix, so the next message boundary is unknowable; previously the decoder returnedTrueand kept parsing at an unknown offset, silently desynchronizing and corrupting every following message in the packet. - Static file serving in
src/blocksd/api/http.pyrejects backslashes, drive letters, and UNC paths before resolution, and usesis_relative_to()for containment instead of string prefix matching. - Device names from topology are partitioned at the first null byte and validated as printable and non-empty before being stored.
- Firmware version strings get the same printable check and report
nullwhen the device sends non-text bytes, as a Lightpad Block M on firmware 1.1.0 does.
🪟 Windows Support
- Run the daemon with
blocksd run --foreground. There is no background service on Windows:blocksd installandblocksd uninstallraiseOSError("LaunchAgents are unavailable on Windows."). api_enableddefaults tofalseon Windows (Field(default_factory=lambda: sys.platform != "win32")insrc/blocksd/config/schema.py). The Unix socket API is unavailable;src/blocksd/api/server.pyraises if it is requested. Settingapi_enabled = trueorapi_socketon Windows fails at startup with a plain one-line error fromsrc/blocksd/cli/app.py.- CLI commands require
web_enabled = true.src/blocksd/cli/client.pybuilds the endpoint fromweb_hostandweb_port, normalizing0.0.0.0to127.0.0.1and::to::1, bracketing IPv6 literals, and ignoring proxy environment variables. - Configuration and presets live under
%APPDATA%\blocksd\via_windows_config_directory()insrc/blocksd/paths.py. - Preset reads and writes pin UTF-8 explicitly in
src/blocksd/config/presets.pyso Windows code pages cannot corrupt non-ASCII device or preset names. - systemd notifications in
src/blocksd/sdnotify.pyshort-circuit on Windows, andsrc/blocksd/daemon.pyfalls back tosignal.signal()withloop.call_soon_threadsafe()sinceadd_signal_handler()is unavailable, restoring original handlers on shutdown. - Windows holds exclusive MIDI port locks: close ROLI Dashboard and any DAW owning the device before starting blocksd. Physical Piano M renderer execution, replug, and DAW coexistence are still untested, which is why this is labeled preview.
⚙️ Build, Packaging, and CI
pyproject.tomladdswebsockets>=17.2, theOperating System :: Microsoft :: Windowsclassifier, the custom Hatchling build hook,/nativeandhatch_build.pyin the sdist, and uv cache keys coveringnative/**/*.scripts/check_distribution.pycompiles a one-line LittleFoot program and asserts the output exceeds 10 bytes, so a broken or missing compiler bundle fails the distribution check rather than shipping.- The Homebrew formula template
packaging/homebrew/blocksd.rb.inruns the same compiler smoke test duringbrew test. - AUR
PKGBUILDmoves fromarch=('any')toarch=('x86_64' 'aarch64')and addscmakepluspython-websockets>=17.2. - Release publishing adopts shared-workflows v2 with
artifact-mode: files, keeping individual asset download URLs instead of a single archive. - Dependency bumps across the JavaScript (14 updates) and Python (8 updates) groups, plus
astral-sh/setup-uv,pypa/gh-action-pypi-publish,actions/setup-node, andactions/upload-artifact. - New test suites:
tests/littlefoot/test_compiler.py,tests/topology/test_program_loading.py,tests/cli/test_program.py,tests/test_config_sync.py,tests/windows/test_client.py, andtests/windows/test_runtime.py.
⚠️ Breaking Changes
- Battery is now a percentage. The
batteryvalue in logs, CLI output, API payloads, and the dashboard is scaledround(level * 100 / 31). A full battery reports100where v0.7.0 reported31. Remove any client-side rescaling of this field. - Touch velocity sign and scale changed.
vx,vy, andvzwere decoded as offset binary around 128 and are now two's complement signed 8-bit over[-1.0, 127/128]. Values that previously came through inverted or offset will differ; recalibrate any thresholds derived from v0.7.0 output. - Overflowing event subscribers are disconnected. A client that falls behind now receives an error message and a closed connection instead of a socket that stays open and silently stops delivering events. Clients must reconnect and resubscribe.
- Wheels are platform-specific. blocksd no longer publishes a pure-Python wheel. Installs resolve a
py3-none-{platform}wheel carrying the native compiler, and source installs require CMake 3.24 or newer.
🧪 Hardware Verification
- LED rendering is confirmed on Lightpad Block M firmware 1.1.0 and LUMI Keys firmware 1.3.9 on Linux. The original Lightpad Block, Seaboard Block, and control blocks have not been checked on hardware.
- Program compilation and upload are covered by CI on every wheel platform. On-device playback of third-party apps such as Seqoli has not been confirmed on hardware.
- Windows has no hardware results yet, and macOS LED output, sleep and wake, and DAW coexistence remain unverified.
📝 Upgrade Notes
- Update any dashboard, logging, or automation that assumed the 0 to 31 battery scale.
- Audit touch-velocity consumers for the corrected sign and range.
- Add reconnect-and-resubscribe handling for the
event queue overflowederror, and handle the newconfig_resetevent by discarding cached settings. - Building from source now needs CMake 3.24+. Package installs from PyPI, Homebrew, or AUR pick up the prebuilt compiler automatically; AUR users should note the move to arch-specific packages.
- On Windows, start the daemon with
blocksd run --foreground, keepweb_enabled = true, leaveapi_enabledat its default offalse, and remove anyapi_socketsetting copied from a Linux or macOS config. Close ROLI Dashboard first so the MIDI ports are free. - Custom LittleFoot programs do not persist across daemon restarts, and while one is loaded blocksd's LED commands are rejected. Use
blocksd program resetto hand control back to the device's saved program.