# QTFM Simple Gradiometer

Small Windows desktop application for two QTFM sensors.

Current features:

- discovers USB serial ports;
- opens two different sensors independently;
- 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;
- plots Sensor A and Sensor B independently in literal nT units;
- plots the gradiometer value as Sensor A minus Sensor B;
- provides full-size Time Series and ASD tabs;
- plots one-sided amplitude spectral density for Sensor A, Sensor B, and A−B;
- 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 Sensor A, Sensor B, and A−B
  amplitudes together;
- 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, both
  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 either connected sensor to wired Master or Slave mode;
- queries and displays each sensor's confirmed sync role;
- enables wired synchronization only for one Master plus one Slave;
- displays the live distance between the two device millisecond clocks;
- flags clock separation above 1000 ms or mismatched sample phases as needing sync;
- 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 Sensor A,
  Sensor B, or both sensors;
- provides an independent reboot button and data-rate selector for each sensor;
- records one compact CSV containing exact-timestamp Sensor A, Sensor B, and
  A−B pairs, with both sensor serial numbers in the column names;
- rejects a two-Master gradiometer configuration while allowing already-running
  two-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, and 60 second display windows;
- closes both ports 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 A−B 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.

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.

The command bar can target Sensor A, Sensor B, or both 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`.
