commit ad079e577128053bcedf975793b8684008a0bba6 Author: Alex Date: Thu May 21 20:29:55 2026 -0500 Initial commit: Radiacode Monitor project diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f053e23 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.json +*.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..01e0557 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Radiacode Monitor + +This project provides a real-time monitoring dashboard for a Radiacode gamma spectrometer. It processes live radiation data, stores it in a SQLite database, and serves an updated JSON payload for a web-based frontend. + +## Features + +- **Real-time Monitoring**: Displays live dose rate and count rate trends. +- **Energy Spectrum**: Shows the accumulated gamma energy spectrum (keV). +- **24-Hour Spectrogram**: A heatmap waterfall view of the radiation spectrum at 5-minute intervals over the last 24 hours. +- **Data Persistence**: Uses SQLite to store historical radiation data. +- **Web Dashboard**: A lightweight HTML/JavaScript frontend using Plotly for interactive charting. + +## File Structure + +- `rc_read2.py`: The main Python script that interfaces with the Radiacode device, processes data, and updates the database and web JSON. +- `frontend.htm`: The web dashboard displaying the charts. +- `radiacode_data.db`: SQLite database containing historical spectrum and rate data. +- `live_spectrum.json`: Aggregated JSON data used by the frontend. + +## Setup and Usage + +1. **Prerequisites**: + - A Radiacode device connected to the system. + - Python 3 with `sqlite3` and the `radiacode` wrapper library installed. + +2. **Running the Monitor**: + Execute the Python script to start the data collection loop: + ```bash + python rc_read2.py + ``` + +3. **Viewing the Dashboard**: + The dashboard is designed to be served via a web server (like Apache) that can access the `live_spectrum.json` file. Point your web server to the directory containing `frontend.htm`. + +## Data Processing + +- **Intervals**: The script polls for spectrum data every 5 minutes and performs a hardware reset to ensure clean window alignment. +- **Aggregation**: The `update_web_json` function downsamples the last 24 hours of rate data and aggregates spectrum counts to keep the JSON payload size manageable for the web frontend. diff --git a/frontend.htm b/frontend.htm new file mode 100644 index 0000000..de83b0c --- /dev/null +++ b/frontend.htm @@ -0,0 +1,110 @@ +

Radiacode Monitor

+ +
+

24-Hour Gamma Spectrum

+
+
+ +
+

Dose Rate (Last 24h)

+
+
+ +
+

Count Rate (Last 24h)

+
+
+ +
+

24-Hour Spectrogram Waterfall

+
+
+
+

Raw Data

+ + +
+
+

Information

+
There are a few oddities with the data here. You will notice a spike at the highest energy channel. This captures all energies at 2822 KeV and above, making it the widest channel.

During rain, you will see a gradual increase in radiation.

The gamma spectrometer is down the street from a location that performs RT (Radiographic Testing) on their oil/gas equipment, so on many weeknights, you will see spikes in the real time radiation levels.

