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.
- blocksd-0.2.0/.github/FUNDING.yml +4 -0
- blocksd-0.2.0/.github/workflows/ci.yml +27 -0
- blocksd-0.2.0/.github/workflows/publish.yml +63 -0
- blocksd-0.2.0/.github/workflows/release.yml +128 -0
- blocksd-0.2.0/.gitignore +21 -0
- blocksd-0.2.0/.pre-commit-config.yaml +21 -0
- blocksd-0.2.0/.python-version +1 -0
- blocksd-0.2.0/CLAUDE.md +305 -0
- blocksd-0.2.0/CONTRIBUTING.md +76 -0
- blocksd-0.2.0/LICENSE +15 -0
- blocksd-0.2.0/PKG-INFO +362 -0
- blocksd-0.2.0/README.md +335 -0
- blocksd-0.2.0/VISION.md +198 -0
- blocksd-0.2.0/docs/API.md +357 -0
- blocksd-0.2.0/docs/FIRMWARE_NOTES.md +141 -0
- blocksd-0.2.0/firmware/default/loopblock.littlefoot +112 -0
- blocksd-0.2.0/firmware/default/padblock.littlefoot +1313 -0
- blocksd-0.2.0/firmware/default/seaboardblock.littlefoot +195 -0
- blocksd-0.2.0/install.sh +119 -0
- blocksd-0.2.0/justfile +81 -0
- blocksd-0.2.0/packaging/aur/blocksd/PKGBUILD +56 -0
- blocksd-0.2.0/packaging/aur/blocksd-git/PKGBUILD +61 -0
- blocksd-0.2.0/pyproject.toml +172 -0
- blocksd-0.2.0/src/blocksd/__init__.py +3 -0
- blocksd-0.2.0/src/blocksd/__main__.py +5 -0
- blocksd-0.2.0/src/blocksd/api/__init__.py +5 -0
- blocksd-0.2.0/src/blocksd/api/events.py +183 -0
- blocksd-0.2.0/src/blocksd/api/http.py +146 -0
- blocksd-0.2.0/src/blocksd/api/protocol.py +62 -0
- blocksd-0.2.0/src/blocksd/api/server.py +691 -0
- blocksd-0.2.0/src/blocksd/api/websocket.py +73 -0
- blocksd-0.2.0/src/blocksd/cli/__init__.py +1 -0
- blocksd-0.2.0/src/blocksd/cli/app.py +186 -0
- blocksd-0.2.0/src/blocksd/cli/config.py +140 -0
- blocksd-0.2.0/src/blocksd/cli/install.py +153 -0
- blocksd-0.2.0/src/blocksd/cli/led.py +150 -0
- blocksd-0.2.0/src/blocksd/config/__init__.py +1 -0
- blocksd-0.2.0/src/blocksd/config/loader.py +38 -0
- blocksd-0.2.0/src/blocksd/config/schema.py +32 -0
- blocksd-0.2.0/src/blocksd/daemon.py +158 -0
- blocksd-0.2.0/src/blocksd/device/__init__.py +1 -0
- blocksd-0.2.0/src/blocksd/device/config_ids.py +31 -0
- blocksd-0.2.0/src/blocksd/device/connection.py +119 -0
- blocksd-0.2.0/src/blocksd/device/models.py +122 -0
- blocksd-0.2.0/src/blocksd/device/registry.py +67 -0
- blocksd-0.2.0/src/blocksd/led/__init__.py +1 -0
- blocksd-0.2.0/src/blocksd/led/bitmap.py +132 -0
- blocksd-0.2.0/src/blocksd/led/patterns.py +90 -0
- blocksd-0.2.0/src/blocksd/littlefoot/__init__.py +1 -0
- blocksd-0.2.0/src/blocksd/littlefoot/assembler.py +300 -0
- blocksd-0.2.0/src/blocksd/littlefoot/opcodes.py +75 -0
- blocksd-0.2.0/src/blocksd/littlefoot/programs.py +153 -0
- blocksd-0.2.0/src/blocksd/logging.py +26 -0
- blocksd-0.2.0/src/blocksd/protocol/__init__.py +1 -0
- blocksd-0.2.0/src/blocksd/protocol/builder.py +124 -0
- blocksd-0.2.0/src/blocksd/protocol/checksum.py +12 -0
- blocksd-0.2.0/src/blocksd/protocol/constants.py +157 -0
- blocksd-0.2.0/src/blocksd/protocol/data_change.py +405 -0
- blocksd-0.2.0/src/blocksd/protocol/decoder.py +426 -0
- blocksd-0.2.0/src/blocksd/protocol/packing.py +95 -0
- blocksd-0.2.0/src/blocksd/protocol/remote_heap.py +251 -0
- blocksd-0.2.0/src/blocksd/protocol/serial.py +50 -0
- blocksd-0.2.0/src/blocksd/py.typed +0 -0
- blocksd-0.2.0/src/blocksd/sdnotify.py +95 -0
- blocksd-0.2.0/src/blocksd/topology/__init__.py +1 -0
- blocksd-0.2.0/src/blocksd/topology/detector.py +97 -0
- blocksd-0.2.0/src/blocksd/topology/device_group.py +695 -0
- blocksd-0.2.0/src/blocksd/topology/manager.py +175 -0
- blocksd-0.2.0/src/blocksd/web/__init__.py +14 -0
- blocksd-0.2.0/systemd/99-roli-blocks.rules +19 -0
- blocksd-0.2.0/systemd/blocksd.service +22 -0
- blocksd-0.2.0/tests/__init__.py +0 -0
- blocksd-0.2.0/tests/cli/__init__.py +0 -0
- blocksd-0.2.0/tests/cli/test_config.py +42 -0
- blocksd-0.2.0/tests/cli/test_led.py +129 -0
- blocksd-0.2.0/tests/device/__init__.py +0 -0
- blocksd-0.2.0/tests/device/test_events.py +75 -0
- blocksd-0.2.0/tests/led/__init__.py +0 -0
- blocksd-0.2.0/tests/led/test_bitmap.py +229 -0
- blocksd-0.2.0/tests/led/test_patterns.py +120 -0
- blocksd-0.2.0/tests/littlefoot/__init__.py +0 -0
- blocksd-0.2.0/tests/littlefoot/test_assembler.py +234 -0
- blocksd-0.2.0/tests/littlefoot/test_programs.py +57 -0
- blocksd-0.2.0/tests/protocol/__init__.py +0 -0
- blocksd-0.2.0/tests/protocol/test_builder.py +43 -0
- blocksd-0.2.0/tests/protocol/test_builder_config.py +71 -0
- blocksd-0.2.0/tests/protocol/test_checksum.py +23 -0
- blocksd-0.2.0/tests/protocol/test_data_change.py +432 -0
- blocksd-0.2.0/tests/protocol/test_decoder.py +479 -0
- blocksd-0.2.0/tests/protocol/test_packing.py +86 -0
- blocksd-0.2.0/tests/protocol/test_remote_heap.py +503 -0
- blocksd-0.2.0/tests/protocol/test_serial.py +37 -0
- blocksd-0.2.0/tests/test_api_events.py +215 -0
- blocksd-0.2.0/tests/test_api_protocol.py +208 -0
- blocksd-0.2.0/tests/test_http.py +121 -0
- blocksd-0.2.0/tests/test_sdnotify.py +47 -0
- blocksd-0.2.0/tests/test_web_server.py +219 -0
- blocksd-0.2.0/tests/test_websocket.py +106 -0
- blocksd-0.2.0/tests/topology/__init__.py +0 -0
- blocksd-0.2.0/tests/topology/test_detector.py +47 -0
- blocksd-0.2.0/tests/topology/test_device_group.py +318 -0
- blocksd-0.2.0/uv.lock +587 -0
- blocksd-0.2.0/web/biome.json +47 -0
- blocksd-0.2.0/web/bun.lock +365 -0
- blocksd-0.2.0/web/index.html +13 -0
- blocksd-0.2.0/web/package.json +27 -0
- blocksd-0.2.0/web/src/App.tsx +20 -0
- blocksd-0.2.0/web/src/components/dashboard/DashboardView.tsx +48 -0
- blocksd-0.2.0/web/src/components/dashboard/DeviceCard.tsx +67 -0
- blocksd-0.2.0/web/src/components/dashboard/TopologyMap.tsx +102 -0
- blocksd-0.2.0/web/src/components/device/ConfigPanel.tsx +46 -0
- blocksd-0.2.0/web/src/components/device/ConfigSlider.tsx +42 -0
- blocksd-0.2.0/web/src/components/device/DeviceDetailView.tsx +73 -0
- blocksd-0.2.0/web/src/components/layout/AppShell.tsx +28 -0
- blocksd-0.2.0/web/src/components/layout/DeviceList.tsx +56 -0
- blocksd-0.2.0/web/src/components/layout/StatusBar.tsx +47 -0
- blocksd-0.2.0/web/src/hooks/useBlocksd.tsx +104 -0
- blocksd-0.2.0/web/src/hooks/useConfig.ts +73 -0
- blocksd-0.2.0/web/src/hooks/useDevices.ts +42 -0
- blocksd-0.2.0/web/src/hooks/useTopology.ts +30 -0
- blocksd-0.2.0/web/src/lib/constants.ts +128 -0
- blocksd-0.2.0/web/src/lib/frame.ts +18 -0
- blocksd-0.2.0/web/src/lib/protocol.ts +101 -0
- blocksd-0.2.0/web/src/lib/types.ts +41 -0
- blocksd-0.2.0/web/src/main.tsx +10 -0
- blocksd-0.2.0/web/src/pages/Dashboard.tsx +10 -0
- blocksd-0.2.0/web/src/pages/DeviceDetail.tsx +27 -0
- blocksd-0.2.0/web/src/theme/global.css +47 -0
- blocksd-0.2.0/web/src/theme/tokens.ts +18 -0
- blocksd-0.2.0/web/src/vite-env.d.ts +1 -0
- blocksd-0.2.0/web/tsconfig.json +20 -0
- blocksd-0.2.0/web/vite.config.ts +20 -0
|
@@ -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
|
blocksd-0.2.0/.gitignore
ADDED
|
@@ -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
|
blocksd-0.2.0/CLAUDE.md
ADDED
|
@@ -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.
|