Skip to content

Commit d3a7673

Browse files
authored
tooling: Add devcontainer for Python/MicroPython development. (#275)
* tooling: Add devcontainer for Python/MicroPython development. * ci: Standardize Node.js version to 22 across all environments. * tooling: Add venv support to Makefile with MicroPython stubs. * tooling: Configure Pylance, recommended extensions, and MicroPython stubs. * docs: Update CONTRIBUTING with prerequisites and devcontainer docs.
1 parent e7996a2 commit d3a7673

13 files changed

Lines changed: 266 additions & 27 deletions

File tree

.devcontainer/Dockerfile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM mcr.microsoft.com/devcontainers/python:3.10
2+
3+
# System packages for firmware build and board communication
4+
RUN apt-get update && apt-get install -y --no-install-recommends \
5+
gcc-arm-none-eabi \
6+
libnewlib-arm-none-eabi \
7+
openocd \
8+
udev \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
# udev rules for STeaMi board (DAPLink / STM32)
12+
RUN mkdir -p /etc/udev/rules.d \
13+
&& echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", MODE="0666"' \
14+
> /etc/udev/rules.d/99-steami.rules

.devcontainer/devcontainer.json

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{
2+
"name": "STeaMi MicroPython Dev",
3+
"build": {
4+
"dockerfile": "Dockerfile"
5+
},
6+
7+
// USB access for STeaMi board (DAPLink / mpremote / OpenOCD).
8+
// Privileged mode is required for firmware flashing and board communication.
9+
// This is incompatible with GitHub Codespaces but essential for local use.
10+
"privileged": true,
11+
"mounts": ["type=bind,source=/dev/bus/usb,target=/dev/bus/usb"],
12+
"runArgs": ["--device=/dev/bus/usb"],
13+
14+
"remoteEnv": {
15+
"VENV_DIR": "/home/vscode/.venv"
16+
},
17+
18+
"features": {
19+
"ghcr.io/devcontainers/features/node:1": {
20+
"version": "22"
21+
},
22+
"ghcr.io/devcontainers/features/common-utils:2": {
23+
"installZsh": true,
24+
"installOhMyZsh": true,
25+
"upgradePackages": true,
26+
"username": "vscode"
27+
},
28+
"ghcr.io/devcontainers/features/github-cli:1": {},
29+
"ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}
30+
},
31+
32+
"customizations": {
33+
"vscode": {
34+
"settings": {
35+
"terminal.integrated.profiles.linux": {
36+
"zsh": {
37+
"path": "zsh"
38+
}
39+
},
40+
"terminal.integrated.defaultProfile.linux": "zsh",
41+
42+
"editor.minimap.enabled": false,
43+
"editor.formatOnSave": true,
44+
"editor.formatOnPaste": true,
45+
"editor.rulers": [99],
46+
47+
"[python]": {
48+
"editor.defaultFormatter": "charliermarsh.ruff"
49+
},
50+
"[json]": {
51+
"editor.defaultFormatter": "esbenp.prettier-vscode"
52+
},
53+
"[jsonc]": {
54+
"editor.defaultFormatter": "esbenp.prettier-vscode"
55+
},
56+
"[makefile]": {
57+
"editor.tabSize": 8
58+
},
59+
"[yaml]": {
60+
"editor.tabSize": 2
61+
},
62+
63+
"python.defaultInterpreterPath": "/home/vscode/.venv/bin/python",
64+
"python.testing.pytestEnabled": true,
65+
"python.testing.pytestArgs": ["-v", "-k", "mock"],
66+
67+
"files.watcherExclude": {
68+
"**/.git/objects/**": true,
69+
"**/node_modules/**": true,
70+
"**/.build/**": true,
71+
"**/__pycache__/**": true
72+
},
73+
"search.exclude": {
74+
"**/node_modules": true,
75+
"**/.build": true
76+
}
77+
},
78+
"extensions": [
79+
"charliermarsh.ruff",
80+
"ms-python.python",
81+
"ms-python.vscode-pylance",
82+
"ms-vscode.vscode-serial-monitor",
83+
"esbenp.prettier-vscode",
84+
"github.vscode-pull-request-github",
85+
"vivaxy.vscode-conventional-commits"
86+
]
87+
}
88+
},
89+
90+
"postCreateCommand": "make setup VENV_DIR=/home/vscode/.venv && sudo /usr/lib/systemd/systemd-udevd --daemon",
91+
"remoteUser": "vscode"
92+
}

.github/workflows/check-commits.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: "🟢 Set up Node.js"
2323
uses: actions/setup-node@v4
2424
with:
25-
node-version: "20.17.x"
25+
node-version: "22"
2626
cache: "npm"
2727

2828
- name: "🛠 Install commitlint"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ __pycache__
88
node_modules/
99
CLAUDE.md
1010
.build/
11+
.venv/

.vscode/extensions.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"recommendations": [
3+
"charliermarsh.ruff",
4+
"ms-python.python",
5+
"ms-python.vscode-pylance",
6+
"ms-vscode.vscode-serial-monitor",
7+
"esbenp.prettier-vscode",
8+
"github.vscode-pull-request-github",
9+
"vivaxy.vscode-conventional-commits"
10+
]
11+
}