The radiation units appear to be uRem, even though I have the Radiacode set to uSv.
+ +
+ + diff --git a/rc_read2.py b/rc_read2.py new file mode 100644 index 0000000..181af6c --- /dev/null +++ b/rc_read2.py @@ -0,0 +1,196 @@ +import time +import datetime +import sqlite3 +import json +import os +from radiacode import RadiaCode # Assuming this is your wrapper + +# Paths on your mounted NFS share +DB_PATH = "./radiacode_data.db" +JSON_OUTPUT_PATH = "/mnt/Share/radiation/live_spectrum.json" + +def init_db(): + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + # Table for 5-minute spectrum snapshots + cursor.execute('''CREATE TABLE IF NOT EXISTS spectrums ( + timestamp TEXT PRIMARY KEY, + duration_secs REAL, + channels_json TEXT)''') + # Table for instantaneous dose rate snapshots + cursor.execute('''CREATE TABLE IF NOT EXISTS rates ( + timestamp TEXT PRIMARY KEY, + count_rate REAL, + dose_rate REAL)''') + conn.commit() + conn.close() + +def calculate_channels(a0, a1, a2, counts): + """Calculates keV for each channel and pairs it with the count.""" + spectrum_data = [] + for channel_num, count in enumerate(counts): + # Formula: a0 + a1*ch + a2*ch^2 + kev = a0 + (a1 * channel_num) + (a2 * (channel_num ** 2)) + spectrum_data.append({"kev": round(kev, 2), "count": count}) + return spectrum_data + +def update_web_json(): + """Generates a small JSON file containing the latest state for Apache.""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Define the 24-hour lookback window + yesterday = (datetime.datetime.now() - datetime.timedelta(hours=24)).isoformat() + + # 1. Fetch real-time count and dose rate history for the timeline chart + cursor.execute("SELECT timestamp, count_rate, dose_rate FROM rates WHERE timestamp > ? ORDER BY timestamp ASC", (yesterday,)) + all_rates = cursor.fetchall() + + # Downsample rates to avoid massive JSON files (target ~1000 points) + # Instead of taking arbitrary interleaved points (which causes graph jitter and misses peaks), + # we group the data into bins and calculate the maximum value for each bin. + rates_history = [] + if all_rates: + step = max(1, len(all_rates) // 1000) + for i in range(0, len(all_rates), step): + chunk = all_rates[i:i+step] + max_dose = max(r[2] for r in chunk) + max_count = max(r[1] for r in chunk) + # Use the middle timestamp of the chunk to represent the bin's time + timestamp = chunk[len(chunk)//2][0] + rates_history.append((timestamp, max_count, max_dose)) + + # 2. Fetch all 5-minute isolated intervals from the last 24 hours + + cursor.execute("SELECT timestamp, channels_json FROM spectrums WHERE timestamp > ? ORDER BY timestamp ASC", (yesterday,)) + spectrogram_rows = cursor.fetchall() + + spectrogram_history_list = [] + master_aggregation_dict = {} + + # 3. Parse rows: build the waterfall history AND calculate the 24-hour combined sum + for row in spectrogram_rows: + timestamp = row[0] + channels = json.loads(row[1]) + + # Keep this isolated chunk intact for the spectrogram waterfall + # We MUST keep the full array (including zeros) because Plotly's heatmap + # expects zValues[row][col] to map directly to xChannels[col] + spectrogram_history_list.append({"time": timestamp, "counts": [ch["count"] for ch in channels]}) + + # Accumulate the values into our master math dictionary to wipe out empty channels + # Only the accumulated total spectrum can be filtered for zeros to save size + for ch in channels: + kev = ch["kev"] + count = ch["count"] + master_aggregation_dict[kev] = master_aggregation_dict.get(kev, 0) + count + + # Convert the flattened dictionary back to a sorted list of objects for Plotly + accumulated_spectrum_kev = [] + accumulated_spectrum_counts = [] + for kev, count in sorted(master_aggregation_dict.items()): + accumulated_spectrum_kev.append(round(kev, 2)) + accumulated_spectrum_counts.append(count) + + # Prepare parallel arrays for rates_history + rates_history_times = [] + rates_history_count_rates = [] + rates_history_dose_rates = [] + for r in rates_history: + rates_history_times.append(r[0]) + rates_history_count_rates.append(r[1]) + rates_history_dose_rates.append(r[2]) + + # Prepare parallel arrays for spectrogram_history + spectrogram_history_times = [s["time"] for s in spectrogram_history_list] + spectrogram_history_counts = [s["counts"] for s in spectrogram_history_list] + + # 4. Construct the output payload + payload = { + "accumulated_spectrum": { + "kev": accumulated_spectrum_kev, + "count": accumulated_spectrum_counts + }, + "rates_history": { + "time": rates_history_times, + "count_rate": rates_history_count_rates, + "dose_rate": rates_history_dose_rates + }, + "spectrogram_history": { + "time": spectrogram_history_times, + "counts": spectrogram_history_counts + } + } + + # Atomic write to prevent Apache from reading a half-written file + temp_path = JSON_OUTPUT_PATH + ".tmp" + with open(temp_path, 'w') as f: + json.dump(payload, f) + os.replace(temp_path, JSON_OUTPUT_PATH) + conn.close() + +def main(): + init_db() + rc = RadiaCode() + + # Reset device memory on script startup to ensure a clean window alignment + try: + rc.spectrum_reset() + except Exception as e: + print(f"Initial hardware reset failed: {e}") + + # Generate the JSON immediately on startup so the frontend is populated + update_web_json() + print("Initial web JSON generated.") + + last_spectrum_time = time.time() + + while True: + try: + # 1. Handle Continuous Data Buffer + buf = rc.data_buf() + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + for msg in buf: + # Check for DoseRateDB messages + if hasattr(msg, 'dose_rate'): + ts = msg.dt.isoformat() + cursor.execute("INSERT OR REPLACE INTO rates VALUES (?, ?, ?)", + (ts, msg.count_rate, msg.dose_rate)) + conn.commit() + conn.close() + + # 2. Handle 5-minute Spectrum Polling + if time.time() - last_spectrum_time >= 300: # 300 seconds = 5 mins + spec = rc.spectrum() + + # RE-ENABLED RESET: Wipes the device memory so the NEXT 5 mins start from zero + rc.spectrum_reset() + + ts = datetime.datetime.now().isoformat() + + # Process and format this isolated chunk + channels = calculate_channels(spec.a0, spec.a1, spec.a2, spec.counts) + channels_json = json.dumps(channels) + + # Save isolated snapshot to DB + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("INSERT OR REPLACE INTO spectrums VALUES (?, ?, ?)", + (ts, spec.duration.total_seconds(), channels_json)) + conn.commit() + conn.close() + + # 3. Pull from DB, compile math, and write out to Apache + update_web_json() + last_spectrum_time = time.time() + print(f"[{ts}] Database and web JSON refreshed with hardware interval reset.") + + except Exception as e: + print(f"Error loop caught: {e}") + + time.sleep(1) # Sleep briefly to prevent CPU pinning + +if __name__ == "__main__": + main()