Skip to content

feat: Django ASGI simulator + 2-leaf databus MQTT contract#12

Open
dotjae wants to merge 17 commits into
mainfrom
feat/django-simulator
Open

feat: Django ASGI simulator + 2-leaf databus MQTT contract#12
dotjae wants to merge 17 commits into
mainfrom
feat/django-simulator

Conversation

@dotjae

@dotjae dotjae commented Jun 9, 2026

Copy link
Copy Markdown

Summary

Replaces the legacy databus-sim with a standalone Django ASGI simulator and aligns its telemetry with the current databus MQTT contract. The whole branch is a ground-up port; the most recent work migrates the wire format to the new 2-leaf contract.

Architecture (new)

  • Django ASGI app (sim_project + simulator_app) started via uvicorn lifespan.
  • Background asyncio tasks: tick_loop (kinematics → telemetry), RunBinder.poll_loop, Scheduler.run_loop.
  • Domain layer (fleet, kinematics, control), services (databus_client, run_binder, scheduler), realtime (Channels WebSocket broadcast/consumers), and a DRF-style API.
  • Telemetry egress: paho MQTT/TCP → databus telemetry-broker; run lifecycle via httpx → databus REST.
  • Browser UI ported to Django + Channels WebSocket transport.
  • Single .env drives ports/hosts for Docker and local; legacy Celery/Redis/notebook scaffolding removed.

databus MQTT contract migration (latest commit)

The simulator is now a dumb sensor emitter — it publishes only what a real vehicle can sense:

  • Dropped the progression leaf entirely (databus no longer subscribes; stop status is computed server-side).
  • Removed the occupancy_status enum from occupancy (databus recomputes it from occupancy_percentage and discards edge values). Kept raw occupancy_percentage.
  • position unchanged; step_vehicle auto-dwell retained (observable via speed).
  • Removed now-unused occupancy_status() helper and INCOMING_AT_RADIUS_M.

Topics published: transit/vehicle/<id>/{position,occupancy}.

Test plan

  • Full suite green: pytest → 96 passed.
  • Contract guards: tests assert position/occupancy only, no progression leaf, and no occupancy_status on the wire (test_kinematics.py, test_tick_loop.py).
  • End-to-end against a running databus (requires an assigned in-progress run per docs/databus-mqtt-contract-changes.md §5).

Docs

  • AGENTS.md + README.md updated to the 2-leaf contract (topic diagrams, payload tables, JSON examples).
  • Added docs/databus-mqtt-contract-changes.md (the contract spec driving the migration).

dotjae and others added 17 commits May 26, 2026 02:48
…-simulator

Moves the full simulator stack into this repo:
- sim/: Python simulator service with harness, tests, scenarios, and schedule
- web/: nginx-served browser UI (fleet, operator, runs, schedule tabs)
- broker/mosquitto-bridge.conf: WS-to-TCP bridge config for the browser MQTT client

Adds ws-bridge, simulator, and web services to docker-compose.yml.
Simulator defaults now reference the internal app/redis service names.
Updates .gitignore to cover __pycache__, *.pyc, and .DS_Store.
Removes all original Python/Celery app files — this branch now contains
only the simulator stack (web, simulator, ws-bridge, redis).
Also removes app/celery_worker/celery_beat services from docker-compose.yml
and restores DATABUS_BASE_URL default to host.docker.internal:8000.
Simulator was timing out connecting to host.docker.internal:1883 when no
external databus broker is running. Default MQTT_HOST/PORT to the in-network
ws-bridge:1884 so the stack runs standalone; the bridge still forwards to the
host broker when one is available.

Also tracks README.md.
Centralize every port/host the stack publishes or connects to into a root
.env file (with .env.example template), so users can adapt the simulator to a
databus deployment that uses different ports without editing config by hand.

The three files with no native env-var support now render from the
environment at container startup:
- mosquitto bridge: rendered inline by the ws-bridge service command
- nginx: nginx.conf.template via the image's envsubst entrypoint
- browser: config.js.template -> window.SIM_CONFIG (MQTT_WS_PORT)

Also wire SIM_CORS_ORIGINS in http_control.py (was hardcoded :8080), derive
DATABUS_BASE_URL from DATABUS_HOST/DATABUS_HTTP_PORT in compose, gitignore
.env, and document the knobs in a new README Configuration section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove the simulator's Redis dependency so it stays fully independent of
databus's internal Redis (which need not be exposed). Run-lifecycle state is
now read over HTTP via GET /api/run/{run_id}/.

- databus_client: add get_run_state / get_run_hash (404 → None/{}, errors
  logged and treated as not-found, mirroring the old RedisClient semantics)
- run_binder: poll databus.get_run_state; a 404 is "run lost" exactly as a
  missing Redis key was (grace window + force-unbind unchanged)
