Skip to content

Commit 2886c79

Browse files
committed
Squashed commit of the following:
commit c965370 Merge: 5a82ea3 f5f8354 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Mon May 4 12:51:56 2026 -0400 Merge branch 'feature/continuous-attempts-to-reconnect' into develop commit f5f8354 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Mon May 4 12:51:19 2026 -0400 add single-executable build scripts commit 984499f Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Fri Apr 10 19:01:01 2026 -0400 other recorders to reference commit db5596c Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Fri Apr 10 19:00:19 2026 -0400 added icon to built .exe commit d60552b Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 16:26:30 2026 -0400 adding feature pairity commit 5f73d2e Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 15:33:52 2026 -0400 loads config commit c9fc4a4 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 15:23:16 2026 -0400 convert to labrecorder config format commit 57fcd21 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 15:23:07 2026 -0400 gg commit b3e3cfb Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 14:18:58 2026 -0400 file writing logging works commit ebcad34 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 14:17:46 2026 -0400 make excessive debug logging about multicast connections stop commit c3c7123 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 14:02:46 2026 -0400 continuous reconnect loop to try and find the headset instead of just aborting commit 5a82ea3 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 13:23:59 2026 -0400 Squashed commit of the following: commit 69420a1 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 05:16:55 2026 -0400 multi-stream xdf hopefuly commit cb1a7d1 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Wed Apr 8 05:07:40 2026 -0400 xdfwriter basic recording support commit 2c909d6 Merge: 4de17ab 8e3c8ba Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Tue Apr 7 17:37:52 2026 -0400 Merge branch 'feature/final-releases-deploy-all' into develop commit 8e3c8ba Merge: 4de17ab 08a48a4 Author: Pho Hale <CommanderPho@users.noreply.github.com> Date: Thu Mar 12 19:30:00 2026 -0400 Merge pull request #5 from CommanderPho/copilot/finalize-cmake-build-workflow Strip Qt6/GUI from build workflow; build only emotiv_lsl server on all platforms commit 08a48a4 Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Date: Thu Mar 12 23:10:43 2026 +0000 Remove Qt6/GUI from build workflow, build only emotiv_lsl server on all platforms - Remove Qt6 installation step from all platforms - Remove Qt-related Linux dependencies (libgl1-mesa-dev, libxkbcommon-dev, libxcb-cursor0) - Add -DLSLTEMPLATE_BUILD_GUI=OFF -DLSLTEMPLATE_BUILD_CLI=OFF to CMake configure - Remove CLI test steps (emotiv_lsl requires hardware) - Update macOS sign-and-notarize to handle emotiv_lsl instead of GUI/CLI - Update repackage step to include correct files (emotiv_lsl, Frameworks, config) - Add macOS codesign for emotiv_lsl target in CMakeLists.txt - Add robustness to extract step (handle missing subdirectory gracefully) Co-authored-by: CommanderPho <962210+CommanderPho@users.noreply.github.com> commit 701d1bb Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Date: Thu Mar 12 23:06:49 2026 +0000 Initial plan
1 parent 4de17ab commit 2886c79

46 files changed

Lines changed: 4135 additions & 126 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/lr_mainwindow_snip.cpp

