On-device gamma spectrometry and isotope identification for RadiaCode detectors, on an ESP32-S3 touchscreen.
Go to file
highway b9a7343f62 v1.0.6: correct identification of real uranium, thorium and background
Verified against three real sources on hardware, plus a 52-check regression
suite that runs on the device from synthetic spectra.

Identification
- Model the spectral features that are not gamma lines: backscatter, Compton
  edge, single and double escape peaks, sum peaks, and 511 keV annihilation.
  The matcher assumed every peak was a line and handed these to whichever
  nuclide sat nearest, which was the single cause behind phantom Na-22,
  Ba-133 and Am-241 rather than three separate faults.
- A nuclide identified entirely below 110 keV must clear 9 sigma: that region
  is unmodelled X-ray fluorescence and backscatter, and a 4.9 sigma bump at
  63 keV (really Th-234) was being reported as Am-241 at 100%.
- Decay chains: two members with one line each corroborate one another, and a
  single member matched at 8 sigma can carry its chain. Requiring one nuclide
  to show two lines lost a thorium mantle whose Pb-212 was unmistakable.
- Name the dominant chain rather than reporting a mixture. At this resolution
  both natural chains fit the same peaks - their line ratios differ by 0.4%,
  far below what 8.4% FWHM resolves - so real ore read 97% uranium against
  70% thorium and was being called NATURAL MIX.
- Chain anchors and verdicts now have hysteresis, so evidence sitting near a
  threshold no longer empties the candidate list or retracts a correct answer.

Honesty of the display
- An elevated sample is never called background. Identifying which nuclide
  takes counting time; noticing that something is there does not.
- Verdicts are graded: URANIUM? below 80% confidence, with the number shown.
- Quality grades the identification, not the strongest peak, so an
  unidentified bump no longer reports EXCELLENT.
- Natural chains and K-40 must be above ambient to earn a verdict; every wall
  contains both.

Regression suite (ANALYSIS_SELFTEST=1)
Synthetic spectra built from the library at the detector's real resolution,
with a Compton continuum and reproducible noise: uranium, thorium, mixed,
Cs-137, Co-60, Am-241, K-40, background, indoor background with annihilation,
ambient thorium in building materials, elevated-but-unidentified, insufficient
data, and centroid accuracy. It fails on real defects - it caught a background
false positive never seen on hardware, and rejected two fixes that broke other
cases.
2026-08-31 15:16:22 +02:00
firmware v1.0.6: correct identification of real uranium, thorium and background 2026-08-31 15:16:22 +02:00
RadiaCode-Spectrometer v1.0.6: correct identification of real uranium, thorium and background 2026-08-31 15:16:22 +02:00
tools RadiaCode Spectrometer Display: initial public release 2026-08-30 10:25:50 +02:00
.gitignore RadiaCode Spectrometer Display: initial public release 2026-08-30 10:25:50 +02:00
LICENSE RadiaCode Spectrometer Display: initial public release 2026-08-30 10:25:50 +02:00
README.md Fix OTA install: erase flash before opening the download connection 2026-08-30 10:56:09 +02:00

RadiaCode Spectrometer Display

A standalone touchscreen companion for RadiaCode gamma spectrometers. An ESP32-S3 board with a 4.3" display connects to the detector over Bluetooth LE, analyses the live gamma spectrum on-device, and tells you in plain language what it thinks you are looking at — no phone, no app, no cloud.

Isotope candidates screen

What it does

  • Connects over BLE to RadiaCode-102 / 103 / 103G / 110 and reads the live 1024-channel spectrum, dose rate and count rate.
  • Identifies nuclides on-device with a classical peak-search and energy-library match, tuned to what a scintillator can actually resolve.
  • Answers the real question — "URANIUM ORE", "THORIUM", "BACKGROUND" — rather than only listing daughter nuclides.
  • Tells you how long to keep measuring. Peak significance grows as √time, so the device extrapolates from your current statistics and says "keep measuring ~4m 30s" instead of leaving you guessing.
  • Background subtraction with named references per location, so weak samples can be separated from room radon and building-material K-40.
  • Keeps measuring through interruptions. The detector accumulates internally, so a dropped BLE link, a disconnect, or a reboot of the display can all resume the same run. Enable auto-reconnect in Settings — it is off by default so a new device never connects to anything unasked.
  • Logs to microSD — CSV spectra with calibration coefficients, ready for offline analysis in Gamma-MCA.

What it does not do

Being honest about the physics matters more than a longer feature list.

The RadiaCode-110 measures about 8.4% FWHM at 662 keV. Gamma lines closer together than roughly 60100 keV blur into a single bump. Strong, isolated sources (Cs-137, Co-60, Am-241, K-40, and the natural uranium and thorium chains) are identifiable. Weak mixtures are not reliably separable, and no amount of software changes that. Treat the output as ranked probable candidates, not a laboratory result.

Near background level, results depend entirely on having recorded a background reference — ambient air genuinely contains radon daughters (real U-238 chain members) and walls genuinely contain K-40.

Hardware

Part Notes
Guition JC4827W543 ESP32-S3 board 4.3" 480×272 IPS, NV3041A over QSPI, GT911 capacitive touch, 4 MB flash / 8 MB PSRAM. The capacitive variant — the resistive one wires its touch controller to the SD card's SPI pins.
A RadiaCode detector 102 / 103 / 103G / 110. Developed and tested against an RC-110.
microSD card Optional. Must be FAT32 — Windows formats cards over 32 GB as exFAT, which the ESP32 cannot read. The device can reformat a card for you.
USB-C cable Power and flashing.

