Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM mcr.microsoft.com/devcontainers/python:3.10

# System packages for firmware build and board communication
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc-arm-none-eabi \
libnewlib-arm-none-eabi \
openocd \
udev \
&& rm -rf /var/lib/apt/lists/*

# udev rules for STeaMi board (DAPLink / STM32)
RUN mkdir -p /etc/udev/rules.d \
&& echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="0d28", MODE="0666"' \
> /etc/udev/rules.d/99-steami.rules
92 changes: 92 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"name": "STeaMi MicroPython Dev",
"build": {
"dockerfile": "Dockerfile"
},

// USB access for STeaMi board (DAPLink / mpremote / OpenOCD).
// Privileged mode is required for firmware flashing and board communication.
// This is incompatible with GitHub Codespaces but essential for local use.
"privileged": true,
"mounts": ["type=bind,source=/dev/bus/usb,target=/dev/bus/usb"],
"runArgs": ["--device=/dev/bus/usb"],
Comment on lines +10 to +12
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The devcontainer is advertised as working in GitHub Codespaces, but privileged: true plus USB device mounts (/dev/bus/usb) are generally unsupported there and can prevent the container from starting. Consider making USB passthrough optional (e.g., via a separate/local override devcontainer config) and keeping the default config Codespaces-compatible.

Suggested change
"privileged": true,
"mounts": ["type=bind,source=/dev/bus/usb,target=/dev/bus/usb"],
"runArgs": ["--device=/dev/bus/usb"],
// NOTE: GitHub Codespaces does not support privileged containers or direct USB device mounts.
// To enable USB passthrough in a local Docker/devcontainer environment, create a
// `.devcontainer/devcontainer.local.json` that extends this config and adds, for example:
// "privileged": true,
// "mounts": ["type=bind,source=/dev/bus/usb,target=/dev/bus/usb"],
// "runArgs": ["--device=/dev/bus/usb"],

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mounts binding /dev/bus/usb and runArgs with --device=/dev/bus/usb are overlapping ways to expose the same device path. Consider keeping just one to reduce redundancy and potential host-compatibility issues.

Suggested change
"runArgs": ["--device=/dev/bus/usb"],

Copilot uses AI. Check for mistakes.

"remoteEnv": {
"VENV_DIR": "/home/vscode/.venv"
},

"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "22"
},
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"installOhMyZsh": true,
"upgradePackages": true,
"username": "vscode"
},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/stuartleeks/dev-container-features/shell-history:0": {}
},

"customizations": {
"vscode": {
"settings": {
"terminal.integrated.profiles.linux": {
"zsh": {
"path": "zsh"
}
},
"terminal.integrated.defaultProfile.linux": "zsh",

"editor.minimap.enabled": false,
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.rulers": [99],

"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[makefile]": {
"editor.tabSize": 8
},
"[yaml]": {
"editor.tabSize": 2
},

"python.defaultInterpreterPath": "/home/vscode/.venv/bin/python",
"python.testing.pytestEnabled": true,
"python.testing.pytestArgs": ["-v", "-k", "mock"],

"files.watcherExclude": {
"**/.git/objects/**": true,
"**/node_modules/**": true,
"**/.build/**": true,
"**/__pycache__/**": true
},
"search.exclude": {
"**/node_modules": true,
"**/.build": true
}
},
"extensions": [
"charliermarsh.ruff",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-vscode.vscode-serial-monitor",
"esbenp.prettier-vscode",
"github.vscode-pull-request-github",
"vivaxy.vscode-conventional-commits"
]
}
},

"postCreateCommand": "make setup VENV_DIR=/home/vscode/.venv && sudo /usr/lib/systemd/systemd-udevd --daemon",
"remoteUser": "vscode"
}
2 changes: 1 addition & 1 deletion .github/workflows/check-commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: "🟢 Set up Node.js"
uses: actions/setup-node@v4
with:
node-version: "20.17.x"
node-version: "22"
cache: "npm"

