blocksd 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. blocksd-0.2.0/.github/FUNDING.yml +4 -0
  2. blocksd-0.2.0/.github/workflows/ci.yml +27 -0
  3. blocksd-0.2.0/.github/workflows/publish.yml +63 -0
  4. blocksd-0.2.0/.github/workflows/release.yml +128 -0
  5. blocksd-0.2.0/.gitignore +21 -0
  6. blocksd-0.2.0/.pre-commit-config.yaml +21 -0
  7. blocksd-0.2.0/.python-version +1 -0
  8. blocksd-0.2.0/CLAUDE.md +305 -0
  9. blocksd-0.2.0/CONTRIBUTING.md +76 -0
  10. blocksd-0.2.0/LICENSE +15 -0
  11. blocksd-0.2.0/PKG-INFO +362 -0
  12. blocksd-0.2.0/README.md +335 -0
  13. blocksd-0.2.0/VISION.md +198 -0
  14. blocksd-0.2.0/docs/API.md +357 -0
  15. blocksd-0.2.0/docs/FIRMWARE_NOTES.md +141 -0
  16. blocksd-0.2.0/firmware/default/loopblock.littlefoot +112 -0
  17. blocksd-0.2.0/firmware/default/padblock.littlefoot +1313 -0
  18. blocksd-0.2.0/firmware/default/seaboardblock.littlefoot +195 -0
  19. blocksd-0.2.0/install.sh +119 -0
  20. blocksd-0.2.0/justfile +81 -0
  21. blocksd-0.2.0/packaging/aur/blocksd/PKGBUILD +56 -0
  22. blocksd-0.2.0/packaging/aur/blocksd-git/PKGBUILD +61 -0
  23. blocksd-0.2.0/pyproject.toml +172 -0
  24. blocksd-0.2.0/src/blocksd/__init__.py +3 -0
  25. blocksd-0.2.0/src/blocksd/__main__.py +5 -0
  26. blocksd-0.2.0/src/blocksd/api/__init__.py +5 -0
  27. blocksd-0.2.0/src/blocksd/api/events.py +183 -0
  28. blocksd-0.2.0/src/blocksd/api/http.py +146 -0
  29. blocksd-0.2.0/src/blocksd/api/protocol.py +62 -0
  30. blocksd-0.2.0/src/blocksd/api/server.py +691 -0
  31. blocksd-0.2.0/src/blocksd/api/websocket.py +73 -0
  32. blocksd-0.2.0/src/blocksd/cli/__init__.py +1 -0
  33. blocksd-0.2.0/src/blocksd/cli/app.py +186 -0
  34. blocksd-0.2.0/src/blocksd/cli/config.py +140 -0
  35. blocksd-0.2.0/src/blocksd/cli/install.py +153 -0
  36. blocksd-0.2.0/src/blocksd/cli/led.py +150 -0
  37. blocksd-0.2.0/src/blocksd/config/__init__.py +1 -0
  38. blocksd-0.2.0/src/blocksd/config/loader.py +38 -0
  39. blocksd-0.2.0/src/blocksd/config/schema.py +32 -0
  40. blocksd-0.2.0/src/blocksd/daemon.py +158 -0
  41. blocksd-0.2.0/src/blocksd/device/__init__.py +1 -0
  42. blocksd-0.2.0/src/blocksd/device/config_ids.py +31 -0
  43. blocksd-0.2.0/src/blocksd/device/connection.py +119 -0
  44. blocksd-0.2.0/src/blocksd/device/models.py +122 -0
  45. blocksd-0.2.0/src/blocksd/device/registry.py +67 -0
  46. blocksd-0.2.0/src/blocksd/led/__init__.py +1 -0
  47. blocksd-0.2.0/src/blocksd/led/bitmap.py +132 -0
  48. blocksd-0.2.0/src/blocksd/led/patterns.py +90 -0
  49. blocksd-0.2.0/src/blocksd/littlefoot/__init__.py +1 -0
  50. blocksd-0.2.0/src/blocksd/littlefoot/assembler.py +300 -0
  51. blocksd-0.2.0/src/blocksd/littlefoot/opcodes.py +75 -0
  52. blocksd-0.2.0/src/blocksd/littlefoot/programs.py +153 -0
  53. blocksd-0.2.0/src/blocksd/logging.py +26 -0
  54. blocksd-0.2.0/src/blocksd/protocol/__init__.py +1 -0
  55. blocksd-0.2.0/src/blocksd/protocol/builder.py +124 -0
  56. blocksd-0.2.0/src/blocksd/protocol/checksum.py +12 -0
  57. blocksd-0.2.0/src/blocksd/protocol/constants.py +157 -0
  58. blocksd-0.2.0/src/blocksd/protocol/data_change.py +405 -0
  59. blocksd-0.2.0/src/blocksd/protocol/decoder.py +426 -0
  60. blocksd-0.2.0/src/blocksd/protocol/packing.py +95 -0
  61. blocksd-0.2.0/src/blocksd/protocol/remote_heap.py +251 -0
  62. blocksd-0.2.0/src/blocksd/protocol/serial.py +50 -0
  63. blocksd-0.2.0/src/blocksd/py.typed +0 -0
  64. blocksd-0.2.0/src/blocksd/sdnotify.py +95 -0
  65. blocksd-0.2.0/src/blocksd/topology/__init__.py +1 -0
  66. blocksd-0.2.0/src/blocksd/topology/detector.py +97 -0
  67. blocksd-0.2.0/src/blocksd/topology/device_group.py +695 -0
  68. blocksd-0.2.0/src/blocksd/topology/manager.py +175 -0
  69. blocksd-0.2.0/src/blocksd/web/__init__.py +14 -0
  70. blocksd-0.2.0/systemd/99-roli-blocks.rules +19 -0
  71. blocksd-0.2.0/systemd/blocksd.service +22 -0
  72. blocksd-0.2.0/tests/__init__.py +0 -0
  73. blocksd-0.2.0/tests/cli/__init__.py +0 -0
  74. blocksd-0.2.0/tests/cli/test_config.py +42 -0
  75. blocksd-0.2.0/tests/cli/test_led.py +129 -0
  76. blocksd-0.2.0/tests/device/__init__.py +0 -0
  77. blocksd-0.2.0/tests/device/test_events.py +75 -0
  78. blocksd-0.2.0/tests/led/__init__.py +0 -0
  79. blocksd-0.2.0/tests/led/test_bitmap.py +229 -0
  80. blocksd-0.2.0/tests/led/test_patterns.py +120 -0
  81. blocksd-0.2.0/tests/littlefoot/__init__.py +0 -0
  82. blocksd-0.2.0/tests/littlefoot/test_assembler.py +234 -0
  83. blocksd-0.2.0/tests/littlefoot/test_programs.py +57 -0
  84. blocksd-0.2.0/tests/protocol/__init__.py +0 -0
  85. blocksd-0.2.0/tests/protocol/test_builder.py +43 -0
  86. blocksd-0.2.0/tests/protocol/test_builder_config.py +71 -0
  87. blocksd-0.2.0/tests/protocol/test_checksum.py +23 -0
  88. blocksd-0.2.0/tests/protocol/test_data_change.py +432 -0
  89. blocksd-0.2.0/tests/protocol/test_decoder.py +479 -0
  90. blocksd-0.2.0/tests/protocol/test_packing.py +86 -0
  91. blocksd-0.2.0/tests/protocol/test_remote_heap.py +503 -0
  92. blocksd-0.2.0/tests/protocol/test_serial.py +37 -0
  93. blocksd-0.2.0/tests/test_api_events.py +215 -0
  94. blocksd-0.2.0/tests/test_api_protocol.py +208 -0
  95. blocksd-0.2.0/tests/test_http.py +121 -0
  96. blocksd-0.2.0/tests/test_sdnotify.py +47 -0
  97. blocksd-0.2.0/tests/test_web_server.py +219 -0
  98. blocksd-0.2.0/tests/test_websocket.py +106 -0
  99. blocksd-0.2.0/tests/topology/__init__.py +0 -0
  100. blocksd-0.2.0/tests/topology/test_detector.py +47 -0
  101. blocksd-0.2.0/tests/topology/test_device_group.py +318 -0
  102. blocksd-0.2.0/uv.lock +587 -0
  103. blocksd-0.2.0/web/biome.json +47 -0
  104. blocksd-0.2.0/web/bun.lock +365 -0
  105. blocksd-0.2.0/web/index.html +13 -0
  106. blocksd-0.2.0/web/package.json +27 -0
  107. blocksd-0.2.0/web/src/App.tsx +20 -0
  108. blocksd-0.2.0/web/src/components/dashboard/DashboardView.tsx +48 -0
  109. blocksd-0.2.0/web/src/components/dashboard/DeviceCard.tsx +67 -0
  110. blocksd-0.2.0/web/src/components/dashboard/TopologyMap.tsx +102 -0
  111. blocksd-0.2.0/web/src/components/device/ConfigPanel.tsx +46 -0
  112. blocksd-0.2.0/web/src/components/device/ConfigSlider.tsx +42 -0
  113. blocksd-0.2.0/web/src/components/device/DeviceDetailView.tsx +73 -0
  114. blocksd-0.2.0/web/src/components/layout/AppShell.tsx +28 -0
  115. blocksd-0.2.0/web/src/components/layout/DeviceList.tsx +56 -0
  116. blocksd-0.2.0/web/src/components/layout/StatusBar.tsx +47 -0
  117. blocksd-0.2.0/web/src/hooks/useBlocksd.tsx +104 -0
  118. blocksd-0.2.0/web/src/hooks/useConfig.ts +73 -0
  119. blocksd-0.2.0/web/src/hooks/useDevices.ts +42 -0
  120. blocksd-0.2.0/web/src/hooks/useTopology.ts +30 -0
  121. blocksd-0.2.0/web/src/lib/constants.ts +128 -0
  122. blocksd-0.2.0/web/src/lib/frame.ts +18 -0
  123. blocksd-0.2.0/web/src/lib/protocol.ts +101 -0
  124. blocksd-0.2.0/web/src/lib/types.ts +41 -0
  125. blocksd-0.2.0/web/src/main.tsx +10 -0
  126. blocksd-0.2.0/web/src/pages/Dashboard.tsx +10 -0
  127. blocksd-0.2.0/web/src/pages/DeviceDetail.tsx +27 -0
  128. blocksd-0.2.0/web/src/theme/global.css +47 -0
  129. blocksd-0.2.0/web/src/theme/tokens.ts +18 -0
  130. blocksd-0.2.0/web/src/vite-env.d.ts +1 -0
  131. blocksd-0.2.0/web/tsconfig.json +20 -0
  132. blocksd-0.2.0/web/vite.config.ts +20 -0