Nothing needs soldering; the board is used as sold.

Flashing

Built with PlatformIO.

git clone https://git.vhelectronics.com/highway/RadiaCode-Spectrometer.git
cd RadiaCode-Spectrometer/RadiaCode-Spectrometer
pio run -t upload

To watch the diagnostic log:

pio device monitor

The project uses a custom partition table (partitions.csv) giving two 1.9 MB OTA slots instead of the stock 1.31 MB, reclaimed from the unused SPIFFS region. Both app slots are required for over-the-air updates.

First run

On first power-up the device shows a setup screen:

  1. Tap WiFi network (SSID) and type it on the on-screen keyboard.
  2. Tap WiFi password and enter it.
  3. Tap DONE.

WiFi is optional and only used for firmware updates — the spectrometer works fully without it. Credentials are stored in the ESP32's NVS flash and are never part of the source or the firmware image.

Setup does not reappear on later boots. It stays reachable as the last screen in the swipe cycle.

Using it

Swipe left/right to move between seven screens:

Screen Purpose
Home Dose rate, count rate, sample verdict, device status, connect
Isotopes Ranked candidate nuclides; tap one for its full line list
Spectrum Live chart, log/linear (tap to toggle), marked peaks
Analysis Verdict, confidence in σ, chain evidence, how long to keep measuring
Background Record / enable / manage the background reference
Data SD card status, CSV export, card formatting
Settings WiFi, auto-reconnect, updates, brightness, factory reset

Settings and their defaults

Setting Default Notes
Auto-reconnect / resume after reboot off Resumes an interrupted run. Off out of the box so nothing connects unasked.
Connect to detector at startup off Links as soon as the board powers up.
Check for firmware updates at startup off Needs WiFi; the row stays disabled until a network is set.
Display brightness 78% Tap to cycle.

All of these persist in NVS flash until a factory reset.

A typical measurement

  1. Home → CONNECT.
  2. Optionally record a background: Background → START RECORDING with nothing near the detector, 1060 minutes, then STOP & SAVE.
  3. Place your sample and tap RESET on Home to start a fresh run.
  4. Watch Analysis — it tells you when the result is statistically solid.

Longer is better, but with diminishing returns: significance grows as √time, so quadrupling the time only doubles your confidence.

Firmware updates

Settings → Firmware / updates:

  • CHECK NOW queries the latest release from this repository.
  • If a newer version exists the row becomes INSTALL x.y.z; tapping it downloads and flashes it, then reboots.
  • Check for firmware updates at startup does this automatically.

Updates are written to the inactive OTA slot — the running firmware is never overwritten, so a failed or interrupted download leaves the device working on the current version. Requires WiFi.

Factory reset

Settings → Factory defaults (tap twice to confirm) erases WiFi credentials, all preferences and the stored background reference, then reboots into first-run setup. It does not touch files on the SD card.

The microSD card

Entirely optional — measurement and identification never depend on it.

Path Contents
/spectra/ Exported CSV spectra, plus a 5-minute autosave checkpoint
/background/ Named background references (HOME, CAR, BASEMENT…)
/import/ Drop RadiaCode .xml exports here to import as backgrounds
/isotopes.csv Optional replacement nuclide library

Bringing in RadiaCode app backgrounds

Copy XML exports from the official RadiaCode software into /import, then Data → BKG LIBRARY → IMPORT XML. Pick a target slot and tap the file; it is converted and activated in one step.

Extending the nuclide library

On first run with a card the device writes /isotopes_template.csv containing its built-in table. Rename it to isotopes.csv, add nuclides, and reboot — up to 300 nuclides. One row per gamma line:

symbol,name,category,chain,energy_keV,intensity
Bi-214,Bismuth-214,natural,U238,609.31,0.4549
Cs-137,Caesium-137,artificial,none,661.66,0.851

category is natural|artificial|medical|calibration, chain is U238|Th232|none, and intensity is absolute emission probability per decay (01), not relative.

How identification works

  1. Background reference (if any) is subtracted.
  2. The spectrum is smoothed with a resolution-aware window — a peak is ~9 channels wide at 100 keV but ~46 at 2614 keV.
  3. A continuum is estimated from side-bands either side of each peak.
  4. Peaks are found on the net spectrum and ranked by statistical significance, net / √(sample + background + continuum).
  5. Lines are matched within ±0.6 FWHM, weighted by detectability (emission × detector efficiency) rather than raw emission — a small CsI crystal sees Tl-208's 583 keV line far more readily than its 2614 keV one.
  6. Each peak is awarded to the nuclide that best explains it, so one peak cannot confirm several nuclides at once.
  7. Decay-chain members are combined: uranium is identified through Bi-214, Pb-214 and Ra-226, because U-238 itself emits essentially no detectable gammas.

Safety note

This is a hobbyist instrument, not a certified radiation safety device. Do not rely on it for occupational or medical decisions. If you collect radioactive minerals, the practical hazards are dust and radon rather than external dose: don't grind specimens, wash your hands, and store them ventilated.

Credits

  • mkgeiger/RadiaCode — Arduino BLE library for RadiaCode devices (MIT), vendored with documented patches.
  • cdump/radiacode — the reverse-engineered protocol everything else builds on.
  • moononournation/Arduino_GFX — display driver, vendored with an NV3041A QSPI fix.
  • Nuclear data follows published NNDC/ENSDF values.

RadiaCode publishes no official API; all BLE integration is community reverse-engineering. Not affiliated with or endorsed by RadiaCode.

License

MIT — see LICENSE.