Skip to content

Development Guide

Varun Karthic edited this page Oct 10, 2026 · 4 revisions

Development Guide

How to work on imPress: setting up, the repository layout, the workflow, conventions, and recipes for common changes. References: docs/guides/development-setup.md, docs/design/decisions.md.

Contents


Setting up

Tool Version For
Python 3.12+ (3.14 tested locally) the backend and tests
C compiler (clang or gcc) any recent firmware host tests (no ESP-IDF needed)
ESP-IDF or Docker v6.1 / any firmware builds
Node.js 20 the React UI (optional)
Chromium or Google Chrome any browser tests (optional; Playwright can download Chromium)

Backend with auto-reload

cd backend
python -m venv .venv && source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -r requirements-dev.txt                      # runtime + pytest, httpx, ruff, pip-audit
cat > .env <<'EOF'
IMPRESS_DEBUG=true                       # allows the published default secrets locally
IMPRESS_INITIAL_ADMIN_PASSWORD=dev-admin
EOF
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
  • Web app: http://localhost:8000; sign in as admin / dev-admin.
  • API explorer: http://localhost:8000/docs.
  • Database: backend/impress.db. Delete it for a fresh start; tables and the admin are recreated on boot.
  • Working directory: always start from backend/. The database path, .env and firmware_bins/ are relative to it.

Seeding data quickly

TOKEN=$(curl -s localhost:8000/api/auth/login -H 'content-type: application/json' \
  -d '{"username":"admin","password":"dev-admin"}' | python -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
H="Authorization: Bearer $TOKEN"
curl -s localhost:8000/api/admin/classes -H "$H" -H 'content-type: application/json' \
  -d '{"name":"Physics","code":"PHY101","classroom_code":"RM-1"}'
curl -s -X POST localhost:8000/api/classes/1/activate -H "$H"
curl -s localhost:8000/api/students/ -H "$H" -H 'content-type: application/json' \
  -d '{"roll_number":"ABCDE12345","student_name":"Asha Rao"}'

The React UI (optional)

cd frontend
npm ci
npm run dev          # http://localhost:5173, proxying /api and /ws to :8000

The React UI is a development dashboard and doesn't cover every feature. The built-in web app is the product UI.

Firmware

firmware/run_host_tests.sh                 # all host C suites with ASan/UBSan
python -m pytest -q firmware/tests         # structural tests
docker run --rm -v "$PWD/firmware":/project -w /project/class_c6 espressif/idf:v6.1 idf.py build

More in Firmware: Build and Flash.

Repository layout

imPress/
├── backend/
│   ├── app/
│   │   ├── main.py              app, startup, /health, the web app
│   │   ├── config.py            settings (IMPRESS_*)
│   │   ├── auth.py              passwords, sessions, roles
│   │   ├── device_auth.py       shared and per-device keys
│   │   ├── database.py          engine, commit helpers, foreign keys
│   │   ├── migrations.py        versioned migrations
│   │   ├── models.py, schemas.py
│   │   ├── routers/             auth, admin, classes, courses, students, quizzes, polls, device, firmware, deployments
│   │   ├── services/            presence, health, student_modules, deployments, firmware_*, *_import, records, sessions
│   │   ├── ws/                  WebSocket rooms
│   │   ├── static/, templates/  the web app
│   ├── tests/                   backend tests, gateway simulator, fixtures
│   ├── requirements*.txt        runtime, dev, and the hash-locked set
│   └── .env.example
├── firmware/
│   ├── protocol/                shared wire formats (+ host tests)
│   ├── class_c6/                gateway (+ host tests)
│   ├── class_s3/                hub
│   ├── student/                 student module (+ host tests)
│   ├── contract/                the backend → gateway WebSocket fixture
│   ├── tests/                   structural tests
│   └── sdkconfig.defaults.signed
├── scripts/                     install.sh, start.sh, init_env.py, smoke_test.py, firmware_key.sh, build_signed.sh, verify_firmware.py
├── ui_tests/                    Playwright browser tests
├── tests/                       repository, CI and documentation checks
├── frontend/                    React development UI
├── docs/                        architecture, design, API, guides, reference, engineering, hardware, firmware, testing, deployment
├── run_tests.py                 the test runner
├── VERSION                      release version
└── .github/workflows/ci.yml

Workflow

  1. Open or pick an issue. Bigger changes get a detailed issue first.
  2. Branch from varun/v2.1: feat/<summary>, fix/<summary> or docs/<summary>, one branch per issue. Never commit to main, v2 or v3, and never force-push.
  3. Change code and tests together. Run python run_tests.py (add --with-ui / --with-idf when you touched those parts).
  4. Update the docs in docs/. Repository checks fail on broken links and anchors, undocumented routes or settings, and an incomplete README.
  5. Commit with conventional messages (feat(scope): …, fix(…), docs(…), test(…), ci(…), security(…)).
  6. Open a pull request into varun/v2.1 with a detailed description:
    • summary and linked issue;
    • technical explanation;
    • files and tests;
    • CI;
    • compatibility;
    • database, firmware and security impact;
    • screenshots;
    • rollback;
    • hardware verification, stated honestly.
  7. Merge only when the CI result check is green.

Conventions

Area Convention
Python ruff (pyflakes rules); type hints; comments explain why, not what
C no blocking work in radio callbacks; driver-owned buffers outlive the operation; one owner per cJSON tree; every wire format through firmware/protocol
Errors HTTPException with a clear message; commit_or_conflict() for writes that can race
Time timeutil.istnow(); store naive IST
Activity log_activity(db, …) before the commit, so the log row commits with the change
Broadcasts after the commit
Settings new IMPRESS_* settings go in config.py, docs/guides/configuration.md and, if important, .env.example
Secrets never in the repository; .gitignore excludes *.pem, *.key, .env, *.db
Line endings LF (.gitattributes)

Simulating a gateway without hardware

K='X-API-Key: impress-device-key-2024'   # the DEBUG default; else your IMPRESS_DEVICE_API_KEY
curl -s localhost:8000/api/device/register -H "$K" -H 'content-type: application/json' \
  -d '{"mac_address":"48:F6:EE:00:00:01","device_type":"c6","device_name":"sim","classroom_code":"RM-1"}'
# → {"device_id":1,"status":"registered","class_id":1,"device_key":"…"}   (you may keep using the shared key)
curl -s localhost:8000/api/device/batch -H "$K" -H 'content-type: application/json' -d '{"device_type":"c6","messages":[
  {"type":"student_join","enrollment_number":"ABCDE12345","device_mac":"48:F6:EE:00:00:01","device_id":4660}]}'