- name: "🛠 Install commitlint"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ __pycache__
node_modules/
CLAUDE.md
.build/
.venv/
11 changes: 11 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"recommendations": [
"charliermarsh.ruff",
"ms-python.python",
"ms-python.vscode-pylance",
"ms-vscode.vscode-serial-monitor",
"esbenp.prettier-vscode",
"github.vscode-pull-request-github",
"vivaxy.vscode-conventional-commits"
]
}
30 changes: 30 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"python.languageServer": "Pylance",
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.analysis.typeCheckingMode": "basic",
"python.analysis.extraPaths": [
"lib/apds9960",
"lib/bme280",
"lib/bq27441",
"lib/daplink_flash",
"lib/gc9a01",
"lib/hts221",
"lib/im34dt05",
"lib/ism330dl",
"lib/lis2mdl",
"lib/mcp23009e",
"lib/ssd1327",
"lib/steami_config",
"lib/vl53l1x",
"lib/wsen-hids",
"lib/wsen-pads"
],
"python.analysis.stubPath": "typings",
"python.analysis.diagnosticSeverityOverrides": {
"reportMissingModuleSource": "none",
"reportWildcardImportFromLibrary": "none",
"reportGeneralTypeIssues": "warning"
},
"pylint.enabled": false,
"mypy-type-checker.enabled": false
}
35 changes: 33 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,40 @@ docs: Update README driver table.
test(mcp23009e): Add mock scenarios for mcp23009e driver.
```

## Prerequisites

For local development (without dev container):
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONTRIBUTING says the dev container includes Node.js 20, but the actual devcontainer feature and repo engines/CI have been updated to Node 22. Please update this section to match the effective Node version (or adjust the devcontainer/engines back to 20 if that’s the intent).

Copilot uses AI. Check for mistakes.

* Python 3.10+
* Node.js 22+ (for husky, commitlint, lint-staged, semantic-release)
* `arm-none-eabi-gcc` toolchain (for `make firmware`)
* OpenOCD (for `make deploy`)
* `mpremote` (installed via `pip install -e ".[test]"`)
* GitHub CLI (`gh`)

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.

## Dev Container

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.

1. Open the repository in VS Code
2. When prompted, click **Reopen in Container** (or use the command palette: *Dev Containers: Reopen in Container*)
3. The container runs `make setup` automatically on creation

The container also provides:

* **zsh + oh-my-zsh** as default shell with persistent shell history
* **Pylance** configured with MicroPython STM32 stubs (no false `import machine` errors)
* **Serial Monitor** extension for board communication
* **USB passthrough** for mpremote, OpenOCD, and firmware flashing (the container runs in privileged mode with `/dev/bus/usb` mounted)
* **udev rules** for the DAPLink interface (auto-started on container creation)

Note: GitHub Codespaces is not supported because the container requires privileged mode and USB device access for board communication.

## Workflow

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

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.

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

## Notes

Expand Down
52 changes: 31 additions & 21 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

include env.mk

# Venv path (override with VENV_DIR=/path for devcontainer)
VENV_DIR ?= .venv

# Use venv Python/tools when available, fallback to system
PYTHON := $(shell [ -x $(VENV_DIR)/bin/python ] && echo $(VENV_DIR)/bin/python || echo python3)

# --- Setup ---

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

$(VENV_DIR)/bin/activate:
python3 -m venv $(VENV_DIR)

.PHONY: install
install: node_modules/.package-lock.json ## Install dev tools (pip + npm)
python3 -m pip install -e ".[dev,test]"
install: $(VENV_DIR)/bin/activate node_modules/.package-lock.json ## Install dev tools (pip + npm)
$(VENV_DIR)/bin/pip install -e ".[dev,test]"

# --- Linting ---

.PHONY: lint
lint: ## Run ruff linter
ruff check
$(PYTHON) -m ruff check

.PHONY: lint-fix
lint-fix: ## Auto-fix lint issues
ruff check --fix
$(PYTHON) -m ruff check --fix

# --- Testing ---

# Dynamic per-scenario targets (test-apds9960, test-hts221, etc.)
# Uses 'driver' field for driver scenarios, filename stem for board scenarios.
# Convention: for board scenarios, the YAML 'name' field must match the filename.
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)
$(foreach s,$(SCENARIOS),$(eval .PHONY: test-$(s))$(eval test-$(s): ; python3 -m pytest tests/ -v -k "$(s)" --port $$(PORT) -s))
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)
$(foreach s,$(SCENARIOS),$(eval .PHONY: test-$(s))$(eval test-$(s): ; $(PYTHON) -m pytest tests/ -v -k "$(s)" --port $$(PORT) -s))

.PHONY: test-mock
test-mock: ## Run mock tests (no hardware needed)
python3 -m pytest tests/ -v -k mock
$(PYTHON) -m pytest tests/ -v -k mock

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

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

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

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

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

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

# --- CI ---

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

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

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

.PHONY: firmware-clean
firmware-clean: ## Clean firmware build artifacts
Expand All @@ -138,11 +147,11 @@ firmware-clean: ## Clean firmware build artifacts

.PHONY: repl
repl: ## Open MicroPython REPL on the board
mpremote connect $(PORT)
$(PYTHON) -m mpremote connect $(PORT)

.PHONY: mount
mount: ## Mount lib/ on the board for live testing
mpremote connect $(PORT) mount lib/
$(PYTHON) -m mpremote connect $(PORT) mount lib/

# --- Release ---

Expand Down Expand Up @@ -179,7 +188,7 @@ bump: ## Create a version tag (PART=patch|minor|major, default: patch)
fi; \
echo "$$LAST → $$NEXT"; \
VERSION=$${NEXT#v}; \
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))"; \
$(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))"; \
git add pyproject.toml; \
git commit -m "chore: Bump version to $$NEXT."; \
git tag -a "$$NEXT" -m "Release $$NEXT"; \
Expand All @@ -194,10 +203,11 @@ clean: ## Remove build artifacts and caches
find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .ruff_cache -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .mypy_cache -exec rm -rf {} + 2>/dev/null || true
find . -type d -name '*.egg-info' -exec rm -rf {} + 2>/dev/null || true

.PHONY: deepclean
deepclean: clean ## Remove everything including node_modules and firmware
rm -rf node_modules
deepclean: clean ## Remove everything including node_modules, venv and firmware
rm -rf node_modules $(VENV_DIR)
@if [ -d "$(BUILD_DIR)" ]; then rm -rf "$(BUILD_DIR)"; fi

.PHONY: help
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": true,
"description": "MicroPython driver library for the STeaMi board.",
"engines": {
"node": ">=20.17"
"node": ">=22"
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bumping the minimum supported Node version from >=20.17 to >=22 is a breaking change for contributors not using the dev container/CI. If there isn’t a specific dependency requiring Node 22, consider keeping the previous floor (or document the rationale clearly in CONTRIBUTING/README).

Suggested change
"node": ">=22"
"node": ">=20.17"

Copilot uses AI. Check for mistakes.
},
"scripts": {
"build": "make build",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ license = {text = "GPL-3.0-or-later"}
requires-python = ">=3.7"

[project.optional-dependencies]
dev = ["ruff==0.11.6"]
dev = ["ruff==0.11.6", "micropython-stm32-stubs>=1.24"]
test = ["pytest==7.4.0", "pyyaml==6.0.2", "mpremote>=1.0"]

[tool.setuptools]
Expand Down
Loading
Loading