- http_control: GET /run/{id} reads via databus.get_run_hash; drop redis_client
- simulator: drop the RedisClient context manager and wiring
- delete sim/redis_client.py and its tests; remove redis + fakeredis deps
- compose: drop REDIS_URL and the now-unused bundled redis service/volume
- tests: replace fakeredis fixtures with a fake databus client; add
  get_run_state/get_run_hash unit tests
- docs: update README + .env.example to describe HTTP run-state polling

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bootable skeleton: sim_project (settings/urls/asgi with lifespan handler),
simulator_app (runtime seam + empty domain/services/realtime/api packages),
single-service docker-compose, Dockerfile (single daphne worker), healthz
smoke test. No domain logic yet — Phase 2 fills the runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…me, API)

- domain: fleet (verbatim), kinematics (publish split into pure payload builder),
  control (transport-agnostic apply_control / apply_global_control)
- services: databus_client, run_binder, scheduler ported ~verbatim
- realtime: FleetConsumer (Channels), throttled broadcast replacing StatePublisher
- runtime: lifespan-started tick_loop + binder + scheduler background tasks
- api: DRF endpoints (/sim/*), control POST routes, /databus/ proxy
- serve via uvicorn (daphne 4.x omits ASGI lifespan); daphne kept test-only
- set DJANGO_SETTINGS_MODULE in asgi.py for bare uvicorn boot
- 94 tests, 85% coverage; live WS+control smoke verified without databus

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ws_client.js: one /ws/fleet/ socket exposes the old subscribe()/publish()
  interface; inbound {type} routed to old topic subscribers, publish() shimmed
  to POST /sim/control/* (replaces all MQTT.js browser usage)
- app.js rewired to ws_client; schedule payload (runs vs old entries) reconciled
- index.html served as Django template; assets via {% static %}; MQTT CDN +
  config.js dropped
- WhiteNoise serves static under uvicorn/ASGI (no nginx)
- live boot verified: index + all static assets 200, no mqtt/config refs

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README: one-service architecture, uvicorn quickstart, full env-var table,
  WS/control/REST API reference, schedule format, operational invariants,
  manual E2E checklist, troubleshooting (all verified against code)
- AGENTS.md: LLM-oriented module map, 'how do I' recipes, gotchas, invariants
- fix(compose): schedule bind-mount now read-write from canonical data path so
  PUT /sim/schedule works

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- settings.py loads .env via python-dotenv (override=False; no-op in Docker)
- derive MQTT_HOST/PORT from DATABUS_MQTT_*, DATABUS_BASE_URL from DATABUS_HOST/
  DATABUS_HTTP_PORT so one .env drives both Docker and local runs
- refresh .env.example to the single-service var set (drop stale nginx/ws-bridge
  ports); WEB_PORT moves the whole service
- add scripts/dev.sh: sources .env, binds uvicorn to $WEB_PORT
- README: .env works in both paths; ports table reorganized

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The standalone FastAPI simulator and nginx/MQTT web UI are fully replaced by the
Django app (simulator_app/). Removed:
- old sim modules: simulator, fleet, controller, state_publisher, databus_client,
  run_binder, scheduler, http_control (+ their tests)
- old sim extras: schedule.yaml, Dockerfile, CONTRACTS/HANDOFF/AGENT_RUNBOOK docs
- web/ (nginx UI) — replaced by simulator_app/static + templates

Kept under sim/: the FSM test harness (harness/, scenarios/, test_fsm_graph,
conftest, mappings.yaml, shapes.json, extract_shapes.py, pyproject/uv.lock).
Verified: Django app 94 tests pass; harness runs via 'python -m harness'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Superseded by the shipped implementation + README/AGENTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The harness was the last non-simulator code in the repo. Removed sim/ entirely
(harness/, scenarios/, mappings/shapes data, its uv project). The simulator has
its own data under simulator_app/data/ and does not depend on sim/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The harness and old simulator/web code were removed; scrub the repo-layout
tree and the out-of-scope harness note so the docs match the lean tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
databus stopped subscribing to transit/vehicle/+/progression and now
recomputes occupancy_status server-side. Make the simulator a dumb sensor
emitter that publishes only position and occupancy:

- kinematics.build_vehicle_payloads: drop the progression leaf and the
  occupancy_status enum; keep raw occupancy_percentage. Remove the now-unused
  occupancy_status() helper and INCOMING_AT_RADIUS_M constant. Position
  unchanged; step_vehicle auto-dwell retained (observable via speed).
- tests: assert position/occupancy only, no progression leaf, no
  occupancy_status on the wire.
- docs (AGENTS.md, README.md): update topic diagrams, payload tables, and
  JSON examples to the 2-leaf contract; add docs/ contract spec.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant