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()