From 7a82fa5f98b7d445f5062e2cbcd50bd9ecbe0418 Mon Sep 17 00:00:00 2001 From: Jemin Kachhadiya Date: Wed, 26 Aug 2026 12:10:38 -0500 Subject: [PATCH 1/3] project re-structured and re-based all docs --- .github/workflows/base-station.yml | 12 +- README.md | 50 +- {docs => firmware/docs}/DISPENSE_CYCLE.md | 3 +- {docs => firmware/docs}/HARDCODED_VALUES.md | 4 +- {docs => firmware/docs}/WIRING.md | 0 {examples => firmware/examples}/Node/Node.ino | 0 .../DispenseTest/DispenseTest.ino | 0 .../ActuatorCalTest/ActuatorCalTest.ino | 0 .../CanBusTest/CanBusTest.ino | 0 .../HardwareExamples/LEDTest/LEDTest.ino | 0 .../MousePresenceTest/MousePresenceTest.ino | 0 .../PhotogateTest/PhotogateTest.ino | 0 .../StepperMotorTest/StepperMotorTest.ino | 0 .../library.properties | 0 {src => firmware/src}/VFM.cpp | 0 {src => firmware/src}/VFM.h | 0 {src => firmware/src}/hardware/VFMPins.h | 0 {src => firmware/src}/services/CanService.cpp | 0 {src => firmware/src}/services/CanService.h | 0 .../src}/services/DispenserService.cpp | 0 .../src}/services/DispenserService.h | 0 {src => firmware/src}/services/LedService.h | 0 .../src}/services/NodeIdentity.cpp | 0 {src => firmware/src}/services/NodeIdentity.h | 0 .../src}/services/PresenceService.cpp | 0 .../src}/services/PresenceService.h | 0 {src => firmware/src}/services/ServiceTypes.h | 0 {tools => packages}/dev_gui/README.md | 139 ++-- .../dev_gui/base_station/__init__.py | 0 .../dev_gui/base_station/app.py | 0 .../dev_gui/base_station/can_manager.py | 0 .../dev_gui/base_station/dev_settings.py | 0 .../dev_gui/base_station/discovery_manager.py | 0 .../dev_gui/base_station/experiment/README.md | 725 ++++++++++++++++++ .../base_station/experiment/__init__.py | 8 +- .../base_station/experiment/context.py | 61 +- .../dev_gui/base_station/experiment/events.py | 4 +- .../base_station/experiment/gui_controller.py | 8 +- .../dev_gui/base_station/experiment/kit.py | 0 .../dev_gui/base_station/experiment/runner.py | 58 +- .../dev_gui/base_station/experiment/schema.py | 2 +- .../dev_gui/base_station/experiment/script.py | 0 .../experiment/templates/__init__.py | 0 .../experiment/templates/fixed_and_random.py | 5 +- .../experiment/templates/free_feeding.py | 1 - .../templates/probability_delivery.py | 3 +- .../experiment/templates/two_armed_bandit.py | 1 - .../dev_gui/base_station/io_manager.py | 0 .../dev_gui/base_station/log_manager.py | 0 .../dev_gui/base_station/mac_id_registry.py | 0 .../dev_gui/base_station/node_registry.py | 0 .../dev_gui/base_station/pellet_ledger.py | 0 .../dev_gui/base_station/protocol.py | 0 .../dev_gui/base_station/storage.py | 0 .../dev_gui/deploy/80-can.network | 0 {tools => packages}/dev_gui/deploy/README.md | 8 +- .../dev_gui/deploy/boot-config-can.append.txt | 0 .../dev_gui/deploy/setup-can.sh | 4 +- .../docs}/BASE_STATION_HARDCODED_VALUES.md | 14 +- {docs => packages/dev_gui/docs}/GUI.png | Bin packages/dev_gui/examples/templates/README.md | 41 + .../examples/templates/alternation.json | 37 + .../dev_gui/examples/templates/alternation.py | 70 ++ .../dev_gui/examples/templates/bnc_paced.json | 46 ++ .../dev_gui/examples/templates/bnc_paced.py | 73 ++ .../dev_gui/experiments/fixed_and_random.json | 0 .../dev_gui/experiments/free_feeding.json | 0 .../experiments/probability_delivery.json | 0 .../dev_gui/experiments/two_armed_bandit.json | 0 {tools => packages}/dev_gui/node_simulator.py | 0 {tools => packages}/dev_gui/pytest.ini | 0 {tools => packages}/dev_gui/requirements.txt | 2 +- {tools => packages}/dev_gui/run.py | 0 {tools => packages}/dev_gui/run_report.py | 0 {tools => packages}/dev_gui/setup_storage.sh | 0 {tools => packages}/dev_gui/tests/__init__.py | 0 {tools => packages}/dev_gui/tests/test_app.py | 0 .../dev_gui/tests/test_app_pellet_counting.py | 0 .../tests/test_behavioral_log_names.py | 0 .../dev_gui/tests/test_discovery_manager.py | 0 .../dev_gui/tests/test_experiment.py | 4 +- .../dev_gui/tests/test_experiment_schema.py | 22 + {tools => packages}/dev_gui/tests/test_hat.py | 4 +- .../dev_gui/tests/test_log_manager.py | 0 .../dev_gui/tests/test_mac_id_registry.py | 0 .../dev_gui/tests/test_node_registry.py | 0 .../dev_gui/tests/test_pellet_ledger.py | 0 .../dev_gui/tests/test_protocol.py | 0 .../dev_gui/tests/test_run_report_shim.py | 0 .../dev_gui/tests/test_schedule.py | 0 .../dev_gui/tests/test_script.py | 0 .../dev_gui/tests/test_two_armed_bandit.py | 0 packages/sfm-analysis/README.md | 31 +- packages/sfm-analysis/docs/ANALYSIS_GUIDE.md | 107 ++- .../examples/analysis/exp_events_by_name.py | 39 + .../examples/report_design/README.md | 36 + .../report_design/analyses/alternation.py | 29 + .../report_design/designs/alternation.json | 23 + .../report_design/sections/alternation.py | 56 ++ .../sfm-analysis/src/sfm_analysis/__init__.py | 4 +- .../sfm-analysis/src/sfm_analysis/logs.py | 17 +- .../sfm-analysis/src/sfm_analysis/protocol.py | 4 +- .../src/sfm_analysis/report/loader.py | 4 +- .../sfm-analysis/tests/report_fixtures.py | 2 +- packages/sfm-analysis/tests/test_examples.py | 8 + .../dev_gui/base_station/experiment/README.md | 629 --------------- tools/dev_gui/example_experiment.py | 53 -- tools/dev_gui/run_experiment.py | 281 ------- tools/dev_gui/tests/test_run.py | 91 --- 109 files changed, 1507 insertions(+), 1316 deletions(-) rename {docs => firmware/docs}/DISPENSE_CYCLE.md (98%) rename {docs => firmware/docs}/HARDCODED_VALUES.md (99%) rename {docs => firmware/docs}/WIRING.md (100%) rename {examples => firmware/examples}/Node/Node.ino (100%) rename {examples => firmware/examples}/Troubleshooting/DispenseTest/DispenseTest.ino (100%) rename {examples => firmware/examples}/Troubleshooting/HardwareExamples/ActuatorCalTest/ActuatorCalTest.ino (100%) rename {examples => firmware/examples}/Troubleshooting/HardwareExamples/CanBusTest/CanBusTest.ino (100%) rename {examples => firmware/examples}/Troubleshooting/HardwareExamples/LEDTest/LEDTest.ino (100%) rename {examples => firmware/examples}/Troubleshooting/HardwareExamples/MousePresenceTest/MousePresenceTest.ino (100%) rename {examples => firmware/examples}/Troubleshooting/HardwareExamples/PhotogateTest/PhotogateTest.ino (100%) rename {examples => firmware/examples}/Troubleshooting/HardwareExamples/StepperMotorTest/StepperMotorTest.ino (100%) rename library.properties => firmware/library.properties (100%) rename {src => firmware/src}/VFM.cpp (100%) rename {src => firmware/src}/VFM.h (100%) rename {src => firmware/src}/hardware/VFMPins.h (100%) rename {src => firmware/src}/services/CanService.cpp (100%) rename {src => firmware/src}/services/CanService.h (100%) rename {src => firmware/src}/services/DispenserService.cpp (100%) rename {src => firmware/src}/services/DispenserService.h (100%) rename {src => firmware/src}/services/LedService.h (100%) rename {src => firmware/src}/services/NodeIdentity.cpp (100%) rename {src => firmware/src}/services/NodeIdentity.h (100%) rename {src => firmware/src}/services/PresenceService.cpp (100%) rename {src => firmware/src}/services/PresenceService.h (100%) rename {src => firmware/src}/services/ServiceTypes.h (100%) rename {tools => packages}/dev_gui/README.md (76%) rename {tools => packages}/dev_gui/base_station/__init__.py (100%) rename {tools => packages}/dev_gui/base_station/app.py (100%) rename {tools => packages}/dev_gui/base_station/can_manager.py (100%) rename {tools => packages}/dev_gui/base_station/dev_settings.py (100%) rename {tools => packages}/dev_gui/base_station/discovery_manager.py (100%) create mode 100644 packages/dev_gui/base_station/experiment/README.md rename {tools => packages}/dev_gui/base_station/experiment/__init__.py (65%) rename {tools => packages}/dev_gui/base_station/experiment/context.py (94%) rename {tools => packages}/dev_gui/base_station/experiment/events.py (99%) rename {tools => packages}/dev_gui/base_station/experiment/gui_controller.py (95%) rename {tools => packages}/dev_gui/base_station/experiment/kit.py (100%) rename {tools => packages}/dev_gui/base_station/experiment/runner.py (91%) rename {tools => packages}/dev_gui/base_station/experiment/schema.py (99%) rename {tools => packages}/dev_gui/base_station/experiment/script.py (100%) rename {tools => packages}/dev_gui/base_station/experiment/templates/__init__.py (100%) rename {tools => packages}/dev_gui/base_station/experiment/templates/fixed_and_random.py (97%) rename {tools => packages}/dev_gui/base_station/experiment/templates/free_feeding.py (99%) rename {tools => packages}/dev_gui/base_station/experiment/templates/probability_delivery.py (98%) rename {tools => packages}/dev_gui/base_station/experiment/templates/two_armed_bandit.py (99%) rename {tools => packages}/dev_gui/base_station/io_manager.py (100%) rename {tools => packages}/dev_gui/base_station/log_manager.py (100%) rename {tools => packages}/dev_gui/base_station/mac_id_registry.py (100%) rename {tools => packages}/dev_gui/base_station/node_registry.py (100%) rename {tools => packages}/dev_gui/base_station/pellet_ledger.py (100%) rename {tools => packages}/dev_gui/base_station/protocol.py (100%) rename {tools => packages}/dev_gui/base_station/storage.py (100%) rename {tools => packages}/dev_gui/deploy/80-can.network (100%) rename {tools => packages}/dev_gui/deploy/README.md (94%) rename {tools => packages}/dev_gui/deploy/boot-config-can.append.txt (100%) rename {tools => packages}/dev_gui/deploy/setup-can.sh (95%) rename {docs => packages/dev_gui/docs}/BASE_STATION_HARDCODED_VALUES.md (95%) rename {docs => packages/dev_gui/docs}/GUI.png (100%) create mode 100644 packages/dev_gui/examples/templates/README.md create mode 100644 packages/dev_gui/examples/templates/alternation.json create mode 100644 packages/dev_gui/examples/templates/alternation.py create mode 100644 packages/dev_gui/examples/templates/bnc_paced.json create mode 100644 packages/dev_gui/examples/templates/bnc_paced.py rename {tools => packages}/dev_gui/experiments/fixed_and_random.json (100%) rename {tools => packages}/dev_gui/experiments/free_feeding.json (100%) rename {tools => packages}/dev_gui/experiments/probability_delivery.json (100%) rename {tools => packages}/dev_gui/experiments/two_armed_bandit.json (100%) rename {tools => packages}/dev_gui/node_simulator.py (100%) rename {tools => packages}/dev_gui/pytest.ini (100%) rename {tools => packages}/dev_gui/requirements.txt (92%) rename {tools => packages}/dev_gui/run.py (100%) rename {tools => packages}/dev_gui/run_report.py (100%) rename {tools => packages}/dev_gui/setup_storage.sh (100%) rename {tools => packages}/dev_gui/tests/__init__.py (100%) rename {tools => packages}/dev_gui/tests/test_app.py (100%) rename {tools => packages}/dev_gui/tests/test_app_pellet_counting.py (100%) rename {tools => packages}/dev_gui/tests/test_behavioral_log_names.py (100%) rename {tools => packages}/dev_gui/tests/test_discovery_manager.py (100%) rename {tools => packages}/dev_gui/tests/test_experiment.py (99%) rename {tools => packages}/dev_gui/tests/test_experiment_schema.py (85%) rename {tools => packages}/dev_gui/tests/test_hat.py (99%) rename {tools => packages}/dev_gui/tests/test_log_manager.py (100%) rename {tools => packages}/dev_gui/tests/test_mac_id_registry.py (100%) rename {tools => packages}/dev_gui/tests/test_node_registry.py (100%) rename {tools => packages}/dev_gui/tests/test_pellet_ledger.py (100%) rename {tools => packages}/dev_gui/tests/test_protocol.py (100%) rename {tools => packages}/dev_gui/tests/test_run_report_shim.py (100%) rename {tools => packages}/dev_gui/tests/test_schedule.py (100%) rename {tools => packages}/dev_gui/tests/test_script.py (100%) rename {tools => packages}/dev_gui/tests/test_two_armed_bandit.py (100%) create mode 100644 packages/sfm-analysis/examples/analysis/exp_events_by_name.py create mode 100644 packages/sfm-analysis/examples/report_design/README.md create mode 100644 packages/sfm-analysis/examples/report_design/analyses/alternation.py create mode 100644 packages/sfm-analysis/examples/report_design/designs/alternation.json create mode 100644 packages/sfm-analysis/examples/report_design/sections/alternation.py delete mode 100644 tools/dev_gui/base_station/experiment/README.md delete mode 100644 tools/dev_gui/example_experiment.py delete mode 100644 tools/dev_gui/run_experiment.py delete mode 100644 tools/dev_gui/tests/test_run.py diff --git a/.github/workflows/base-station.yml b/.github/workflows/base-station.yml index 15589be..82d937e 100644 --- a/.github/workflows/base-station.yml +++ b/.github/workflows/base-station.yml @@ -4,12 +4,12 @@ on: push: branches: [main] paths: - - "tools/dev_gui/**" + - "packages/dev_gui/**" - "packages/sfm-analysis/**" - ".github/workflows/base-station.yml" pull_request: paths: - - "tools/dev_gui/**" + - "packages/dev_gui/**" - "packages/sfm-analysis/**" - ".github/workflows/base-station.yml" workflow_dispatch: @@ -26,7 +26,7 @@ jobs: # everything else is importable and testable on a plain Linux runner. # The remaining ~26 tests (test_app.py, test_app_pellet_counting.py, # test_schedule.py, test_hat.py) are Pi-only and run manually — see - # tools/dev_gui/README.md. + # packages/dev_gui/README.md. runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -35,9 +35,9 @@ jobs: python-version: "3.11" - run: pip install -e packages/sfm-analysis pytest python-can - name: syntax-check everything, including the GUI modules we cannot import - run: python -m compileall -q tools/dev_gui/base_station tools/dev_gui/*.py + run: python -m compileall -q packages/dev_gui/base_station packages/dev_gui/*.py - name: run the hardware-free base-station tests - working-directory: tools/dev_gui + working-directory: packages/dev_gui run: | pytest -q \ tests/test_protocol.py tests/test_log_manager.py \ @@ -45,4 +45,4 @@ jobs: tests/test_pellet_ledger.py tests/test_mac_id_registry.py \ tests/test_discovery_manager.py tests/test_experiment.py \ tests/test_experiment_schema.py tests/test_script.py \ - tests/test_two_armed_bandit.py tests/test_run.py + tests/test_two_armed_bandit.py diff --git a/README.md b/README.md index 617ea49..3afc8d1 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,23 @@ # VFM +Spatial Foraging Platform: node firmware, base-station developer GUI, and +session analysis. Each application lives in its own directory. + ## Launch the GUI ``` -cd Project/VFM/tools/dev_gui +cd Project/VFM/packages/dev_gui python run.py ``` ## Generate a behavior report ``` -cd Project/VFM/tools/dev_gui +cd Project/VFM/packages/dev_gui python run_report.py ``` -With no arguments this opens an interactive session picker. For list/combine, date filters, designs, and other use cases, see [Behavior reports](tools/dev_gui/README.md#behavior-reports-session-csv--printable-html) in the GUI README. +With no arguments this opens an interactive session picker. For list/combine, date filters, designs, and other use cases, see [Behavior reports](packages/dev_gui/README.md#behavior-reports-session-csv--printable-html) in the GUI README. Report generation itself is cross-platform — it doesn't need a Raspberry Pi or any base-station hardware library. It ships as its own package, [`sfm-analysis`](packages/sfm-analysis/), installable with `pip install sfm-analysis` on Windows, macOS, or Linux; `run_report.py` above is a thin wrapper around its `sfm-report` CLI. See [packages/sfm-analysis/README.md](packages/sfm-analysis/README.md). @@ -22,33 +25,38 @@ Report generation itself is cross-platform — it doesn't need a Raspberry Pi or VFM is a firmware library for the Spatial Foraging Platform node. It provides non-blocking, service-oriented stepper-driven pellet dispensing, CAN bus communication with a base-station command/event/heartbeat protocol. +The Arduino library is the `firmware/` folder (not the repo root). Copy or symlink `firmware/` into `Arduino/libraries/VFM`, or zip that folder and add it via *Sketch → Include Library → Add .ZIP Library…*. + ## Documentation | Doc | What it's for | |-----|---------------| -| [tools/dev_gui/README.md](tools/dev_gui/README.md) | Base-station GUI: install, run, CAN bring-up, experiments, session logs, and behavior reports | -| [tools/dev_gui/deploy/README.md](tools/dev_gui/deploy/README.md) | One-time Raspberry Pi CAN HAT setup (`can0`) before running against real hardware | -| [tools/dev_gui/base_station/experiment/README.md](tools/dev_gui/base_station/experiment/README.md) | How to author custom experiment templates (Python API + JSON params) | +| [packages/dev_gui/README.md](packages/dev_gui/README.md) | Base-station GUI: install, run, CAN bring-up, experiments, session logs, and behavior reports | +| [packages/dev_gui/deploy/README.md](packages/dev_gui/deploy/README.md) | One-time Raspberry Pi CAN HAT setup (`can0`) before running against real hardware | +| [packages/dev_gui/base_station/experiment/README.md](packages/dev_gui/base_station/experiment/README.md) | How to author custom experiment templates (Python API + JSON params) | | [packages/sfm-analysis/README.md](packages/sfm-analysis/README.md) | Cross-platform session analysis and report generation — install, CLI, report design/section architecture, Python API | | [packages/sfm-analysis/docs/ANALYSIS_GUIDE.md](packages/sfm-analysis/docs/ANALYSIS_GUIDE.md) | Analysis reference: every log column, event name, derived metric field, and tidy-table column, plus a worked example | -| [docs/WIRING.md](docs/WIRING.md) | Firmware pin names → physical jobs (motors, sensors, CAN, LEDs, discovery) | -| [docs/DISPENSE_CYCLE.md](docs/DISPENSE_CYCLE.md) | How a node loads, raises, and reports pellet take / faults during a dispense | -| [docs/HARDCODED_VALUES.md](docs/HARDCODED_VALUES.md) | Tunable firmware constants checklist (timings, speeds, thresholds) | -| [docs/BASE_STATION_HARDCODED_VALUES.md](docs/BASE_STATION_HARDCODED_VALUES.md) | Base-station timings and defaults (GUI, experiments, reports) | +| [firmware/docs/WIRING.md](firmware/docs/WIRING.md) | Firmware pin names → physical jobs (motors, sensors, CAN, LEDs, discovery) | +| [firmware/docs/DISPENSE_CYCLE.md](firmware/docs/DISPENSE_CYCLE.md) | How a node loads, raises, and reports pellet take / faults during a dispense | +| [firmware/docs/HARDCODED_VALUES.md](firmware/docs/HARDCODED_VALUES.md) | Tunable firmware constants checklist (timings, speeds, thresholds) | +| [packages/dev_gui/docs/BASE_STATION_HARDCODED_VALUES.md](packages/dev_gui/docs/BASE_STATION_HARDCODED_VALUES.md) | Base-station timings and defaults (GUI, experiments, reports) | ## Project structure ``` VFM/ -├── src/ # Arduino library (ESP32-S3 node firmware) -│ ├── VFM.h / VFM.cpp # Library entry point -│ ├── hardware/ # Pin definitions -│ └── services/ # CAN bus, dispenser, node identity, presence, LED services -├── examples/ # Arduino sketches -│ ├── Node/ # Main node firmware sketch -│ └── Troubleshooting/ # Hardware bring-up / diagnostic sketches -├── docs/ # Wiring, dispense cycle, and hardcoded-value reference docs -├── tools/dev_gui/ # Python base-station GUI + CAN tooling (see tools/dev_gui/README.md) -├── packages/sfm-analysis/ # Cross-platform session analysis / report SDK (pip install sfm-analysis) -└── library.properties # Arduino library metadata +├── firmware/ # Arduino library (ESP32-S3 node) +│ ├── src/ # VFM.h / services / pin definitions +│ ├── examples/ # Node sketch + hardware bring-up sketches +│ ├── docs/ # Wiring, dispense cycle, firmware tunables +│ └── library.properties +├── packages/ +│ ├── dev_gui/ # Raspberry Pi developer GUI + CAN tooling +│ │ ├── experiments/ # JSON schemas for built-in experiments +│ │ ├── examples/templates/ # Copy-from custom experiment starters +│ │ └── base_station/experiment/ # Engine + templates (see its README) +│ └── sfm-analysis/ # Cross-platform session analysis / report SDK +│ ├── docs/ANALYSIS_GUIDE.md +│ └── examples/ # Analysis recipes + a custom report-design starter +└── README.md ``` diff --git a/docs/DISPENSE_CYCLE.md b/firmware/docs/DISPENSE_CYCLE.md similarity index 98% rename from docs/DISPENSE_CYCLE.md rename to firmware/docs/DISPENSE_CYCLE.md index a2997a0..48ccd1b 100644 --- a/docs/DISPENSE_CYCLE.md +++ b/firmware/docs/DISPENSE_CYCLE.md @@ -214,6 +214,7 @@ Node → base on CAN ID `0x300 + nodeId`. Byte 0 is the event code. | `0x0E` | `NoFeedPresented` | count LE16 (unchanged) | A no-feed raise finished; empty plate at the top. `count` does NOT increment | | `0x0F` | `Dwelling` | count LE16 | Phase entered: holding at the drop position, M1 idle (no-feed cycle) | | `0x10` | `PresenceCalResult` | ok(1), threshold LE32, samples LE16 | Response to a `CalibratePresence` command — see below | +| `0x11` | `ConfigApplied` | configType(1), ok(1), value LE32 | Ack of a `SetConfig` (heartbeat interval, presence factor) | `count` is the node's running total of `Loaded` milestones — `NoFeedPresented` deliberately does not advance @@ -226,7 +227,7 @@ the number that appears in logs or reports. The base station keeps its own per-r at zero when a session opens, and folds the node's counter in as a **delta** so a milestone frame lost to the bus is recovered by the next frame carrying the count (or by the next heartbeat) and reported as a gap rather than silently dropped. The node counter is the independent witness that makes that detection possible, which -is why the firmware still sends it. See `tools/dev_gui/base_station/pellet_ledger.py`. +is why the firmware still sends it. See `packages/dev_gui/base_station/pellet_ledger.py`. **Presence recalibration.** `CanCmd::CalibratePresence` (`0x09`, no payload, broadcast-friendly) starts a fresh 5 s idle-pad capture on the presence sensor — the same action as a short `PIN_BTN` click. The node replies with diff --git a/docs/HARDCODED_VALUES.md b/firmware/docs/HARDCODED_VALUES.md similarity index 99% rename from docs/HARDCODED_VALUES.md rename to firmware/docs/HARDCODED_VALUES.md index f951102..acd23da 100644 --- a/docs/HARDCODED_VALUES.md +++ b/firmware/docs/HARDCODED_VALUES.md @@ -3,7 +3,7 @@ Living note of firmware constants that may need changing after bench or field tweaks. **Update this file when you change a default.** Source of truth remains the code; this is the checklist. -GUI / experiment / report defaults: [BASE_STATION_HARDCODED_VALUES.md](BASE_STATION_HARDCODED_VALUES.md). +GUI / experiment / report defaults: [BASE_STATION_HARDCODED_VALUES.md](../../packages/dev_gui/docs/BASE_STATION_HARDCODED_VALUES.md). Pins (`VFMPins.h`) and CAN ID opcodes (`ServiceTypes.h`) are omitted unless they carry timing or motion meaning. For what these timers guard and where they sit in the cycle, see [DISPENSE_CYCLE.md](DISPENSE_CYCLE.md). @@ -67,7 +67,7 @@ Not overrideable via SetConfig CAN yet — only compile-time / setter before beg **No tunables.** A no-feed cycle holds at the drop position until it sees a `Raising` event from another node on the bus, then raises. There is no dwell constant, no clamp, and no payload on `DispenseNoFeed` — the fed node's own raise is the timing reference, so nothing here needs tuning per rig. See -`docs/DISPENSE_CYCLE.md` § No-feed dispense. +[DISPENSE_CYCLE.md](DISPENSE_CYCLE.md) § No-feed dispense. There is also no timeout: a node whose peer never raises holds until `Recover`. diff --git a/docs/WIRING.md b/firmware/docs/WIRING.md similarity index 100% rename from docs/WIRING.md rename to firmware/docs/WIRING.md diff --git a/examples/Node/Node.ino b/firmware/examples/Node/Node.ino similarity index 100% rename from examples/Node/Node.ino rename to firmware/examples/Node/Node.ino diff --git a/examples/Troubleshooting/DispenseTest/DispenseTest.ino b/firmware/examples/Troubleshooting/DispenseTest/DispenseTest.ino similarity index 100% rename from examples/Troubleshooting/DispenseTest/DispenseTest.ino rename to firmware/examples/Troubleshooting/DispenseTest/DispenseTest.ino diff --git a/examples/Troubleshooting/HardwareExamples/ActuatorCalTest/ActuatorCalTest.ino b/firmware/examples/Troubleshooting/HardwareExamples/ActuatorCalTest/ActuatorCalTest.ino similarity index 100% rename from examples/Troubleshooting/HardwareExamples/ActuatorCalTest/ActuatorCalTest.ino rename to firmware/examples/Troubleshooting/HardwareExamples/ActuatorCalTest/ActuatorCalTest.ino diff --git a/examples/Troubleshooting/HardwareExamples/CanBusTest/CanBusTest.ino b/firmware/examples/Troubleshooting/HardwareExamples/CanBusTest/CanBusTest.ino similarity index 100% rename from examples/Troubleshooting/HardwareExamples/CanBusTest/CanBusTest.ino rename to firmware/examples/Troubleshooting/HardwareExamples/CanBusTest/CanBusTest.ino diff --git a/examples/Troubleshooting/HardwareExamples/LEDTest/LEDTest.ino b/firmware/examples/Troubleshooting/HardwareExamples/LEDTest/LEDTest.ino similarity index 100% rename from examples/Troubleshooting/HardwareExamples/LEDTest/LEDTest.ino rename to firmware/examples/Troubleshooting/HardwareExamples/LEDTest/LEDTest.ino diff --git a/examples/Troubleshooting/HardwareExamples/MousePresenceTest/MousePresenceTest.ino b/firmware/examples/Troubleshooting/HardwareExamples/MousePresenceTest/MousePresenceTest.ino similarity index 100% rename from examples/Troubleshooting/HardwareExamples/MousePresenceTest/MousePresenceTest.ino rename to firmware/examples/Troubleshooting/HardwareExamples/MousePresenceTest/MousePresenceTest.ino diff --git a/examples/Troubleshooting/HardwareExamples/PhotogateTest/PhotogateTest.ino b/firmware/examples/Troubleshooting/HardwareExamples/PhotogateTest/PhotogateTest.ino similarity index 100% rename from examples/Troubleshooting/HardwareExamples/PhotogateTest/PhotogateTest.ino rename to firmware/examples/Troubleshooting/HardwareExamples/PhotogateTest/PhotogateTest.ino diff --git a/examples/Troubleshooting/HardwareExamples/StepperMotorTest/StepperMotorTest.ino b/firmware/examples/Troubleshooting/HardwareExamples/StepperMotorTest/StepperMotorTest.ino similarity index 100% rename from examples/Troubleshooting/HardwareExamples/StepperMotorTest/StepperMotorTest.ino rename to firmware/examples/Troubleshooting/HardwareExamples/StepperMotorTest/StepperMotorTest.ino diff --git a/library.properties b/firmware/library.properties similarity index 100% rename from library.properties rename to firmware/library.properties diff --git a/src/VFM.cpp b/firmware/src/VFM.cpp similarity index 100% rename from src/VFM.cpp rename to firmware/src/VFM.cpp diff --git a/src/VFM.h b/firmware/src/VFM.h similarity index 100% rename from src/VFM.h rename to firmware/src/VFM.h diff --git a/src/hardware/VFMPins.h b/firmware/src/hardware/VFMPins.h similarity index 100% rename from src/hardware/VFMPins.h rename to firmware/src/hardware/VFMPins.h diff --git a/src/services/CanService.cpp b/firmware/src/services/CanService.cpp similarity index 100% rename from src/services/CanService.cpp rename to firmware/src/services/CanService.cpp diff --git a/src/services/CanService.h b/firmware/src/services/CanService.h similarity index 100% rename from src/services/CanService.h rename to firmware/src/services/CanService.h diff --git a/src/services/DispenserService.cpp b/firmware/src/services/DispenserService.cpp similarity index 100% rename from src/services/DispenserService.cpp rename to firmware/src/services/DispenserService.cpp diff --git a/src/services/DispenserService.h b/firmware/src/services/DispenserService.h similarity index 100% rename from src/services/DispenserService.h rename to firmware/src/services/DispenserService.h diff --git a/src/services/LedService.h b/firmware/src/services/LedService.h similarity index 100% rename from src/services/LedService.h rename to firmware/src/services/LedService.h diff --git a/src/services/NodeIdentity.cpp b/firmware/src/services/NodeIdentity.cpp similarity index 100% rename from src/services/NodeIdentity.cpp rename to firmware/src/services/NodeIdentity.cpp diff --git a/src/services/NodeIdentity.h b/firmware/src/services/NodeIdentity.h similarity index 100% rename from src/services/NodeIdentity.h rename to firmware/src/services/NodeIdentity.h diff --git a/src/services/PresenceService.cpp b/firmware/src/services/PresenceService.cpp similarity index 100% rename from src/services/PresenceService.cpp rename to firmware/src/services/PresenceService.cpp diff --git a/src/services/PresenceService.h b/firmware/src/services/PresenceService.h similarity index 100% rename from src/services/PresenceService.h rename to firmware/src/services/PresenceService.h diff --git a/src/services/ServiceTypes.h b/firmware/src/services/ServiceTypes.h similarity index 100% rename from src/services/ServiceTypes.h rename to firmware/src/services/ServiceTypes.h diff --git a/tools/dev_gui/README.md b/packages/dev_gui/README.md similarity index 76% rename from tools/dev_gui/README.md rename to packages/dev_gui/README.md index ec63541..aee036f 100644 --- a/tools/dev_gui/README.md +++ b/packages/dev_gui/README.md @@ -3,7 +3,7 @@ Python desktop application (DearPyGui) for the **Spatial Foraging Module (SFM)** — a base station plus multiple **VFM** nodes — over the CAN bus on a Raspberry Pi 5. -![SFM Developer GUI](../../docs/GUI.png) +![SFM Developer GUI](docs/GUI.png) ## Requirements @@ -18,7 +18,7 @@ controller device tree overlay and bring up `can0`. This only needs to be done once per Pi: ```bash -cd tools/dev_gui/deploy +cd packages/dev_gui/deploy sudo ./setup-can.sh # reboots automatically if needed sudo ./setup-can.sh --verify # confirm can0 is healthy after reboot ``` @@ -29,14 +29,14 @@ troubleshooting if `--verify` reports a failure. ## Install ```bash -cd tools/dev_gui +cd packages/dev_gui pip install -r requirements.txt --break-system-packages ``` ## Run ```bash -cd tools/dev_gui +cd packages/dev_gui # Real hardware (can0) — GUI: python run.py @@ -60,7 +60,7 @@ For `run_report.py` options (list sessions, combine cohorts, date filters, desig and more), see [Behavior reports](#behavior-reports-session-csv--printable-html). Timings and parameter defaults used by the GUI, experiments, and reports: -[docs/BASE_STATION_HARDCODED_VALUES.md](../../docs/BASE_STATION_HARDCODED_VALUES.md). +[docs/BASE_STATION_HARDCODED_VALUES.md](docs/BASE_STATION_HARDCODED_VALUES.md). ## CLI options @@ -102,8 +102,8 @@ its own package (see [Behavior reports](#behavior-reports-session-csv--printable ```bash # Base station -cd tools/dev_gui -pip install -r requirements.txt # includes -e ../../packages/sfm-analysis +cd packages/dev_gui +pip install -r requirements.txt # includes -e ../sfm-analysis python -m pytest tests/ -v # Report / analysis SDK @@ -112,12 +112,14 @@ pip install -e ".[dev]" pytest ``` -## Experiment engine (headless task/session API) +## Experiment engine -The GUI is for live monitoring and discovery. Behavioral tasks live in a -separate **event-driven experiment engine** under -[`base_station/experiment/`](base_station/experiment/). Nodes stay dumb (commands in, -events out); your script decides what to do next. +Behavioral tasks live under +[`base_station/experiment/`](base_station/experiment/). Nodes stay dumb +(commands in, events out); the template decides what to do next. **You +always start a session from the GUI** — pick a template, fill the form, +enter a session name, click Start. The node tiles and event log stay live +while it runs. ### Quick start — free feeding against the simulator @@ -125,39 +127,42 @@ events out); your script decides what to do next. # Terminal 1 — fake nodes (Linux / Pi; needs vcan0 — see above) python node_simulator.py --interface vcan0 --nodes 3 --skip-discovery -# Terminal 2 — run the built-in free-feeding template for 60 s -python run_experiment.py free_feeding --interface vcan0 --nodes 1,2,3 \ - --seconds 60 --reload-delay 2 --no-io --log-dir ~/sfm_logs +# Terminal 2 — GUI on the same bus +python run.py --interface vcan0 --nodes 3 ``` -On a non-Pi host use `--no-io` so GPIO/BNC setup is skipped. +On the Experiment panel: choose **Free Feeding**, set Advance / duration +if you want, enter a session name, Start. ### Write your own experiment +A GUI-hosted template is two files with the same name: a `build()` factory +and a JSON parameter schema. File layout and two copy-from starters +(event-based round-robin, and an `@exp.script` BNC-paced FR1): +[examples/templates/](examples/templates/). Authoring guide: +[`base_station/experiment/README.md`](base_station/experiment/README.md). + ```python -from base_station.experiment import Experiment, EventKind +from ..runner import Experiment -exp = Experiment(nodes=[1, 2, 3], name="my_task") +def build(nodes=None, *, name="my_task", delay_s=2.0, **_): + exp = Experiment(nodes=list(nodes or [1, 2, 3]), name=name) -@exp.on_start -def start(control): - for n in control.nodes: - control.dispense(n) + @exp.on_start + def start(control): + for n in control.nodes: + control.dispense(n) -@exp.on_pellet_taken -def reload(control, event): - control.after(2.0, lambda: control.dispense(event.node_id)) + @exp.on_pellet_taken + def reload(control, event): + control.after(delay_s, lambda: control.dispense(event.node_id), + node=event.node_id) -exp.end_after(hours=12) -# exp.run(interface="vcan0") # or: save as my_task.py and use the CLI -``` - -```bash -python run_experiment.py my_task.py --interface vcan0 --no-io + return exp ``` -A script may expose either `exp = Experiment(...)` or -`def build(**kwargs) -> Experiment`. +Copy that to `base_station/experiment/templates/my_task.py` and add +`experiments/my_task.json`; it appears in the GUI on the next launch. ### Built-in templates @@ -171,30 +176,34 @@ A script may expose either `exp = Experiment(...)` or ### API surface - **Events** (`EventKind`): `ON_PLATE`, `LOADED`, `PELLET_TAKEN`, - `FAULT`, phase events, `PRESENCE_CHANGED`, `PG_CHANGED`, plus derived - `DOME_OPENED` / `DOME_CLOSED`, `NODE_ONLINE` / `NODE_OFFLINE`, and - base-station `BNC_IN`, `SESSION_START`, `SESSION_END`. + `NO_FEED_PRESENTED`, `FEED_SKIPPED`, `FAULT`, phase events, + `PRESENCE_CHANGED`, `PG_CHANGED`, plus derived `DOME_OPENED` / + `DOME_CLOSED`, `NODE_ONLINE` / `NODE_OFFLINE`, and base-station + `BNC_IN`, `SESSION_START`, `SESSION_END`. - **Control actions**: `dispense` (pass `feed=False` for a no-pellet motion-only cycle), `recover`, `broadcast_dispense`, `broadcast_recover`, `bnc_pulse`, `set_heartbeat_interval`, `after` / `every` timers, named `counter` / `incr`, `log`. - **Sequential tasks**: `@exp.script` — see `base_station/experiment/README.md` §5. - **Lifecycle**: `start_when(condition)`, `end_after(hours=…, pellets=…)`, `end_when(condition)`. -- **Hosting**: `exp.run(interface=…)` (blocking) or - `runner = exp.make_runner(…); runner.step(now)` for GUI integration later. +- **Hosting**: the GUI drives `ExperimentController` → + `runner.step(now)` (`gui_controller.py`). Tests use + `exp.make_runner(); runner.inject(...); runner.step(now)` with no CAN. -Experiment-level CSV logs go to `--log-dir` as -`experiment__YYYYMMDD_HHMMSS.csv` (separate from the raw-CAN session log). +Every CAN frame, BNC edge, and `control.log(...)` row lands in the unified +session CSV (`/.csv`). ## Behavior reports (session CSV → printable HTML) `run_report.py` turns one or more session CSVs into a self-contained, -printable HTML behavior report — no server, no JavaScript, no external -dependencies beyond the standard library. Charts are inline SVG; print -with Ctrl+P / Cmd+P. +printable HTML behavior report — no server, no external dependencies +beyond the standard library. Charts are inline SVG; print with Ctrl+P / +Cmd+P. An interactive pan/zoom timeline is embedded by default (one +inline `