@@ -0,0 +1,4 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: hyperb1iss
4
+ ko_fi: hyperb1iss
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ ci:
11
+ uses: hyperb1iss/shared-workflows/.github/workflows/python-ci.yml@v1
12
+ with:
13
+ python-version: "3.13"
14
+ system-deps: libasound2-dev
15
+
16
+ typecheck:
17
+ name: Type Check
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v6
21
+ - run: sudo apt-get update && sudo apt-get install -y libasound2-dev
22
+ - uses: astral-sh/setup-uv@v7
23
+ - uses: actions/setup-python@v6
24
+ with:
25
+ python-version: "3.13"
26
+ - run: uv sync
27
+ - run: uv run ty check src
@@ -0,0 +1,63 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: write
10
+ id-token: write
11
+ actions: read
12
+
13
+ jobs:
14
+ pypi:
15
+ name: Publish to PyPI
16
+ runs-on: ubuntu-latest
17
+ environment:
18
+ name: pypi
19
+ url: https://pypi.org/p/blocksd
20
+ steps:
21
+ - uses: actions/checkout@v6
22
+ with:
23
+ ref: ${{ github.ref }}
24
+
25
+ - uses: astral-sh/setup-uv@v7
26
+
27
+ - uses: actions/setup-python@v6
28
+ with:
29
+ python-version: "3.13"
30
+
31
+ - name: Build package
32
+ run: uv build
33
+
34
+ - name: Publish to PyPI
35
+ uses: pypa/gh-action-pypi-publish@release/v1
36
+
37
+ github-release:
38
+ uses: hyperb1iss/shared-workflows/.github/workflows/github-release.yml@v1
39
+ needs: [pypi]
40
+ permissions:
41
+ contents: write
42
+ actions: read
43
+ secrets: inherit
44
+
45
+ aur:
46
+ name: Update AUR
47
+ runs-on: ubuntu-latest
48
+ needs: [pypi]
49
+ steps:
50
+ - uses: actions/checkout@v6
51
+
52
+ - name: Extract version from tag
53
+ id: version
54
+ run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
55
+
56
+ - name: Generate .SRCINFO and publish to AUR
57
+ uses: KSXGitHub/github-actions-deploy-aur@v3.0.1
58
+ with:
59
+ pkgname: blocksd
60
+ pkgbuild: packaging/aur/blocksd/PKGBUILD
61
+ updpkgsums: true
62
+ commit_message: "Update to ${{ steps.version.outputs.version }}"
63
+ ssh_private_key: ${{ secrets.AUR_SSH_KEY }}
@@ -0,0 +1,128 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ version:
7
+ description: "Version to release (leave empty for auto-bump)"
8
+ required: false
9
+ default: ""
10
+ bump:
11
+ description: "Version bump type (if version not specified)"
12
+ required: false
13
+ type: choice
14
+ default: "patch"
15
+ options:
16
+ - patch
17
+ - minor
18
+ - major
19
+ dry_run:
20
+ description: "Dry run (skip tag, publish, and release)"
21
+ required: false
22
+ type: boolean
23
+ default: false
24
+
25
+ permissions:
26
+ actions: write
27
+ contents: write
28
+ id-token: write
29
+
30
+ concurrency:
31
+ group: release
32
+ cancel-in-progress: false
33
+
34
+ jobs:
35
+ release:
36
+ name: Release
37
+ runs-on: ubuntu-latest
38
+ timeout-minutes: 15
39
+
40
+ steps:
41
+ - uses: actions/checkout@v6
42
+ with:
43
+ fetch-depth: 0
44
+ token: ${{ secrets.GITHUB_TOKEN }}
45
+
46
+ - name: Configure git
47
+ run: |
48
+ git config user.name "github-actions[bot]"
49
+ git config user.email "github-actions[bot]@users.noreply.github.com"
50
+
51
+ - name: Determine version
52
+ id: version
53
+ run: |
54
+ CURRENT=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
55
+ echo "Current version: $CURRENT"
56
+
57
+ if [[ -n "${{ inputs.version }}" ]]; then
58
+ VERSION="${{ inputs.version }}"
59
+ else
60
+ IFS='.' read -r MAJOR MINOR PATCH <<< "${CURRENT%%-*}"
61
+ case "${{ inputs.bump }}" in
62
+ major) VERSION="$((MAJOR + 1)).0.0" ;;
63
+ minor) VERSION="${MAJOR}.$((MINOR + 1)).0" ;;
64
+ patch) VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))" ;;
65
+ esac
66
+ fi
67
+
68
+ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
69
+ echo "::error::Invalid version format: $VERSION"
70
+ exit 1
71
+ fi
72
+
73
+ if git tag -l "v$VERSION" | grep -q "v$VERSION"; then
74
+ echo "::error::Tag v$VERSION already exists"
75
+ exit 1
76
+ fi
77
+
78
+ echo "current=$CURRENT" >> $GITHUB_OUTPUT
79
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
80
+ echo "Releasing: $CURRENT → $VERSION"
81
+
82
+ - name: Install uv
83
+ uses: astral-sh/setup-uv@v7
84
+
85
+ - uses: actions/setup-python@v6
86
+ with:
87
+ python-version: "3.13"
88
+
89
+ - name: Update version in pyproject.toml
90
+ run: |
91
+ VERSION="${{ steps.version.outputs.version }}"
92
+ sed -i "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
93
+ echo "Updated pyproject.toml to $VERSION"
94
+
95
+ - name: Build package
96
+ run: uv build
97
+
98
+ - name: Commit version bump
99
+ if: ${{ !inputs.dry_run }}
100
+ run: |
101
+ git add pyproject.toml
102
+ git commit -m "release: v${{ steps.version.outputs.version }}"
103
+
104
+ - name: Create and push tag
105
+ if: ${{ !inputs.dry_run }}
106
+ run: |
107
+ git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}"
108
+ git push origin HEAD:${{ github.ref_name }}
109
+ git push origin "v${{ steps.version.outputs.version }}"
110
+
111
+ - name: Trigger publish workflow
112
+ if: ${{ !inputs.dry_run }}
113
+ run: |
114
+ gh workflow run publish.yml --ref "v${{ steps.version.outputs.version }}"
115
+ env:
116
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
117
+
118
+ - name: Summary
119
+ run: |
120
+ echo "## Release ${{ inputs.dry_run && '[DRY RUN] ' || '' }}v${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
121
+ echo "" >> $GITHUB_STEP_SUMMARY
122
+ if [[ "${{ inputs.dry_run }}" == "true" ]]; then
123
+ echo "Build passed. Ready to release." >> $GITHUB_STEP_SUMMARY
124
+ else
125
+ echo "Released: ${{ steps.version.outputs.current }} → ${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
126
+ echo "" >> $GITHUB_STEP_SUMMARY
127
+ echo "[blocksd on PyPI](https://pypi.org/project/blocksd/${{ steps.version.outputs.version }}/)" >> $GITHUB_STEP_SUMMARY
128
+ fi
@@ -0,0 +1,21 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ .env
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .ty/
14
+ *.so
15
+ .coverage
16
+ htmlcov/
17
+ .hypothesis/
18
+
19
+ # Web UI
20
+ web/node_modules/
21
+ src/blocksd/web/static/
@@ -0,0 +1,21 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-yaml
8
+ - id: check-toml
9
+ - id: check-added-large-files
10
+ args: ["--maxkb=1000"]
11
+ - id: check-merge-conflict
12
+ - id: detect-private-key
13
+ - id: mixed-line-ending
14
+ args: ["--fix=lf"]
15
+
16
+ - repo: https://github.com/astral-sh/ruff-pre-commit
17
+ rev: v0.14.0
18
+ hooks:
19
+ - id: ruff
20
+ args: [--fix, --exit-non-zero-on-fix]
21
+ - id: ruff-format
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,305 @@
1
+ # blocksd — ROLI Blocks Linux Daemon
2
+
3
+ ## Project Overview
4
+
5
+ Linux daemon that implements the ROLI Blocks protocol to keep devices alive, control LEDs, and manage topology. ROLI devices require an active host-side handshake over MIDI SysEx to enter "API mode" — without it, they show a "searching" animation and eventually power off.
6
+
7
+ **Stack:** Python 3.13, asyncio, python-rtmidi, Typer, Rich, Pydantic
8
+ **Package Manager:** uv
9
+ **Linter:** ruff
10
+ **Type Checker:** ty
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ daemon.py asyncio main loop, signal handling
16
+ └─ TopologyManager polls MIDI ports every 1.5s
17
+ └─ DeviceGroup per-USB lifecycle (serial → topology → API mode → ping)
18
+ └─ MidiConnection python-rtmidi wrapper (send/receive SysEx)
19
+ └─ protocol/* pure protocol logic (packing, builder, decoder)
20
+ ```
21
+
22
+ ## ROLI Blocks Protocol Reference
23
+
24
+ ### SysEx Framing
25
+
26
+ All BLOCKS protocol messages are MIDI SysEx:
27
+
28
+ ```
29
+ F0 00 21 10 77 [deviceIndex] [7-bit packed payload] [checksum] F7
30
+ │ │ │ │ │
31
+ │ │ │ └ lower 6 bits = topology index └ payload checksum & 0x7F
32
+ │ │ │ bit 6: 0=host→device, 1=device→host
33
+ │ │ └ BLOCKS product byte
34
+ │ └ ROLI manufacturer ID
35
+ └ SysEx start
36
+ ```
37
+
38
+ ### Checksum Calculation
39
+
40
+ ```python
41
+ def calculate_checksum(data: bytes) -> int:
42
+ checksum = len(data) & 0xFF
43
+ for byte in data:
44
+ checksum = (checksum + (checksum * 2 + byte)) & 0xFF
45
+ return checksum & 0x7F
46
+ ```
47
+
48
+ ### 7-Bit Packing
49
+
50
+ Payload bits are packed LSB-first across 7-bit bytes (bit 7 always 0 for MIDI safety). When a value spans byte boundaries, low bits fill the current byte remainder, high bits continue in the next byte's low bits.
51
+
52
+ ### Serial Number Request (separate SysEx format)
53
+
54
+ - **Request:** `F0 00 21 10 78 3F F7`
55
+ - **Response header:** `F0 00 21 10 78`
56
+ - Response contains MAC prefix `48:B6:20:` followed by 16-char serial
57
+ - Serial prefixes identify device type:
58
+ - `LPB`/`LPM` = Lightpad Block
59
+ - `SBB` = Seaboard Block
60
+ - `LKB` = LUMI Keys Block
61
+ - `LIC` = Live Block
62
+ - `LOC` = Loop Block
63
+ - `DCB` = Developer Control Block
64
+ - `TCB` = Touch Block
65
+
66
+ ### USB Device IDs (Vendor `0x2AF4`)
67
+
68
+ | PID | Device |
69
+ | -------- | ------------------- |
70
+ | `0x0100` | Seaboard (original) |
71
+ | `0x0200` | Seaboard RISE 25 |
72
+ | `0x0210` | Seaboard RISE 49 |
73
+ | `0x0700` | Seaboard Block |
74
+ | `0x0900` | Lightpad Block |
75
+ | `0x0E00` | LUMI Keys / Piano M |
76
+ | `0x0F00` | ROLI Piano (49-key) |
77
+ | `0x1000` | ROLI Airwave Pedal |
78
+
79
+ ### Protocol Constants
80
+
81
+ ```
82
+ ROLI_SYSEX_HEADER = F0 00 21 10 77
83
+ SERIAL_DUMP_REQUEST = F0 00 21 10 78 3F F7
84
+ SERIAL_RESPONSE_HDR = F0 00 21 10 78
85
+ RESET_MASTER = F0 00 21 10 49 F7
86
+ PROTOCOL_VERSION = 1
87
+ TOPOLOGY_INDEX_BROADCAST = 63
88
+ API_MODE_PING_TIMEOUT_MS = 5000
89
+ ```
90
+
91
+ ### Message Types — Device → Host
92
+
93
+ | ID | Name | Payload |
94
+ | ------ | ---------------------- | ----------------------------------------------- |
95
+ | `0x01` | deviceTopology | 7b deviceCount, 8b connectionCount, then blocks |
96
+ | `0x02` | packetACK | 10b packetCounter |
97
+ | `0x03` | firmwareUpdateACK | 7b code, 32b detail |
98
+ | `0x04` | deviceTopologyExtend | continuation of topology |
99
+ | `0x05` | deviceTopologyEnd | signals end of multi-packet topology |
100
+ | `0x06` | deviceVersion | index + version string |
101
+ | `0x07` | deviceName | index + name string |
102
+ | `0x10` | touchStart | 7b devIdx, 5b touchIdx, 12b x, 12b y, 8b z |
103
+ | `0x11` | touchMove | same as touchStart |
104
+ | `0x12` | touchEnd | same as touchStart |
105
+ | `0x13` | touchStartWithVelocity | + 8b vx, 8b vy, 8b vz |
106
+ | `0x14` | touchMoveWithVelocity | + velocity |
107
+ | `0x15` | touchEndWithVelocity | + velocity |
108
+ | `0x18` | configMessage | config command + data |
109
+ | `0x20` | controlButtonDown | 7b devIdx, 12b buttonID |
110
+ | `0x21` | controlButtonUp | 7b devIdx, 12b buttonID |
111
+ | `0x28` | programEventMessage | 3 × 32b integers |
112
+ | `0x30` | logMessage | string data |
113
+
114
+ ### Message Types — Host → Device
115
+
116
+ | ID | Name | Payload |
117
+ | ------ | -------------------- | ---------------------------------- |
118
+ | `0x01` | deviceCommandMessage | 9b command (see Device Commands) |
119
+ | `0x02` | sharedDataChange | 16b packetIndex + data change cmds |
120
+ | `0x03` | programEventMessage | 3 × 32b integers |
121
+ | `0x04` | firmwareUpdatePacket | 7b size + 7-bit encoded data |
122
+ | `0x10` | configMessage | 4b configCmd + item + value |
123
+ | `0x11` | factoryReset | (no payload) |
124
+ | `0x12` | blockReset | (no payload) |
125
+ | `0x20` | setName | 7b length + 7-bit chars |
126
+
127
+ ### Device Commands (inside deviceCommandMessage)
128
+
129
+ | ID | Name |
130
+ | ------ | ---------------------- |
131
+ | `0x00` | beginAPIMode |
132
+ | `0x01` | requestTopologyMessage |
133
+ | `0x02` | endAPIMode |
134
+ | `0x03` | ping |
135
+ | `0x04` | debugMode |
136
+ | `0x05` | saveProgramAsDefault |
137
+
138
+ ### Config Commands
139
+
140
+ | ID | Name |
141
+ | ------ | ------------------ |
142
+ | `0x00` | setConfig |
143
+ | `0x01` | requestConfig |
144
+ | `0x02` | requestFactorySync |
145
+ | `0x03` | requestUserSync |
146
+ | `0x04` | updateConfig |
147
+ | `0x05` | updateUserConfig |
148
+ | `0x06` | setConfigState |
149
+ | `0x07` | factorySyncEnd |
150
+ | `0x08` | clusterConfigSync |
151
+ | `0x09` | factorySyncReset |
152
+
153
+ ### Data Change Commands (for sharedDataChange / program upload)
154
+
155
+ | ID | Name | Extra bits |
156
+ | --- | ------------------------ | --------------------------- |
157
+ | `0` | endOfPacket | — |
158
+ | `1` | endOfChanges | — |
159
+ | `2` | skipBytesFew | 4b count |
160
+ | `3` | skipBytesMany | 8b count |
161
+ | `4` | setSequenceOfBytes | (8b value + 1b continues)×N |
162
+ | `5` | setFewBytesWithValue | 4b count + 8b value |
163
+ | `6` | setFewBytesWithLastValue | 4b count |
164
+ | `7` | setManyBytesWithValue | 8b count + 8b value |
165
+
166
+ ### Topology Packet Format
167
+
168
+ Device info block (per device in topology):
169
+
170
+ - 16 × 7-bit chars: serial number
171
+ - 5 bits: battery level
172
+ - 1 bit: battery charging
173
+
174
+ Connection info block:
175
+
176
+ - 7 bits: device1 topology index
177
+ - 5 bits: device1 port (clockwise from top-left)
178
+ - 7 bits: device2 topology index
179
+ - 5 bits: device2 port
180
+
181
+ Max 6 devices and 24 connections per topology packet. Use extend/end for larger topologies.
182
+
183
+ ### Connection Lifecycle (from roli_ConnectedDeviceGroup.cpp)
184
+
185
+ ```
186
+ 1. Scan MIDI ports for names containing "BLOCK" or "Block"
187
+ 2. Open matched in/out pair
188
+ 3. Send serial dump: F0 00 21 10 78 3F F7 (retry every 300ms)
189
+ 4. Parse serial from response (find "48:B6:20:", skip MAC, read 16 chars)
190
+ 5. Send requestTopologyMessage (cmd 0x01) to device index 0
191
+ 6. Parse topology response → build device list
192
+ 7. For each device not yet in API mode:
193
+ a. Send endAPIMode (cmd 0x02) — reset stale state
194
+ b. Send beginAPIMode (cmd 0x00) — enter rich protocol
195
+ 8. Ping loop:
196
+ - Master block: ping every ~400ms
197
+ - DNA-connected blocks: ping every ~1666ms
198
+ - Device timeout: 5000ms without ping → falls back to MPE mode
199
+ 9. On ACK received: update ping timer for that device
200
+ 10. On topology change: re-request topology
201
+ 11. On disconnect: destroy group, emit device-removed
202
+ ```
203
+
204
+ ### Bit Sizes Reference
205
+
206
+ | Field | Bits |
207
+ | ------------------ | ---- |
208
+ | MessageType | 7 |
209
+ | ProtocolVersion | 8 |
210
+ | PacketTimestamp | 32 |
211
+ | TimestampOffset | 5 |
212
+ | TopologyIndex | 7 |
213
+ | DeviceCount | 7 |
214
+ | ConnectionCount | 8 |
215
+ | BatteryLevel | 5 |
216
+ | BatteryCharging | 1 |
217
+ | ConnectorPort | 5 |
218
+ | TouchIndex | 5 |
219
+ | TouchPosition.x | 12 |
220
+ | TouchPosition.y | 12 |
221
+ | TouchPosition.z | 8 |
222
+ | TouchVelocity.v\* | 8 |
223
+ | DeviceCommand | 9 |
224
+ | ConfigCommand | 4 |
225
+ | ConfigItemIndex | 8 |
226
+ | ConfigItemValue | 32 |
227
+ | ControlButtonID | 12 |
228
+ | PacketCounter | 10 |
229
+ | PacketIndex | 16 |
230
+ | DataChangeCommand | 3 |
231
+ | ByteCountFew | 4 |
232
+ | ByteCountMany | 8 |
233
+ | ByteValue | 8 |
234
+ | ByteSequenceCont | 1 |
235
+ | FirmwareUpdateACK | 7 |
236
+ | FirmwareUpdateDtl | 32 |
237
+ | FirmwareUpdateSize | 7 |
238
+
239
+ ### Device Memory Sizes
240
+
241
+ | Device Type | Program + Heap | Stack |
242
+ | ------------- | -------------- | ----- |
243
+ | Pad Block | 7200 bytes | 800 |
244
+ | Control Block | 3000 bytes | 800 |
245
+
246
+ ## Key Reference Files
247
+
248
+ Protocol source (cloned to `~/Downloads/roli-extracted/roli_blocks_basics/`):
249
+
250
+ - `protocol/roli_BitPackingUtilities.h` — 7-bit packing algorithm (CRITICAL to port correctly)
251
+ - `protocol/roli_BlocksProtocolDefinitions.h` — all enums, constants, bit sizes
252
+ - `protocol/roli_HostPacketBuilder.h` — host→device packet construction
253
+ - `protocol/roli_HostPacketDecoder.h` — device→host packet parsing
254
+ - `protocol/roli_BlockModels.h` — device type definitions and capabilities
255
+ - `topology/internal/roli_ConnectedDeviceGroup.cpp` — full device lifecycle state machine
256
+ - `topology/internal/roli_BlockSerialReader.cpp` — serial number request/parse
257
+ - `topology/internal/roli_MIDIDeviceDetector.cpp` — MIDI port scanning/matching
258
+ - `topology/internal/roli_Detector.cpp` — top-level detection loop
259
+ - `topology/internal/roli_MidiDeviceConnection.cpp` — MIDI I/O wrapper
260
+
261
+ Extracted ROLI Connect installer (`~/Downloads/roli-extracted/`):
262
+
263
+ - `rpkg-driver/` — Windows driver package (reference only)
264
+ - `midi-driver/DriverINF` — USB VID/PID mapping
265
+ - `app-asar-unpacked/` — Electron app source (minified JS)
266
+ - `app/resources/app/resources/extra/firmware/default/*.littlefoot` — device firmware
267
+
268
+ ## Implementation Phases
269
+
270
+ ### Phase 1: Protocol Core (no hardware needed)
271
+
272
+ `protocol/constants.py` → `protocol/checksum.py` → `protocol/packing.py` → `protocol/builder.py` → `protocol/decoder.py` → `protocol/serial.py` + full test suite
273
+
274
+ ### Phase 2: Device Models
275
+
276
+ `device/models.py` → `device/registry.py` → `device/config_ids.py`
277
+
278
+ ### Phase 3: Connection Layer
279
+
280
+ `device/connection.py` → `topology/detector.py` → `topology/device_group.py` → `topology/manager.py`
281
+
282
+ ### Phase 4: Daemon + Config
283
+
284
+ `daemon.py` → `config/schema.py` + `config/loader.py` → `logging.py`
285
+
286
+ ### Phase 5: CLI
287
+
288
+ `cli/app.py` → `cli/status.py` → `cli/config_cmd.py` → `cli/led_cmd.py`
289
+
290
+ ### Phase 6: LED Control + Program Upload
291
+
292
+ `led/bitmap.py` → `led/patterns.py` → `protocol/data_change.py`
293
+
294
+ ### Phase 7: Polish
295
+
296
+ systemd service → udev rules → `cli/service.py` → sd_notify integration
297
+
298
+ ## Critical Implementation Notes
299
+
300
+ - **7-bit packing is the #1 risk** — must be tested exhaustively with round-trips and golden vectors from the C++ implementation
301
+ - **Ping timing is critical** — 5000ms timeout means we need reliable <400ms ping intervals. Use dedicated asyncio tasks, not shared timers
302
+ - **rtmidi callbacks arrive on a separate thread** — marshal to asyncio via `loop.call_soon_threadsafe()` or `asyncio.Queue`
303
+ - **ALSA handles multi-client MIDI natively** — we don't block DAW access
304
+ - **Device detection**: match MIDI port names containing "BLOCK" or "Block", validate with USB VID `0x2AF4` via sysfs as fallback
305
+ - **Incoming packet processing**: strip SysEx header (5 bytes), first byte after header is device index, remaining bytes are payload + checksum (last byte). Validate checksum on payload bytes, then create `Packed7BitReader` from payload (excluding checksum)
@@ -0,0 +1,76 @@
1
+ # Contributing to blocksd
2
+
3
+ Contributions welcome! Bug fixes, device support, new features, we'd love your help.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/hyperb1iss/blocksd.git
9
+ cd blocksd
10
+ uv sync # installs all dependencies including dev tools
11
+ ```
12
+
13
+ ## Development Workflow
14
+
15
+ The project includes a [justfile](https://just.systems/) for common tasks:
16
+
17
+ ```bash
18
+ just check # lint + format check + typecheck + tests (the full gate)
19
+ just test # run all tests
20
+ just test-v # verbose test output
21
+ just test-mod protocol # tests for a specific module
22
+ just lint # ruff lint
23
+ just fmt # auto-format
24
+ just typecheck # ty check
25
+ just fix # auto-fix lint + format
26
+ ```
27
+
28
+ Or run tools directly:
29
+
30
+ ```bash
31
+ uv run pytest # all tests
32
+ uv run pytest -v # verbose
33
+ uv run pytest tests/protocol/ # specific module
34
+ uv run ruff check . # lint
35
+ uv run ruff format . # auto-format
36
+ uv run ty check # type check
37
+ uv run blocksd run -v # run the daemon (requires hardware)
38
+ ```
39
+
40
+ ## Project Structure
41
+
42
+ - **`src/blocksd/protocol/`**: pure protocol logic (packing, builder, decoder), no I/O, fully testable
43
+ - **`src/blocksd/device/`**: device models, config IDs, connection layer
44
+ - **`src/blocksd/topology/`**: device discovery, lifecycle management, topology tracking
45
+ - **`src/blocksd/led/`**: LED bitmap grid and pattern generators
46
+ - **`src/blocksd/littlefoot/`**: LittleFoot VM opcodes, assembler, programs
47
+ - **`src/blocksd/api/`**: Unix socket + WebSocket server, event broadcasting
48
+ - **`src/blocksd/web/`**: web dashboard assets
49
+ - **`src/blocksd/config/`**: daemon configuration (Pydantic schema, TOML loader)
50
+ - **`src/blocksd/cli/`**: Typer CLI commands
51
+ - **`tests/`**: mirrors source structure
52
+
53
+ ## Best Areas to Contribute
54
+
55
+ - **LED patterns**: new visual patterns for Lightpad blocks
56
+ - **Touch event handling**: higher-level gestures, MIDI mapping
57
+ - **Web dashboard**: enhance the `blocksd ui` real-time interface
58
+ - **D-Bus interface**: IPC for desktop integration
59
+ - **Device support**: testing with Seaboard, Live, Loop, or Developer blocks
60
+ - **Documentation**: usage guides, API examples (see `docs/` for protocol reference)
61
+
62
+ ## Guidelines
63
+
64
+ - All code must pass `ruff check`, `ruff format --check`, and `ty check`
65
+ - Add tests for new functionality, the protocol layer especially should have exhaustive coverage
66
+ - Keep commits focused and descriptive
67
+ - The protocol layer is pure functions with no I/O, keep it that way
68
+
69
+ ## Testing Without Hardware
70
+
71
+ Most of the codebase is testable without a physical ROLI device. The protocol layer, LED bitmap logic, and LittleFoot assembler are all pure computation. Only the topology/connection layer requires hardware.
72
+
73
+ ```bash
74
+ # Run just the hardware-free tests
75
+ uv run pytest tests/protocol/ tests/led/ tests/littlefoot/ tests/device/
76
+ ```
blocksd-0.2.0/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025-2026 hyperb1iss (Stefanie Jane)
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.