Skip to content

feat(mock): simulate the optional hardware phone apps react to - #299

Merged
jewbetcha merged 4 commits into
open-flight:mainfrom
btripp:feat/mock-hardware-sim
Oct 18, 2026
Merged

jewbetcha merged 4 commits into
open-flight:mainfrom
btripp:feat/mock-hardware-sim

Conversation

@btripp

@btripp btripp commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Reviewing this PR

One commit, 11 files. Production code: about 206 added and 33 removed lines in src/ (server.py +124/−32, new power/providers/mock.py +74, power/factory.py +6, power/providers/__init__.py +2/−1). Tests: two new files, 404 lines. Docs: +48/−2.

Suggested order:

  1. src/openflight/power/providers/mock.py and power/factory.py: the simulated battery and its registration as --battery mock.
  2. src/openflight/server.py, MockLaunchMonitor: processing states, fail, enrichment_ms, enrich().
  3. src/openflight/server.py, wiring: handle_simulate_shot(data), _mock_enrichment_enabled(), the mock early return in _enrich_shot_from_optional_hardware, _has_slow_shot_enrichment, start_monitor(mock_enrichment_ms=...), and the --mock-enrichment-ms flag with its validation.
  4. tests/test_mock_hardware.py, tests/test_mock_power.py.
  5. Docs.

The PR does not depend on #297 (network shot stream) or the phone tilt calibration PR, and it does not touch either feature.

What does this PR do?

Mock mode can now produce every event a phone app reacts to, so a Pi with no radar, UPS or camera can exercise the app end to end:

  • Processing states. MockLaunchMonitor reports shot_processing capturing and then calculating to the processing callback, the same way the rolling-buffer radar does. start_monitor now passes processing_callback=on_shot_processing to the mock monitor.
  • Failed captures. The simulate_shot Socket.IO event accepts {"fail": true}. The mock then reports capturing, calculating and failed, and records no shot. MockSwingSpeedMonitor works as before.
  • Provisional then final shots. With --mock-enrichment-ms MS, mock shots go through the real slow-enrichment pipeline. Each shot is recorded without horizontal launch, club path and spin axis and is published as provisional (enrichment: pending). After MS milliseconds, MockLaunchMonitor.enrich() fills in those fields and the shot is finalized with the same event_id. If MS is longer than the 20 s deadline, the shot is finalized as skipped/deadline, as on real hardware. The flag is off by default, so plain --mock behaves exactly as before. The server rejects the flag when it is negative, or when it is used without --mock in launch-monitor mode (including with --mock-swing-speed).
  • Battery. --battery mock adds a deterministic simulated battery. Each 5 s poll moves it one step: from 100% on battery down through low and critical, then plugged_in charging back to 100%. A full cycle takes about 100 s. It never falls back to the Linux sysfs battery, so a laptop's own battery cannot replace the simulation.

Docs: a "Simulating hardware on a Pi without it" section in docs/ios-ble.md, the mock provider in docs/using/battery.md, the new flag in docs/using/running.md and docs/reference/cli.md, and a changelog entry under Unreleased → Added.

Why was this required?

This is follow-up 4 from #285. Phone apps handle shot_processing, provisional and final shots (upserted by event_id), skipped enrichment and power_status. Until now, the only way to produce those events was a Pi with an OPS243 rolling buffer, IWR6843 or camera, and a UPS. App developers without that hardware could not test those code paths. This PR is independent of #297 and of the calibration PR.

Automated tests

  • tests/test_mock_hardware.py (new):
    • Monitor: shots report capturing then calculating. fail=True reports failed and records nothing. Shots still work with no processing callback, and when the callback raises. Enrichment is off by default and leaves the direction fields populated. With enrichment on, the direction fields stay empty until enrich() runs. A negative delay is rejected.
    • Server: _has_slow_shot_enrichment is false for mock shots by default and true with enrichment on. handle_simulate_shot({"fail": True}) fails a shot and handle_simulate_shot() records one.
    • Phone over the BLE loopback harness (tests/ble_harness.py): processing states, then a provisional shot, then a final shot with one event_id. Past the deadline (shortened to 0.2 s in the test), the shot finalizes as skipped/deadline. A failed capture shows failed and sends no shot.
    • CLI: --mock-enrichment-ms without --mock, with --mock-swing-speed, or with a negative value exits with code 2.
  • tests/test_mock_power.py (new): the cycle starts full on battery, reaches all four power states, discharges and then charges monotonically, repeats exactly, and keeps percent and voltage plausible. The factory picks the mock even when a real sysfs battery exists. PowerMonitor(provider="mock") publishes statuses. --battery mock parses. Non-positive steps are rejected.
  • The existing tests that build MockLaunchMonitor() and call simulate_shot (test_server.py, test_ble_loopback.py, test_phone_transport.py) pass unchanged.

Manual (human) testing