Lines changed: 722 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
name: CFG-driven XDF naming
3+
overview: When `--record` is omitted, resolve an XDF output path from the bundled `LSLTemplate.cfg` by extending `AppConfig` and `ConfigManager::load()` with a new `[Recording]` section (enabled flag, output subdirectory, filename template with placeholders). `--record <path>` continues to override. Executable-relative paths and `create_directories` ensure the file can be created.
4+
todos:
5+
- id: extend-appconfig-recording
6+
content: Add recording fields to AppConfig; section-aware load/save in Config.cpp
7+
status: completed
8+
- id: resolve-recording-path
9+
content: Implement resolveRecordingOutputPath + expose exe dir helper; update LSLTemplate.cfg [Recording]
10+
status: completed
11+
- id: emotiv-main-wireup
12+
content: "emotiv main.cpp: findConfigFile + resolve when --record absent"
13+
status: completed
14+
- id: readme-emotiv-cfg
15+
content: Document cfg-driven XDF naming and override behavior in README
16+
status: completed
17+
isProject: false
18+
---
19+
20+
# CFG-driven default XDF filenames for emotiv_lsl
21+
22+
## Current behavior
23+
24+
- [`src/emotiv/main.cpp`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/src/emotiv/main.cpp) only sets `record_file` when `--record <path>` is present; otherwise recording is off ([`emotiv_base.cpp`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/src/emotiv/emotiv_base.cpp) gates on `!record_file.empty()`).
25+
- The repo already ships [`LSLTemplate.cfg`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/LSLTemplate.cfg) with `[Stream]` / `[Device]`, and [`src/core/src/Config.cpp`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/src/core/src/Config.cpp) implements `lsltemplate::ConfigManager::load` / `findConfigFile`. `emotiv_lsl` already links `LSLTemplate::core` but does not use this config for recording.
26+
27+
## Desired behavior
28+
29+
| CLI | Result |
30+
|-----|--------|
31+
| `--record C:\foo\bar.xdf` | Use that path (unchanged override). |
32+
| No `--record` | If `[Recording]` in the loaded cfg enables auto recording, build a unique path from template; otherwise no XDF (same as today). |
33+
34+
## Design
35+
36+
1. **Extend [`AppConfig`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/src/core/include/lsltemplate/Config.hpp)** with optional recording fields, for example:
37+
- `bool recording_auto_enabled = false`
38+
- `std::string recording_directory = "recordings"` (subdirectory under the executable directory when relative)
39+
- `std::string recording_filename_template = "{stream_name}_{date}_{time}.xdf"`
40+
41+
2. **Section-aware parsing in `ConfigManager::load`** ([`Config.cpp`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/src/core/src/Config.cpp)): today `current_section` is read but ignored, so keys are global. Refactor so:
42+
- `[Stream]` keys (`name`, `type`, `channels`, `sample_rate`, …) only apply when `current_section == "Stream"` (case-sensitive or normalized to match existing files).
43+
- `[Device]` keys (`device_param`, …) only when section is `Device`.
44+
- New `[Recording]` keys (`enabled`, `directory`, `filename_template`, optional `basename` to override `stream_name` for filenames only) only when section is `Recording`.
45+
46+
This avoids collisions if someone adds a `name` under `[Recording]` later.
47+
48+
3. **Append `[Recording]` to [`LSLTemplate.cfg`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/LSLTemplate.cfg)** with commented defaults: `enabled=0` so existing behavior is preserved until the user opts in.
49+
50+
4. **Path resolution helper** (small function in `Config.cpp` + declaration in `Config.hpp`, or a dedicated `.cpp` under `src/emotiv` if you prefer to keep core generic — recommendation: keep **`resolveRecordingOutputPath(const AppConfig&, const std::filesystem::path& exe_dir)`** in `lsltemplate` next to config so the GUI/CLI could reuse it later):
51+
52+
- If `!recording_auto_enabled`, return `nullopt`.
53+
- Build base dir = `exe_dir / recording_directory` (if `recording_directory` is absolute, use it as-is per `std::filesystem` rules).
54+
- Expand template: replace `{stream_name}` with `AppConfig.stream_name`, optional `{basename}` if added, `{date}` → local `YYYY-MM-DD`, `{time}``HH-MM-SS` (filesystem-safe). Sanitize or reject path components if needed (minimal: avoid `:` on Windows in the filename only by using `-` for time).
55+
- `create_directories` for the parent of the final `.xdf` path.
56+
- Return absolute path string (or `path`) for `record_file`.
57+
58+
Reuse the same executable-directory logic already used in [`emotiv_lsl_log_config.cpp`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/src/emotiv/emotiv_lsl_log_config.cpp) — either export a shared `exe_parent_dir()` from a small shared helper or duplicate the minimal call via `lsltemplate::ConfigManager` / existing `getExecutablePath` in `Config.cpp` (already has `getExecutablePath()` in anonymous namespace; consider exposing a public `getExecutableDirectory()` on `ConfigManager` to avoid duplication).
59+
60+
5. **`main.cpp` (emotiv)** after argv parsing:
61+
- If `record_file` non-empty → keep as today.
62+
- Else → `findConfigFile("LSLTemplate.cfg")`, `load`, then `resolveRecordingOutputPath`. If result is set, assign to `record_file` and print the same “Recording natively to XDF file: …” line as manual `--record`.
63+
64+
6. **`ConfigManager::save`**: Update to write `[Recording]` with the new fields so the GUI “Save” does not strip recording options (optional but recommended for consistency).
65+
66+
7. **Docs**: Short paragraph in [`README.md`](C:/Users/pho/repos/EmotivEpoc/ACTIVE_DEV/emotiv-lsl-cpp/README.md) under the emotiv section: `--record` overrides; otherwise enable `[Recording]` in `LSLTemplate.cfg` next to the executable, placeholders, and example.
67+
68+
## Testing
69+
70+
- No `--record`, default cfg `enabled=0`: no recording (no regression).
71+
- Set `enabled=1` in cfg, run without `--confirm`: file appears under `exe_dir/recordings/…`, logs show resolved path.
72+
- `--record` explicit path: cfg ignored for path choice.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
name: Emotiv connection status
3+
overview: Clarify console status so "USB receiver open" is not confused with "headset streaming," and add a paced read loop plus counters so you can distinguish no dongle, dongle idle (waiting for headset), and active EEG/motion packets.
4+
todos:
5+
- id: reword-connect
6+
content: Change post-open log to USB receiver + waiting semantics; optionally print serial once
7+
status: completed
8+
- id: hid-read-timeout
9+
content: Use hid_read_timeout in inner loop with small ms timeout for periodic status
10+
status: completed
11+
- id: stream-detection
12+
content: Track raw reports vs first has_eeg||has_motion; throttle waiting/diagnostic messages
13+
status: completed
14+
- id: retry-message
15+
content: Clarify find_open_emotiv_device failure message (dongle/receiver not detected)
16+
status: completed
17+
- id: manual-test
18+
content: Build Release; test no dongle / dongle only / full headset scenarios
19+
status: completed
20+
isProject: false
21+
---
22+
23+
# Distinct status: dongle vs headset vs streaming
24+
25+
## What goes wrong today
26+
27+
- In [`EmotivBase::main_loop()`](src/emotiv/emotiv_base.cpp), after `find_open_emotiv_device()` succeeds, the code prints **"Connected to Emotiv device: " + `device_name`** (line ~180). `device_name` is the product string from the subclass (e.g. Epoc X in [`emotiv_epoc_x.cpp`](src/emotiv/emotiv_epoc_x.cpp)), not live link state.
28+
- The inner loop uses **`hid_read`** with no timeout ([`emotiv_base.cpp`](src/emotiv/emotiv_base.cpp) ~196–202). On `bytes_read == 0` it **`continue`s with no log**, so there is no feedback while the dongle is present but the headset is off or not yet sending EEG/motion frames.
29+
- **Valid streaming signal**: Treat **`result.has_eeg || result.has_motion`** as "headset is sending usable stream data." Do **not** use `has_quality` alone for that: in [`EmotivEpocX::decode_data()`](src/emotiv/emotiv_epoc_x.cpp), quality is filled from fixed byte indices whenever decryption runs and can look "present" without a real EEG stream.
30+
31+
## Proposed behavior (console)
32+
33+
| Situation | User-visible behavior |
34+
|-----------|------------------------|
35+
| No Emotiv HID interface | Keep retry loop; **reword** message to something like **no USB receiver / dongle detected** (same code path as today: `find_open_emotiv_device()` returns null). |
36+
| HID opened, no EEG/motion yet | One line on connect: **USB receiver open** (include `device_name` and serial if you want). Then **rate-limited** lines (e.g. every 3–5 s): **waiting for headset data** (no valid EEG/motion decoded yet). |
37+
| First EEG or motion sample | One line: **streaming** (EEG and/or motion as applicable). |
38+
| USB reports arrive but 0 EEG/motion after N packets | Optional single diagnostic (or same bucket as "waiting"): **reports received but no EEG/motion — check headset power/pairing** (avoids implying the dongle is "broken" when the radio link is down). |
39+
| `hid_read` error | Keep existing **disconnect / reconnect** message. |
40+
41+
## Implementation (minimal surface area)
42+
43+
**File: [`src/emotiv/emotiv_base.cpp`](src/emotiv/emotiv_base.cpp)** (and only if needed, tiny constants in an anonymous namespace—no header change required unless you extract a small helper).
44+
45+
1. **Replace the connect log** so it does not say "Connected to Emotiv device" in the misleading sense. Example intent: *USB receiver connected — waiting for headset (EEG/motion)…*
46+
47+
2. **Switch the read path to `hid_read_timeout()`** (hidapi already linked) with a modest timeout (e.g. 250–500 ms). This yields:
48+
- Predictable wakeups for status printing without busy-spinning on `bytes_read == 0`.
49+
- Same handling for `bytes_read < 0` (reconnect) and `> 0` (process packet).
50+
51+
3. **Per HID session state** (reset each time a new `device` is opened after the inner loop `break`):
52+
- `bool saw_streaming = false` (set when first `has_eeg || has_motion`).
53+
- `uint64_t raw_hid_reports` (increment for each `bytes_read > 0`).
54+
- `steady_clock` last status print time for throttling.
55+
56+
4. **In the inner loop**, when `bytes_read == 0` **or** when data did not yield EEG/motion:
57+
- If `!saw_streaming` and throttle elapsed, print the **waiting** line (and optionally mention `raw_hid_reports` if > 0 to distinguish "silent dongle" vs "reports but no decode").
58+
59+
5. **Optional polish**: Log **serial_number** once on USB connect (already populated in `find_open_emotiv_device()`) to make support/debugging easier.
60+
61+
## Out of scope (unless you ask)
62+
63+
- **GUI / LSL metadata**: mirroring status in [`src/gui/`](src/gui/) or LSL stream description.
64+
- **New CLI flags** (`--verbose` / JSON): easy to add later if you want machine-readable status.
65+
- **Stricter packet validation** in `validate_data()` (e.g. magic bytes): only if empirical capture shows we need it to avoid counting garbage as "reports."
66+
67+
## Verification
68+
69+
- **No dongle**: paced "no receiver" retry message; process stays up (existing reconnect behavior).
70+
- **Dongle only, headset off**: USB-connected line, then repeating **waiting** lines; no "streaming" until EEG/motion.
71+
- **Headset on**: **waiting** stops being relevant after first stream line; outlets behave as today.
72+
- **Unplug mid-run**: existing disconnect path unchanged.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
name: Emotiv reconnect loop
3+
overview: Keep the `emotiv_lsl` console process alive by wrapping HID connect and the read loop in an infinite reconnect cycle with paced retries, resetting LSL outlets between sessions. Fatal errors (e.g. HID init failure) can still exit via the existing `main()` exception handler; normal Ctrl+C / closing the console window still terminate the process without extra code.
4+
todos:
5+
- id: extract-find-open
6+
content: Add find_open_emotiv_device() (no init/exit, nullptr if missing); refactor get_hid_device() to use it
7+
status: completed
8+
- id: restructure-main-loop
9+
content: hid_init once; outer reconnect loop with sleep; inner read loop; close device + reset outlets + has_motion_data; hid_exit at end
10+
status: completed
11+
- id: header-decl
12+
content: Declare find_open_emotiv_device in emotiv_base.h
13+
status: completed
14+
- id: verify-build
15+
content: Configure/build emotiv_lsl and sanity-check no dongle / unplug scenarios
16+
status: completed
17+
isProject: false
18+
---
19+
20+
# Continuous headset reconnect for `emotiv_lsl`
21+
22+
## Current behavior (why the app exits)
23+
24+
1. **Startup**: [`get_hid_device()`](src/emotiv/emotiv_base.cpp) throws `std::runtime_error("Emotiv headset not found")` (or HID init failure). [`main()`](src/emotiv/main.cpp) catches, prints, returns `1` → process exits.
25+
26+
2. **After connect**: [`main_loop()`](src/emotiv/emotiv_base.cpp) reads in a `while (true)` loop. On `hid_read` &lt; 0 it logs, **breaks**, calls `requestStop()` on the recorder if any, `hid_close`, `hid_exit`, and **returns**`main()` returns `0` → process exits.
27+
28+
```mermaid
29+
flowchart TD
30+
start[main] --> ml[main_loop]
31+
ml --> gh[get_hid_device throws if missing]
32+
gh --> read[read loop]
33+
read -->|bytes_read lt 0| cleanup[hid_close hid_exit return]
34+
read -->|forever| read
35+
cleanup --> exit[process exits]
36+
```
37+
38+
## Target behavior
39+
40+
- **Outer infinite loop** in `EmotivBase::main_loop()` (until the OS terminates the process on window close or Ctrl+C).
41+
- **Connect phase**: If no device, log once per retry and `sleep_for` a fixed interval (e.g. 1–2 s) to avoid a busy spin; repeat until open succeeds.
42+
- **Read phase**: Same logic as today; on `bytes_read < 0`, log that reconnect will be attempted, `hid_close` the handle, **reset** `eeg_outlet` / `motion_outlet` / `eeg_quality_outlet` so the next session creates fresh LSL streams, then continue the outer loop.
43+
- **HID lifecycle**: Call `hid_init()` once at the start of `main_loop()` (throw on failure — unchanged severity). Call `hid_exit()` only when `main_loop()` truly ends (in practice the process is usually killed first; this keeps the API balanced if the function ever returns).
44+
45+
## Code changes (single focal file + small header touch)
46+
47+
Primary implementation in [`src/emotiv/emotiv_base.cpp`](src/emotiv/emotiv_base.cpp):
48+
49+
- Add `#include <thread>` for `std::this_thread::sleep_for`.
50+
- Introduce a **`constexpr` retry interval** (or `static const` duration) for “no device” and optionally reuse a short sleep after disconnect (same interval is fine).
51+
- **Extract** the enumeration/open logic from `get_hid_device()` into a new protected helper, e.g. `find_open_emotiv_device()`, that:
52+
- **Does not** call `hid_init()` / `hid_exit()`.
53+
- Returns `nullptr` if no Emotiv interface is found (no throw).
54+
- Refactor **`get_hid_device()`** (still used by [`get_crypto_key()`](src/emotiv/emotiv_epoc_x.cpp) when `serial_number` is empty) to: `hid_init()``find_open_emotiv_device()` → throw if null (preserve existing contract for that rare path).
55+
- Restructure **`main_loop()`** roughly as:
56+
- `hid_init()` once; throw if nonzero.
57+
- `while (true)` {
58+
- `while (!device)` { `device = find_open_emotiv_device();` if null, log + sleep }
59+
- log connected
60+
- **First successful connect only**: if `record_file` non-empty and `!recorder`, build `watchfor` / construct `recording` (same as today, **after** device is open so `get_lsl_source_id()` still sees a populated `serial_number`).
61+
- Inner read loop: existing decode / outlet creation / push logic.
62+
- On read error: log, `break` inner loop.
63+
- `if (recorder) recorder->requestStop();`**same as today** when the session ends (see note below).
64+
- `hid_close(device);` reset the three `unique_ptr` outlets; reset `has_motion_data = false` for sensible logging on the next session.
65+
- }
66+
- `hid_exit()` after the outer loop (unreachable in the normal “infinite” design unless you later add a shutdown flag).
67+
68+
Declare `find_open_emotiv_device()` in [`src/emotiv/emotiv_base.h`](src/emotiv/emotiv_base.h) under `protected` next to `get_hid_device()`.
69+
70+
[`src/emotiv/main.cpp`](src/emotiv/main.cpp): **No change required** for Ctrl+C / close — default process termination already applies. Keep the `try`/`catch` for genuine fatal exceptions (e.g. HID init failure).
71+
72+
## `--record` / XDF behavior (explicit tradeoff)
73+
74+
Today, `requestStop()` runs when the read loop exits, which **finalizes** the background recording. Keeping that call on each HID disconnect means **XDF recording will not resume** on reconnect without a larger redesign (new `recording` instance, file naming, etc.). The plan **preserves** this stop-on-disconnect recording semantics so reconnect work stays focused on **LSL streaming**, matching your stated problem (dongle unplug / connect). If you want recording to span disconnects, that should be a separate follow-up.
75+
76+
## Out of scope
77+
78+
- **GUI / template CLI** ([`src/gui/`](src/gui/), [`src/cli/`](src/cli/)): not the Emotiv dongle executable; unchanged unless you ask to mirror behavior there.
79+
- **Signal handlers** for graceful `hid_close` on Ctrl+C: not required for your stated exit paths; optional hardening later.
80+
81+
## Verification
82+
83+
- Build `emotiv_lsl`, run **without** dongle: console should repeat a clear “not found, retrying” message and stay running.
84+
- Plug dongle: should connect and stream as before.
85+
- Unplug while running: should log disconnect and return to retry loop without exiting.
86+
- With `--record`: first session records; after unplug, behavior matches chosen `requestStop` semantics above.

0 commit comments

Comments
 (0)