MSc Project — BIOE70025
This repository studies whether agents can coordinate their timing from local visual LED flashes and camera detection, without a central timing controller. The platform evolved from early drone-oriented planning to a safer physical ground-robot/HIL testbed; the research question did not change.
The authoritative final-thesis package is
step8_thesis_mainline. It is the evidence map for
supervisory review and the intended basis of the final GitHub package; historical
worktrees are not required to inspect the final narrative.
| Thesis question | Canonical thesis location |
|---|---|
| Model selection: EAPF, Kuramoto, and biological PRC/LCO comparison | step8_thesis_mainline/8a_model_selection/ |
| Physical N=2 synchronisation and physical N=3 all-to-all/directed/bidirectional topologies | step8_thesis_mainline/8b_synchronisation/ |
| Final eight-condition, 80-trial robustness campaign | step8_thesis_mainline/8c_robustness/ |
| Centralised versus decentralised action timing | step8_thesis_mainline/8d_baseline_comparison/ |
| Thesis source, figures, bibliography, and figure-generation code | step8_thesis_mainline/thesis_writing/ |
| Cross-area analysis utilities and claim-to-evidence manifest | step8_thesis_mainline/analysis/ and step8_thesis_mainline/THESIS_CANONICAL_MANIFEST.md |
The principal controller conclusion is deliberately scoped: EAPF is better suited than Kuramoto to this project's binary-flash, camera-mediated, event-based HIL setting. It is not a claim of universal superiority.
The only canonical raw videos for the baseline/action-timing result are the
ten centralised videos in
8d_baseline_comparison/centralized_original/ and the ten decentralised videos
in 8d_baseline_comparison/decentralized_original/. The former 240-fps
topology campaign remains only as SUPERSEDED / NON-CANONICAL VIDEO ANALYSIS
PROVENANCE; its video containers are not thesis evidence and were removed
under the recorded 2026-08-31 policy.
Source code, tests, configurations, analysis scripts, compact result tables,
figures, thesis source, and manifests are intended to be readable in Git. Large
raw experimental payloads are intended for a separately deposited archive with
SHA-256 verification rather than ordinary Git blobs. See
step8_thesis_mainline/GITHUB_PACKAGING_MANIFEST.csv and
step8_thesis_mainline/GITHUB_DATA_ARCHIVE_RESTORATION.md for the precise
staging plan and restoration requirements.
The pinned led_firesync biological-comparator snapshot has no local
LICENSE/COPYING/NOTICE evidence. Its upstream source is therefore retained
locally for provenance but must not be published until redistribution
permission is established; the public package retains only its provenance,
commit, citation, and SHA-256 record.
The retained scalability material is marked REQUIRES_RERUN. It can be reviewed as provenance and research history, but it must not be presented as a clean final reproducible thesis result until compatible simulator-engine provenance is established.
firefly-sync/
├── firefly_sync/ # Project-authored models and hardware interfaces
├── experiments/ # Project-authored runners and analyses
├── tests/ # Unit and integration tests
└── step8_thesis_mainline/ # Canonical final-thesis package
The following operational material documents earlier HIL development. It is not a GitHub submission runbook and should not be used to launch hardware as part of package review.
This section covers the hardware-in-the-loop testbed: a laptop browser displays a flashing leader target, a Raspberry Pi 5 camera observes it, and the Pi detects flash events in real time.
- Raspberry Pi 5 with Arducam 8MP (imx219) on CAM/DISP 0
- GPIO17 → physical LED through a current-limiting resistor
- Laptop screen displaying the leader UI
- Pi camera positioned to observe the laptop screen at short range
# From the laptop:
scp -r firefly-sync pi@dronepi.local:~/Preferred — apt packages (Raspberry Pi OS):
sudo apt install -y python3-flask python3-opencv python3-picamera2 python3-gpiozeroAlternative — pip:
pip install flask opencv-python picamera2 gpiozerocd ~/firefly-sync
python experiments/test_pi_led.py --pin 17 --cycles 5The physical LED on GPIO17 should blink 5 times.
cd firefly-sync
python experiments/run_leader_ui.pyOr open experiments/leader_ui/index.html directly in a browser.
Recommended — top-percentile mode with auto-ROI:
cd ~/firefly-sync
PYTHONPATH=. python3 experiments/stream_pi_camera_detection.py \
--host 0.0.0.0 --port 5000 \
--detection-mode top_percentile --percentile 99 \
--threshold-on 180 --threshold-off 120 --min-interval 0.2 \
--auto-roi --auto-roi-duration 3Manual ROI (if you know the target's pixel position):
PYTHONPATH=. python3 experiments/stream_pi_camera_detection.py \
--host 0.0.0.0 --port 5000 \
--detection-mode top_percentile \
--roi 200 120 240 240 --percentile 99 \
--threshold-on 180 --threshold-off 120 --min-interval 0.2Detection modes:
top_percentile— robust for small targets on dark backgrounds (default)mean— full-frame or ROI mean brightnessbright_blob— largest bright connected component
- In the leader UI, locate the Pi Camera Monitor panel (right side).
- Ensure the URL field shows
http://dronepi.local:5000(edit as needed, e.g.http://192.168.1.127:5000). - Press Connect.
- The MJPEG video feed should appear with detection overlay.
- Press ◎ Auto Locate Flash Region to automatically find the flashing target (keep the leader flashing and the camera still for 3 seconds).
- Status readouts update in real time.
- Camera stream visible in the right panel with ROI rectangle
- ROI source shows
autoormanualafter auto-localisation - Bright (used) changes from dark to bright when the leader flashes ON
- Top %ile brightness is high during ON phases even with a small target
- State switches between OFF and ON, synchronized with the leader flash
- Rising edge count increases by 1 per ON transition
- Estimated frequency approaches the leader UI frequency (e.g. ~1 Hz)
- CSV log saved under
experiments/logs/pi_camera_stream_YYYYMMDD_HHMMSS.csv
timestamp_s, elapsed_time_s, frame_index, detection_mode,
brightness_used, full_frame_mean, top_percentile_brightness,
brightness_mean, state, event_type, rising_edge_count,
estimated_frequency_hz, roi, roi_source, roi_confidence,
percentile, blob_found, blob_area_px, blob_bbox,
threshold_on, threshold_off
One row is written per processed frame. The event_type column is
empty for normal frames and "leader_rising_edge" when a valid rising
edge is detected.
If the camera stream is visible but rising_edge_count stays at 0:
- Fix the camera physically — point it directly at the laptop screen.
- Set leader UI to high contrast — ON brightness 255, OFF brightness 0, background brightness 0 (the default).
- Use a large target size initially — 150–250 px square, centred.
- Start leader flashing at 1 Hz with 50% duty cycle.
- Run the Pi server with local_contrast + adaptive mode (recommended):
PYTHONPATH=. python3 experiments/stream_pi_camera_detection.py \ --host 0.0.0.0 --port 5000 \ --detection-mode local_contrast --auto-roi --auto-roi-duration 3 \ --window-s 5 --norm-on-threshold 0.65 --norm-off-threshold 0.35 \ --min-interval 0.2 - Optional — fix camera exposure to prevent auto-exposure from fighting
the flashing signal:
PYTHONPATH=. python3 experiments/stream_pi_camera_detection.py \ --host 0.0.0.0 --port 5000 \ --detection-mode local_contrast --auto-roi --auto-roi-duration 3 \ --manual-camera --exposure-us 8000 --analogue-gain 1.0 \ --awb-enable false --target-fps 30 - Click "Auto Locate Flash Region" in the leader UI camera panel.
- Check the signal_norm plot in the camera panel:
- If the trace is a clear square-ish wave swinging 0→1, detection is working.
- If the trace is flat → ROI/camera positioning is wrong, or the target is not flashing with enough contrast, or camera auto-exposure is cancelling out the flash.
- If the trace is noisy → ambient light or screen reflections are interfering; increase target size or move camera closer.
- Check adaptive thresholds in the overlay/readouts:
adaptive_lowshould be near the dark-phaselocal_contrastvalue.adaptive_highshould be near the bright-phaselocal_contrastvalue.adaptive_amplitudeshould be comfortably abovemin_amplitude(default 10).
- If state is stuck ON, lower
--norm-off-threshold(e.g. 0.25) or check that the signal drops belowadaptive_lowduring the OFF phase. - If state is stuck OFF, raise
--norm-on-threshold(e.g. 0.55) or check thatadaptive_amplitudeexceeds--min-amplitude.
The signal_norm plot in the leader UI camera panel shows the last ~10 seconds of normalised detection signal:
- Green dashed line at ~0.65 — ON threshold. When the trace crosses above this line from below, a rising edge is detected.
- Red dashed line at ~0.35 — OFF threshold. When the trace drops below this line from above, the state switches to OFF.
- Cyan trace — the normalised signal (0 = dark, 1 = bright).
A working system shows a square-ish waveform swinging between ~0 and ~1, crossing both thresholds once per flash cycle.
| Symptom | Likely Cause | Fix |
|---|---|---|
| State stuck OFF, trace swings 0→1 | norm_on_threshold too high |
Lower --norm-on-threshold to 0.55 |
| State stuck ON, trace swings 1→0 | norm_off_threshold too low |
Raise --norm-off-threshold to 0.45 |
| Trace flat near 0 | ROI wrong / no flash in frame / camera dark | Reposition camera, enlarge target, check leader UI is flashing |
| Trace flat near 0.5 | Camera auto-exposure cancelling flash | Use --manual-camera with fixed exposure |
| Trace periodic but amplitude small | Target too small or dim | Enlarge target, increase ON brightness |
signal_quality stays 0 |
min_amplitude too high |
Lower --min-amplitude or check signal_amplitude readout |
periodicity_confidence low |
Signal noisy or irregular | Improve lighting, reduce screen reflections |
signal_frequency_hz near 0 but edges fire |
Not enough autocorrelation data | Wait 5+ seconds for history to build |
- Laptop and Pi clocks are not synchronised — compare detection
timestamps with Pi-side
perf_counter, not browserperformance.now(). - The browser MJPEG stream is for debugging only; future synchronisation must use Pi-side detection timestamps.
- MJPEG streaming introduces ~100–200 ms latency; detection is real-time on the Pi but display on the laptop lags slightly.
- Fullscreen mode hides both the control panel and the camera panel. Use windowed mode to monitor detection while the leader runs.
Built as part of the MSc in Bioengineering at Imperial College London.