# QTFM Simple Gradiometer

Small Windows desktop application for two to eight QTFM sensors, styled to
match the QTFM2 GUI (QuSpin dark theme, golden-rod accents).

Current features:

- discovers USB serial ports;
- supports 2 to 8 sensors, chosen with the **SENSORS** selector in the header;
- remembers the user-adjusted settings between runs — sensor count, plotted
  pair, RECORD DIFFERENCES, COM port and baud selections, display window, ASD
  options, and the terminal filter — in a human-readable
  `qtfm_settings.json` saved in the folder the executable runs from, so it is
  always easy to find (delete the file to reset to defaults);
- keeps every sensor connection card in its own **SENSOR SETUP** window, so
  the plot area in the main window never shrinks as sensors are added; the
  setup window lays eight cards out in two or three columns to fit the display
  without scrolling;
- opens every sensor independently and refuses to open one port twice;
- uses 115200 baud by default (with the QTFM UI's other common rates);
- validates that each open port is producing correctly framed QTFM data;
- warns when data is silent, garbled, or likely using the wrong baud rate;
- shows each card's live scalar field so every connected sensor is readable at
  a glance, even when it is not the one being plotted;
- always shows three stacked time-series plots — one sensor, a second sensor,
  and their difference — in literal nT units, every trace in QuSpin golden rod;
- lets the **PLOT** selectors put any two of the connected sensors A–H into
  those plots at any time, even while recording; picking the sensor already in
  the other slot swaps the pair (A−B → B−A);
- reports live gradiometer progress next to the plots: a matched-packet
  counter shows that exact-timestamp pairs are being received, both before the
  first plottable value and alongside the live difference;
- replicates the command bar inside the sensor setup window, so filter and
  ASCII commands can be sent from either window (each bar keeps its own
  target);
- provides a **TERMINAL** window showing the raw serial lines from a selected
  sensor, with an optional "starts with" filter (for example `!` for data
  lines or `#` for status lines), pause and clear controls, and a bounded
  2,000-line display;
- plots one-sided amplitude spectral density for both plotted sensors and their
  difference on one shared plot, where golden rod, QuSpin blue and green
  identify the plot slot rather than the sensor letter;
- continuously averages newly available ASD spectra, with a button to restart
  the average;
- provides vertical and horizontal mouse-following ASD cursors that report the
  nearest frequency bin, cursor amplitude, and all three traces' amplitudes;
- lets the ASD FFT window use a chosen number of latest data points (1,000 by
  default);
- uses a linear frequency axis and logarithmic ASD-amplitude axis by default,
  with optional logarithmic X scaling;
- subtracts only packets with exactly matching QTFM millisecond timestamps;
- pauses and clears the gradiometer plot unless timestamps are available, all
  sample rates match, and the timestamp cadence and phase are aligned;
- requires the QTFM millisecond timestamp for plotting;
- reports timestamp availability and offers an enable button when it is absent;
- can set any connected sensor to wired Master or Slave mode;
- queries and displays each sensor's confirmed sync role;
- enables wired synchronization only for exactly one Master plus all remaining
  sensors as Slaves;
- displays the worst live distance between the device millisecond clocks;
- flags clock separation above 1000 ms or mismatched sample phases as needing
  sync, naming the offending sensor;
- displays each sensor's latest `#` status response in its own status bar;
- queries and displays each connected sensor's reported serial number;
- routes an exact ASCII command or selected filter command to one sensor or to
  all connected sensors;
- provides an independent reboot button and data-rate selector for each sensor;
- records one compact CSV with a row per millisecond timestamp shared by every
  recorded sensor: each sensor's field with its serial number in the column
  name. Difference columns are optional — off by default because they multiply
  the file size and are exactly recomputable, with a **RECORD DIFFERENCES**
  checkbox that stores every `reference − sensor` column for a self-contained
  file;
- computes any `X − Y` difference channels from a recording after the fact:
  in-app via the **PARSE RECORDING** window (works from the packaged
  executable, no Python needed), or scripted via the standard-library
  `tools/gradiometer_parser.py` (`--list` to inspect, `--pairs A-B A-C` to
  compute);
- rejects a multi-Master gradiometer configuration while allowing already-running
  all-Slave data only when live alignment checks pass;
- includes a prominent guided wired-sync workflow and recovery timeouts for
  unconfirmed role changes and reboot;
- supports 10, 30, 60, and 300 second display windows;
- closes every port cleanly when the app exits.

The plots use each QTFM sensor's millisecond timestamps as their only timebase.
When timestamps are absent, plotting stays disabled until the user enables them
with the documented `m` serial command.

The ASD view uses a Hann-windowed FFT and reports amplitude spectral density in
`nT/√Hz`. It only updates when the selected window contains uniformly spaced,
valid timestamped samples. New spectral-power windows are accumulated into a
running RMS average. The difference spectrum follows the same synchronization
and exact-timestamp pairing rules as the gradiometer time series, and paused
views remind the user to synchronize the sensors.

Changing the plotted pair keeps each sensor's buffered history, so the two
sensor plots repopulate immediately; only the difference restarts, because it
belongs to the pair that produced it.

Wired synchronization uses the documented single-character commands: space for
Master, `!` for Slave, `"` to query the active role, and `T` to trigger sync
from the Master. A physical Sync_Out-to-Sync_In cable is still required, and
one Master must drive every Slave.

The command bar can target one sensor or all connected sensors. Reboot and
data-rate commands are intentionally kept on the individual sensor cards. The
data-rate selector reads `#Data rate:` acknowledgements and cycles the firmware
rate one step at a time until it confirms the selected interval.

See [GRADIOMETER_GUIDE.md](GRADIOMETER_GUIDE.md) for the complete wired-sync,
role-combination, missing-cable, reboot-recovery, and CSV-recording workflow.

## Run

Double-click `run_app.bat`, or run:

```powershell
py -m pip install -r requirements.txt
py app.py
```

## Sample data and ASD example

The included sample recording contains 37,859 exact timestamp pairs sampled
uniformly at 250 Hz for 151.4 seconds. `examples/sample_asd.png` shows a
17-average ASD made from 4,096-point Hann windows with 50% overlap. Recreate it
with:

```powershell
python tools/generate_sample_asd.py `
  --input qtfm_recording_20260730_190043_gradiometer.csv `
  --output examples/sample_asd.png
```

## Windows distribution

The distribution uses a PyInstaller one-folder executable for reliable startup
and an Inno Setup installer. Both the ZIP and installer contain:

- the standalone Windows application;
- the complete GPL source, tests, and build definitions;
- the sample gradiometer CSV and averaged ASD figure;
- the user guide, GPL license, and third-party notices.

Build from PowerShell with:

```powershell
.\packaging\build_distribution.ps1
```

## License

QTFM Simple Gradiometer is free and open-source software under
GPL-3.0-or-later. See `LICENSE`.