Run on macOS (no Pi, no Bluetooth):

  1. Started uv run openflight-server --mock --mock-enrichment-ms 800 --battery mock --host 127.0.0.1 --web-port 8767, then drove it with a python-socketio client:

    • simulate_shot: shot_processing capturing (t=1.01 s), then calculating (1.17 s). A shot event at 1.33 s had pending set and launch_angle_horizontal, club_path_deg and spin_axis_deg all null. A shot_update arrived about 0.8 s later for the same shot number, with those fields filled in (e.g. −4.4 / −2.3 / −6.3).
    • simulate_shot with {"fail": true}: capturing, calculating, failed, and no shot event.
    • power_status with provider: "mock", state: "on_battery", stepping 92 → 84 → 76 → 68% every 5 s.
  2. openflight-server --mock-enrichment-ms 5 exits 2 with --mock-enrichment-ms requires --mock (launch monitor mode). --mock --mock-enrichment-ms -1 exits 2 with must not be negative. --mock-swing-speed --mock-enrichment-ms 5 is rejected the same way as the first case.

  3. scripts/start-kiosk.sh --mock --ble --battery mock --mock-enrichment-ms 1500 --dry-run forwards every flag unchanged to openflight-server.

  4. Phone payloads, after merging current main. Bluetooth can't run on macOS (bless is Linux-only), so a small launcher replaced BleShotPublisher with a subclass whose start() doesn't open the radio. The subclass still runs the real publish_shot/publish_event encoding and records the exact bytes a phone would receive. The rest is the stock server.main().

    • --mock --battery mock --mock-enrichment-ms 1500: a provisional shot (final:false, enrichment.status:"pending", horizontal launch / club path / spin axis null), then a final shot with the same event_id 1.51 s later (enrichment.status:"complete", fields filled in). Preceded by shot_processing capturing → calculating.
    • simulate_shot {"fail": true}: capturing → calculating → failed, with no shot.
    • Mock battery, polled every 5 s: 100 → 92 … 28% on_battery, 20 and 12% low, 5% critical, then 17 → 29 … 89 → 100% plugged_in with external_power:true, then repeats (about 105 s per cycle).
    • --mock-enrichment-ms 25000: the final shot arrives 20.0 s after the provisional one with enrichment:{"status":"skipped","reason":"deadline"}. The log shows optional enrichment exceeded deadline; finalizing OPS-only, then Ignoring late enrichment for finalized shot #1 when the mock result arrives at 25 s.
    • Plain --mock with no new flags: one complete shot per swing, with no shot_update and no warnings in the log. The only change is the capturing/calculating steps, which add about 0.3 s before the shot.
    • A real --ble on macOS logs Bluetooth unavailable; shot recording will continue without BLE, and shots keep working.

Not yet tested: the phone app over real Bluetooth on a Pi. No Pi was set up for this PR. I'll add the results here once one is.

Checklist

  • Single feature/fix: mock-mode simulation of optional hardware only
  • Automated tests included: tests/test_mock_hardware.py, tests/test_mock_power.py
  • Manual testing described: mock-mode tests on macOS above; phone app over Bluetooth on a Pi still to do
  • Python tests pass (uv run pytest -q: 1831 passed, 10 skipped, after merging current main)
  • Pylint passes (uv run pylint src/openflight/ --fail-under=9: 9.60/10, no messages on added lines)
  • Ruff passes (uv run ruff check src/openflight/, ruff format --check on touched files)
  • UI builds (not applicable: no ui/ changes)
  • UI lint passes (not applicable: no ui/ changes)
  • Updated docs or CHANGELOG if needed
  • No unrelated changes mixed in

🤖 Generated with Claude Code

btripp and others added 2 commits October 9, 2026 11:58
A Pi with no radar, UPS or camera could not produce shot_processing,
provisional-then-final shots or power_status, so phone apps could not
be tested against them. Mock mode now simulates each one through the
real server paths:

- MockLaunchMonitor reports capturing then calculating to the
  processing callback like the rolling-buffer radar, and
  simulate_shot(fail=True) (Socket.IO {"fail": true}) reports failed
  without recording a shot.
- --mock-enrichment-ms MS makes mock shots take the slow-enrichment
  path: recorded without horizontal launch, club path and spin axis,
  published provisional, then finalized once MockLaunchMonitor.enrich
  supplies them after MS. Past the 20 s deadline they finalize as
  skipped, as real hardware does. Off by default, so plain --mock is
  unchanged. The flag is rejected when negative or without --mock
  (launch monitor mode).
- --battery mock adds a deterministic simulated battery that cycles
  from full through low and critical, then charges back to full. It
  never falls back to a real Linux battery, so a laptop's own battery
  cannot stand in for it.

Docs: a "Simulating hardware on a Pi without it" section in the phone
BLE guide, the mock provider in the battery guide, the new flag in the
running guide and CLI reference, and a changelog entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@btripp
btripp marked this pull request as ready for review October 12, 2026 17:56
@jewbetcha
jewbetcha merged commit b0f2b0f into open-flight:main Oct 18, 2026
14 checks passed
HarjotDhanota added a commit to HarjotDhanota/openflight that referenced this pull request Oct 20, 2026
…#305, open-flight#307)

Conflicts:
- server.py: upstream's mock-enrichment shortcut runs first, then our trace marks.
- sim/resolver.py: both sides tagged estimated launch angles; ours keeps
  _resolve_carry, which already replaced the launch-angle carry badge (E4, C8).
- tests/test_sim_resolver.py: both test sets kept; upstream's measured-VLA test
  now expects the estimated carry badge this fork gives an OPS-only carry.
- tests/test_mock_hardware.py: the deadline publishes OPS-only, still pending,
  and the late enrichment finalizes the same shot (D29), not 'skipped'.
- README.md: both contributor bullets kept.
- AGENTS.md: the bench's master plan is linked by repository, not a local doc path.
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.

2 participants