.vscode/settings.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"python.languageServer": "Pylance",
3+
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
4+
"python.analysis.typeCheckingMode": "basic",
5+
"python.analysis.extraPaths": [
6+
"lib/apds9960",
7+
"lib/bme280",
8+
"lib/bq27441",
9+
"lib/daplink_flash",
10+
"lib/gc9a01",
11+
"lib/hts221",
12+
"lib/im34dt05",
13+
"lib/ism330dl",
14+
"lib/lis2mdl",
15+
"lib/mcp23009e",
16+
"lib/ssd1327",
17+
"lib/steami_config",
18+
"lib/vl53l1x",
19+
"lib/wsen-hids",
20+
"lib/wsen-pads"
21+
],
22+
"python.analysis.stubPath": "typings",
23+
"python.analysis.diagnosticSeverityOverrides": {
24+
"reportMissingModuleSource": "none",
25+
"reportWildcardImportFromLibrary": "none",
26+
"reportGeneralTypeIssues": "warning"
27+
},
28+
"pylint.enabled": false,
29+
"mypy-type-checker.enabled": false
30+
}

CONTRIBUTING.md

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,40 @@ docs: Update README driver table.
8383
test(mcp23009e): Add mock scenarios for mcp23009e driver.
8484
```
8585

86+
## Prerequisites
87+
88+
For local development (without dev container):
89+
90+
* Python 3.10+
91+
* Node.js 22+ (for husky, commitlint, lint-staged, semantic-release)
92+
* `arm-none-eabi-gcc` toolchain (for `make firmware`)
93+
* OpenOCD (for `make deploy`)
94+
* `mpremote` (installed via `pip install -e ".[test]"`)
95+
* GitHub CLI (`gh`)
96+
97+
Then run `make setup` to install all dependencies and git hooks. This creates a `.venv` with ruff, pytest, mpremote, and MicroPython type stubs for Pylance.
98+
99+
## Dev Container
100+
101+
A dev container is available for VS Code (local Docker only, not GitHub Codespaces). It includes all prerequisites out of the box: Python 3.10, Node.js 22, ruff, pytest, mpremote, arm-none-eabi-gcc, OpenOCD, and the GitHub CLI.
102+
103+
1. Open the repository in VS Code
104+
2. When prompted, click **Reopen in Container** (or use the command palette: *Dev Containers: Reopen in Container*)
105+
3. The container runs `make setup` automatically on creation
106+
107+
The container also provides:
108+
109+
* **zsh + oh-my-zsh** as default shell with persistent shell history
110+
* **Pylance** configured with MicroPython STM32 stubs (no false `import machine` errors)
111+
* **Serial Monitor** extension for board communication
112+
* **USB passthrough** for mpremote, OpenOCD, and firmware flashing (the container runs in privileged mode with `/dev/bus/usb` mounted)
113+
* **udev rules** for the DAPLink interface (auto-started on container creation)
114+
115+
Note: GitHub Codespaces is not supported because the container requires privileged mode and USB device access for board communication.
116+
86117
## Workflow
87118

88-
1. Set up your environment: `make setup`
119+
1. Set up your environment: open in the dev container, or run `make setup` locally
89120
2. Create a branch from main (format: `feat/`, `fix/`, `docs/`, `tooling/`, `ci/`, `test/`, `style/`, `chore/`, `refactor/`)
90121
3. Write your code and add tests in `tests/scenarios/<driver>.yaml`
91122
4. Run `make ci` to verify everything passes (lint + tests + examples)
@@ -142,7 +173,7 @@ The firmware source is cloned into `.build/micropython-steami/` (gitignored). A
142173

143174
Use `make firmware` for normal rebuilds from the existing local clone. Use `make firmware-update` only when you want to refresh the `micropython-steami` checkout itself or resync the board-specific submodules before rebuilding.
144175

145-
**Requirements**: `arm-none-eabi-gcc` toolchain, OpenOCD for flashing, and `mpremote` for running scripts on the board.
176+
All these tools are included in the dev container. For local development, see the [Prerequisites](#prerequisites) section.
146177

147178
## Notes
148179

Makefile

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44

55
include env.mk
66

7+
# Venv path (override with VENV_DIR=/path for devcontainer)
8+
VENV_DIR ?= .venv
9+
10+
# Use venv Python/tools when available, fallback to system
11+
PYTHON := $(shell [ -x $(VENV_DIR)/bin/python ] && echo $(VENV_DIR)/bin/python || echo python3)
12+
713
# --- Setup ---
814

915
# npm install is re-run only when package.json changes
@@ -18,31 +24,34 @@ prepare: node_modules/.package-lock.json ## Install git hooks
1824
.PHONY: setup
1925
setup: install prepare ## Full dev environment setup
2026

27+
$(VENV_DIR)/bin/activate:
28+
python3 -m venv $(VENV_DIR)
29+
2130
.PHONY: install
22-
install: node_modules/.package-lock.json ## Install dev tools (pip + npm)
23-
python3 -m pip install -e ".[dev,test]"
31+
install: $(VENV_DIR)/bin/activate node_modules/.package-lock.json ## Install dev tools (pip + npm)
32+
$(VENV_DIR)/bin/pip install -e ".[dev,test]"
2433

2534
# --- Linting ---
2635

2736
.PHONY: lint
2837
lint: ## Run ruff linter
29-
ruff check
38+
$(PYTHON) -m ruff check
3039

3140
.PHONY: lint-fix
3241
lint-fix: ## Auto-fix lint issues
33-
ruff check --fix
42+
$(PYTHON) -m ruff check --fix
3443

3544
# --- Testing ---
3645

3746
# Dynamic per-scenario targets (test-apds9960, test-hts221, etc.)
3847
# Uses 'driver' field for driver scenarios, filename stem for board scenarios.
3948
# Convention: for board scenarios, the YAML 'name' field must match the filename.
40-
SCENARIOS := $(shell python3 -c "import yaml,glob,os; [print(d.get('driver',os.path.basename(f).replace('.yaml',''))) for f in sorted(glob.glob('tests/scenarios/*.yaml')) for d in [yaml.safe_load(open(f))]]" 2>/dev/null)
41-
$(foreach s,$(SCENARIOS),$(eval .PHONY: test-$(s))$(eval test-$(s): ; python3 -m pytest tests/ -v -k "$(s)" --port $$(PORT) -s))
49+
SCENARIOS := $(shell $(PYTHON) -c "import yaml,glob,os; [print(d.get('driver',os.path.basename(f).replace('.yaml',''))) for f in sorted(glob.glob('tests/scenarios/*.yaml')) for d in [yaml.safe_load(open(f))]]" 2>/dev/null)
50+
$(foreach s,$(SCENARIOS),$(eval .PHONY: test-$(s))$(eval test-$(s): ; $(PYTHON) -m pytest tests/ -v -k "$(s)" --port $$(PORT) -s))
4251

4352
.PHONY: test-mock
4453
test-mock: ## Run mock tests (no hardware needed)
45-
python3 -m pytest tests/ -v -k mock
54+
$(PYTHON) -m pytest tests/ -v -k mock
4655

4756
.PHONY: test
4857
test: test-mock ## Run mock tests (use 'make test-all' for mock + hardware)
@@ -51,23 +60,23 @@ test: test-mock ## Run mock tests (use 'make test-all' for mock + hardware)
5160

5261
.PHONY: test-hardware
5362
test-hardware: ## Run all hardware tests (needs board on PORT)
54-
python3 -m pytest tests/ -v --port $(PORT) -s -k hardware
63+
$(PYTHON) -m pytest tests/ -v --port $(PORT) -s -k hardware
5564

5665
.PHONY: test-board
5766
test-board: ## Run board tests only (buttons, LEDs, buzzer, screen)
58-
python3 -m pytest tests/ -v --port $(PORT) -s -k "board_ and hardware"
67+
$(PYTHON) -m pytest tests/ -v --port $(PORT) -s -k "board_ and hardware"
5968

6069
.PHONY: test-sensors
6170
test-sensors: ## Run sensor driver hardware tests (I2C devices)
62-
python3 -m pytest tests/ -v --port $(PORT) -s -k "hardware and not board_"
71+
$(PYTHON) -m pytest tests/ -v --port $(PORT) -s -k "hardware and not board_"
6372

6473
.PHONY: test-all
6574
test-all: ## Run all tests (mock + hardware)
66-
python3 -m pytest tests/ -v --port $(PORT) -s
75+
$(PYTHON) -m pytest tests/ -v --port $(PORT) -s
6776

6877
.PHONY: test-examples
6978
test-examples: ## Validate all example files (syntax + imports)
70-
python3 -m pytest tests/test_examples.py -v
79+
$(PYTHON) -m pytest tests/test_examples.py -v
7180

7281
# --- CI ---
7382

@@ -113,20 +122,20 @@ run: ## Run a script on the board with live output (SCRIPT=path/to/file.py)
113122
@if [ -z "$(SCRIPT)" ]; then \
114123
echo "Error: SCRIPT is required. Usage: make run SCRIPT=lib/.../example.py"; exit 1; \
115124
fi
116-
mpremote connect $(PORT) run $(SCRIPT)
125+
$(PYTHON) -m mpremote connect $(PORT) run $(SCRIPT)
117126

118127
.PHONY: deploy-script
119128
deploy-script: ## Deploy a script as main.py for autonomous execution (SCRIPT=path/to/file.py)
120129
@if [ -z "$(SCRIPT)" ]; then \
121130
echo "Error: SCRIPT is required. Usage: make deploy-script SCRIPT=lib/.../example.py"; exit 1; \
122131
fi
123-
mpremote connect $(PORT) cp $(SCRIPT) :main.py
124-
mpremote connect $(PORT) reset
132+
$(PYTHON) -m mpremote connect $(PORT) cp $(SCRIPT) :main.py
133+
$(PYTHON) -m mpremote connect $(PORT) reset
125134
@echo "Script deployed as main.py and board reset."
126135

127136
.PHONY: run-main
128137
run-main: ## Re-execute main.py on the board and capture output
129-
mpremote connect $(PORT) exec "exec(open('/flash/main.py').read())"
138+
$(PYTHON) -m mpremote connect $(PORT) exec "exec(open('/flash/main.py').read())"
130139

131140
.PHONY: firmware-clean
132141
firmware-clean: ## Clean firmware build artifacts
@@ -138,11 +147,11 @@ firmware-clean: ## Clean firmware build artifacts
138147

139148
.PHONY: repl
140149
repl: ## Open MicroPython REPL on the board
141-
mpremote connect $(PORT)
150+
$(PYTHON) -m mpremote connect $(PORT)
142151

143152
.PHONY: mount
144153
mount: ## Mount lib/ on the board for live testing
145-
mpremote connect $(PORT) mount lib/
154+
$(PYTHON) -m mpremote connect $(PORT) mount lib/
146155

147156
# --- Release ---
148157

@@ -179,7 +188,7 @@ bump: ## Create a version tag (PART=patch|minor|major, default: patch)
179188
fi; \
180189
echo "$$LAST → $$NEXT"; \
181190
VERSION=$${NEXT#v}; \
182-
python3 -c "import re, pathlib; p=pathlib.Path('pyproject.toml'); p.write_text(re.sub(r'^version = \".*\"', 'version = \"$$VERSION\"', p.read_text(), count=1, flags=re.MULTILINE))"; \
191+
$(PYTHON) -c "import re, pathlib; p=pathlib.Path('pyproject.toml'); p.write_text(re.sub(r'^version = \".*\"', 'version = \"$$VERSION\"', p.read_text(), count=1, flags=re.MULTILINE))"; \
183192
git add pyproject.toml; \
184193
git commit -m "chore: Bump version to $$NEXT."; \
185194
git tag -a "$$NEXT" -m "Release $$NEXT"; \
@@ -194,10 +203,11 @@ clean: ## Remove build artifacts and caches
194203
find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
195204
find . -type d -name .ruff_cache -exec rm -rf {} + 2>/dev/null || true
196205
find . -type d -name .mypy_cache -exec rm -rf {} + 2>/dev/null || true
206+
find . -type d -name '*.egg-info' -exec rm -rf {} + 2>/dev/null || true
197207

198208
.PHONY: deepclean
199-
deepclean: clean ## Remove everything including node_modules and firmware
200-
rm -rf node_modules
209+
deepclean: clean ## Remove everything including node_modules, venv and firmware
210+
rm -rf node_modules $(VENV_DIR)
201211
@if [ -d "$(BUILD_DIR)" ]; then rm -rf "$(BUILD_DIR)"; fi
202212

203213
.PHONY: help

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"private": true,
44
"description": "MicroPython driver library for the STeaMi board.",
55
"engines": {
6-
"node": ">=20.17"
6+
"node": ">=22"
77
},
88
"scripts": {
99
"build": "make build",

0 commit comments

Comments
 (0)