To receive commands like a gateway, connect a WebSocket client with the key header, then start a quiz in the web app:

websocat -H "X-API-Key: impress-device-key-2024" ws://localhost:8000/ws/class/1?role=device

In tests, use backend/tests/gateway_sim.Gateway: it registers, sends heartbeats, batches with the firmware's retry rules, and runs OTA updates, with faults on demand.

Recipes

Adding an API endpoint

  1. Add it to the right router in backend/app/routers/, with an auth dependency (require_admin, require_teacher_or_admin, a class-access check, or device_caller + authorize for devices).
  2. Add the request and response schemas to schemas.py.
  3. Write tests in backend/tests/, including access control.
  4. Document it in docs/api/rest-api.md (or device-api.md). test_every_api_route_is_documented fails otherwise.
  5. test_route_guards.py checks it refuses unauthenticated callers. If it must be public, add it to the explicit list there, with a reason.

Adding a database column or table

  • A new table: add it to models.py. It is created at startup.
  • A new column on an existing table: add it to models.py and append a migration step (Database and Migrations). Add the table to the legacy-database test's drop list if it's new in this release.

Adding a setting

Add the field to Settings in config.py (it becomes IMPRESS_<NAME>), document it in docs/guides/configuration.md (a test checks), and add it to .env.example if operators should see it.

Changing a wire format or WebSocket command

See Protocols and Mesh → Changing a protocol.

Adding a firmware feature

  1. Put pure logic in its own file with a host test (firmware/<project>/test_host/), as ota_logic.c and student_set.c do.
  2. Put ordering and safety rules in a structural test (firmware/tests/).
  3. Build with ESP-IDF (python run_tests.py --with-idf -s idf) and check that there are no new warnings.
  4. If it changes what the device sends, update the backend schema in the same pull request.

Releasing

Every pull request adds an entry to CHANGELOG.md under ## [Unreleased], in one of Added, Changed, Removed, Fixed, Security (CONTRIBUTING §14.4). CI fails the pull request without one, unless a maintainer adds the label no-changelog.

Patch releases need no action. Every merge to an integration line that passes CI is released as <owner>/vX.Y.<n>, with the CHANGELOG entries new since the previous release on its page (docs/guides/releases.md).

A new release line (for example varun/v2.2):

  1. Create the branch varun/v2.2. Its first green push becomes varun/v2.2.0 and creates the milestone varun/v2.2. The branch name decides the line, and the release rules come from the default branch.
  2. In a pull request, set VERSION and every firmware/*/version.txt to 2.2.0. Add docs/releases/v2.2.md if the page should differ from the newest template, keeping the page structure. In CHANGELOG.md, rename ## [Unreleased] to the old line's last release and start a new one.
  3. Put the issues that must pass before the line is Stable in the milestone varun/v2.2 (label testing). Issues in varun/v2.1 keep gating the new line, because it is built on 2.1.
  4. Deploy the firmware to the rooms (Firmware Management and OTA).

Design decisions worth knowing

Decision Why
ESP-NOW flooding mesh for students no access point; robust to students moving; bounded by TTL (Protocols and Mesh)
Two boards per room (hub + gateway) one radio can't serve the mesh channel and a Wi-Fi association at once
Enrollment number as the student identity, never the device modules can be swapped or re-assigned; no physical tracking of students
One backend process, SQLite simplicity for a campus; WebSocket rooms live in memory
In-repo migration runner instead of Alembic one SQLite file that must also adopt databases with no migration history
The backend → gateway JSON contract is a committed fixture the backend and firmware tests read the same file, so they can't drift
Firmware facts come from the image file names and forms lie; the binary doesn't
Opt-in signing and TLS the key and the certificate must be each site's own
Health computed, not stored the rules can change without a migration

The full decision records are in docs/design/decisions.md.

Clone this wiki locally