diff --git a/.gitignore b/.gitignore index 0efdf19..78f3986 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ __pycache__/ sim_results/ sim_results_multi/ tuningTrials/ -# RL_Trials/ \ No newline at end of file +# RL_Trials/ +venv/ \ No newline at end of file diff --git a/A0Calibration/A0CalibrationSketch/A0CalibrationSketch.ino b/A0Calibration/A0CalibrationSketch/A0CalibrationSketch.ino new file mode 100644 index 0000000..9fbebee --- /dev/null +++ b/A0Calibration/A0CalibrationSketch/A0CalibrationSketch.ino @@ -0,0 +1,152 @@ +// A0CalibrationSketch +// ──────────────────────────────────────────────────────────── +// Trimmed-down calibration streamer. Continuously outputs +// ", " lines over serial, where is the +// position measured by one of the two known sensors (A2, A3) +// and is the raw ADC count from the unknown sensor +// on A0. A companion Python notebook buckets these points +// into 0.05mm intervals and exports an Excel calibration. +// +// Handshake protocol (restartable without re-uploading): +// Wake byte : 'S' Python -> Arduino (start/restart streaming) +// Stop byte : 'X' Python -> Arduino (stop streaming, return to idle) +// +// Idle state : Arduino waits for 'S', ignores all other bytes. +// Stream state: Arduino emits data lines, watches for 'X'. +// On 'X' it returns to idle state. +// +// Python connect sequence (works regardless of current Arduino state): +// 1. Send 'X' (stops streaming if running; no-op if idle) +// 2. sleep 200 ms + flush (drain any in-flight data lines) +// 3. Send 'S' (Arduino: 1 s settle, then prints #READY) +// 4. Wait for '#READY' +// +// Data lines: , \n — no other output ever. +// ──────────────────────────────────────────────────────────── + +#include +#include + +// ── ADC Interrupt-driven 3-channel read (A2, A3, A0) ───────── +// Channel index: 0 → A2 (sensor 0), 1 → A3 (sensor 1), 2 → A0 (unknown) +static const uint8_t adc_mux[3] = {2, 3, 1}; + +volatile uint16_t adc_result[3] = {0, 0, 0}; +volatile bool adc_ready[3] = {false, false, false}; +volatile uint8_t adc_channel = 0; + +void setupADC() { + ADMUX = (1 << REFS0) | adc_mux[0]; // AVCC ref, start on A2 + ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2); // /16 prescaler + ADCSRA |= (1 << ADSC); +} + +// ── OOR digital inputs ─────────────────────────────────────── +#define OOR_PIN_0 12 // HIGH = out of range, sensor 0 (A2) +#define OOR_PIN_1 13 // HIGH = out of range, sensor 1 (A3) + +volatile bool OOR[2]; + +ISR(ADC_vect) { + uint16_t sample = ADC; + uint8_t ch = adc_channel; + uint8_t next = (ch + 1) % 3; + + if (ch < 2) { + OOR[ch] = digitalRead(ch == 0 ? OOR_PIN_0 : OOR_PIN_1); + if (!OOR[ch]) { + adc_result[ch] = sample; + adc_ready[ch] = true; + } + } else { + // A0: no OOR, always store + adc_result[2] = sample; + adc_ready[2] = true; + } + + ADMUX = (ADMUX & 0xF0) | adc_mux[next]; + adc_channel = next; + ADCSRA |= (1 << ADSC); +} + +// ── ADC → mm linear mappings (raw range: 16–26 mm) ────────── +// Kept identical to AltSensorTesting.ino so calibration is +// performed in the same position frame. +#define adcToMM0(adc) ((float)map(adc, 178, 895, 1600, 2600) / 100.0f) +#define adcToMM1(adc) ((float)map(adc, 176, 885, 1600, 2600) / 100.0f) + +// Mounting offsets so sensor 0 → 0–10 mm, sensor 1 → 10–20 mm +#define OFFSET_MM0 15.6f +#define OFFSET_MM1 6.2f + +// ── Streaming state ────────────────────────────────────────── +bool streaming = false; + +// Enter idle: wait for the 'S' wake byte (all other bytes ignored). +// Then settle, clear stale ADC flags, announce #READY, and set streaming. +// Can be called from both setup() and loop() for restartable sessions. +void waitForWake() { + streaming = false; + while (true) { + if (Serial.available()) { + char c = Serial.read(); + if (c == 'S') break; + // Any other byte (e.g. stray 'X') is silently discarded. + } + } + delay(1000); // ADC reference settle + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + adc_ready[0] = adc_ready[1] = adc_ready[2] = false; + } + Serial.println(F("#READY")); + streaming = true; +} + +// ═════════════════════════════════════════════════════════════ +void setup() { + Serial.begin(2000000); + pinMode(OOR_PIN_0, INPUT); + pinMode(OOR_PIN_1, INPUT); + setupADC(); // ADC runs continuously; emission is gated by streaming flag + waitForWake(); +} + +void loop() { + // Check for stop byte before doing any work. + if (Serial.available()) { + char c = Serial.read(); + if (c == 'X') { + waitForWake(); // returns only after next 'S' + #READY + return; + } + } + + if (!streaming) return; + + uint16_t val[3]; + bool ready[3]; + ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { + for (uint8_t i = 0; i < 3; i++) { + ready[i] = adc_ready[i]; + val[i] = adc_result[i]; + adc_ready[i] = false; + } + } + + if (!ready[0] && !ready[1]) return; + + // Emit one line per in-range sensor sample, paired with the + // most recent A0 raw ADC count (val[2] is always fresh). + if (ready[0]) { + float mm = adcToMM0(val[0]) - OFFSET_MM0; + Serial.print(mm, 3); + Serial.print(F(", ")); + Serial.println(val[2]); + } + if (ready[1]) { + float mm = adcToMM1(val[1]) - OFFSET_MM1; + Serial.print(mm, 3); + Serial.print(F(", ")); + Serial.println(val[2]); + } +} diff --git a/A0Calibration/calibrate_a0.ipynb b/A0Calibration/calibrate_a0.ipynb new file mode 100644 index 0000000..08cc9ef --- /dev/null +++ b/A0Calibration/calibrate_a0.ipynb @@ -0,0 +1,483 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A0 Sensor Calibration\n", + "\n", + "Streams `mm, a0_raw` lines from the `A0CalibrationSketch` Arduino sketch, buckets them into 0.05 mm intervals (±0.02 mm acceptance window), and exports a calibration table to `data/` as a timestamped Excel file.\n", + "\n", + "**Workflow**\n", + "1. Upload `A0CalibrationSketch/A0CalibrationSketch.ino` to the Arduino.\n", + "2. Run the **Config** and **Connect** cells below.\n", + "3. Run the **Collect** cell and sweep the target slowly through the full 0–20 mm range. The cell terminates automatically once every bucket has ≥50 samples, or press **Stop Collection** to halt early.\n", + "4. Run the **Export** cell.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "83a5e59e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "401 buckets from 0.0 to 20.0 mm (step 0.05 mm)\n", + "Acceptance window: +/-0.02 mm from each bucket center\n", + "Target: >= 50 samples per bucket\n" + ] + } + ], + "source": [ + "# === Config ===\n", + "SERIAL_PORT = None # e.g. '/dev/cu.usbmodem14101'; None -> auto-detect\n", + "BAUD_RATE = 115200\n", + "\n", + "MM_MIN = 0.0\n", + "MM_MAX = 20.0\n", + "BUCKET_STEP = 0.05 # bucket spacing (mm)\n", + "BUCKET_WINDOW = 0.02 # accept samples within +/- WINDOW of bucket center\n", + "MIN_SAMPLES = 50 # each bucket must reach this count to auto-stop\n", + "\n", + "from pathlib import Path\n", + "DATA_DIR = Path('data')\n", + "DATA_DIR.mkdir(exist_ok=True)\n", + "\n", + "import numpy as np\n", + "\n", + "N_BUCKETS = int(round((MM_MAX - MM_MIN) / BUCKET_STEP)) + 1\n", + "bucket_centers = np.round(MM_MIN + np.arange(N_BUCKETS) * BUCKET_STEP, 4)\n", + "\n", + "print(f\"{N_BUCKETS} buckets from {MM_MIN} to {MM_MAX} mm (step {BUCKET_STEP} mm)\")\n", + "print(f\"Acceptance window: +/-{BUCKET_WINDOW} mm from each bucket center\")\n", + "print(f\"Target: >= {MIN_SAMPLES} samples per bucket\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "7b88a837", + "metadata": {}, + "source": [ + "## 1. Connect to Arduino" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "bee575ca", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opening /dev/cu.usbmodem11301 @ 115200...\n", + "Sending wake signal...\n", + "Waiting for #READY marker...\n", + "Arduino ready - streaming.\n" + ] + } + ], + "source": [ + "import serial\n", + "import serial.tools.list_ports\n", + "import time\n", + "\n", + "def find_port():\n", + " if SERIAL_PORT is not None:\n", + " return SERIAL_PORT\n", + " ports = list(serial.tools.list_ports.comports())\n", + " for p in ports:\n", + " dev = p.device\n", + " desc = (p.description or '').lower()\n", + " if ('usbmodem' in dev or 'usbserial' in dev.lower()\n", + " or 'arduino' in desc or 'wch' in desc):\n", + " return dev\n", + " if ports:\n", + " return ports[0].device\n", + " raise RuntimeError(\"No serial ports found. Set SERIAL_PORT manually.\")\n", + "\n", + "port = find_port()\n", + "print(f\"Opening {port} @ {BAUD_RATE}...\")\n", + "\n", + "# Close any previously-held handle if this cell is re-run.\n", + "try:\n", + " if ser.is_open:\n", + " ser.close()\n", + "except NameError:\n", + " pass\n", + "\n", + "ser = serial.Serial(port, BAUD_RATE, timeout=0.1)\n", + "\n", + "# Opening the port may or may not reset the board.\n", + "# Either way, the Arduino is in idle state (waiting for 'S') after boot.\n", + "# Wait for reset/boot to finish, then flush anything printed before we opened.\n", + "time.sleep(3.0)\n", + "ser.reset_input_buffer()\n", + "\n", + "# Send 'X' first: stops any ongoing stream if the Arduino was already running.\n", + "# If it's in idle state, 'X' is silently ignored.\n", + "ser.write(b'X')\n", + "time.sleep(0.2)\n", + "ser.reset_input_buffer()\n", + "\n", + "# Send the wake byte. Arduino will settle for 1 s then print #READY.\n", + "print(\"Sending wake signal...\")\n", + "ser.write(b'S')\n", + "\n", + "print(\"Waiting for #READY marker...\")\n", + "deadline = time.time() + 15.0\n", + "seen_ready = False\n", + "while time.time() < deadline:\n", + " raw = ser.readline()\n", + " if not raw:\n", + " continue\n", + " if raw.strip() == b'#READY':\n", + " seen_ready = True\n", + " break\n", + "\n", + "if not seen_ready:\n", + " raise RuntimeError(\n", + " \"Did not receive #READY within 15s. \"\n", + " \"Check sketch is uploaded and SERIAL_PORT is correct.\"\n", + " )\n", + "\n", + "# Discard one line to ensure we start reading on a clean boundary.\n", + "_ = ser.readline()\n", + "print(\"Arduino ready - streaming.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "f4955b1c", + "metadata": {}, + "source": [ + "## 2. Collect calibration data\n", + "\n", + "Run this cell and slowly sweep the target through the full 0–20 mm range. The cell drains the serial stream in a background thread, buckets each valid sample, and updates the plot every ~0.3 s.\n", + "\n", + "Press **Stop Collection** to halt early, or let it run until every bucket has reached the target.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1207881e", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0aa7a77335254ed68cf8461be35468c7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(Button(button_style='danger', description='Stop Collection', icon='stop', style=…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "ename": "CancelledError", + "evalue": "", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mCancelledError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 174\u001b[39m\n\u001b[32m 170\u001b[39m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[32m 171\u001b[39m \n\u001b[32m 172\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m asyncio.sleep(\u001b[32m0.05\u001b[39m)\n\u001b[32m 173\u001b[39m \n\u001b[32m--> \u001b[39m\u001b[32m174\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m collect()\n\u001b[32m 175\u001b[39m \n\u001b[32m 176\u001b[39m \u001b[38;5;66;03m# Tell the Arduino to stop streaming and return to idle,\u001b[39;00m\n\u001b[32m 177\u001b[39m \u001b[38;5;66;03m# so the connect cell can restart without re-uploading the sketch.\u001b[39;00m\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 172\u001b[39m, in \u001b[36mcollect\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 168\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m filled == N_BUCKETS:\n\u001b[32m 169\u001b[39m status_label.value += \u001b[33m' -- target reached'\u001b[39m\n\u001b[32m 170\u001b[39m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[32m 171\u001b[39m \n\u001b[32m--> \u001b[39m\u001b[32m172\u001b[39m \u001b[38;5;28;01mawait\u001b[39;00m asyncio.sleep(\u001b[32m0.05\u001b[39m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m/Library/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/tasks.py:718\u001b[39m, in \u001b[36msleep\u001b[39m\u001b[34m(delay, result)\u001b[39m\n\u001b[32m 714\u001b[39m h = loop.call_later(delay,\n\u001b[32m 715\u001b[39m futures._set_result_unless_cancelled,\n\u001b[32m 716\u001b[39m future, result)\n\u001b[32m 717\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m718\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;01mawait\u001b[39;00m future\n\u001b[32m 719\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 720\u001b[39m h.cancel()\n", + "\u001b[31mCancelledError\u001b[39m: " + ] + } + ], + "source": [ + "import re\n", + "import queue\n", + "import threading\n", + "import asyncio\n", + "import time\n", + "import matplotlib.pyplot as plt\n", + "import ipywidgets as widgets\n", + "from IPython.display import display, clear_output\n", + "\n", + "# === Bucket state (reset on each run of this cell) ===\n", + "counts = np.zeros(N_BUCKETS, dtype=np.int64)\n", + "means = np.zeros(N_BUCKETS, dtype=np.float64)\n", + "\n", + "# Matches \", \" with optional leading sign and whitespace.\n", + "LINE_RE = re.compile(r'^\\s*(-?\\d+\\.?\\d*)\\s*,\\s*(\\d+)\\s*$')\n", + "\n", + "def update_bucket(mm, adc):\n", + " idx = int(round((mm - MM_MIN) / BUCKET_STEP))\n", + " if idx < 0 or idx >= N_BUCKETS:\n", + " return False\n", + " if abs(mm - bucket_centers[idx]) > BUCKET_WINDOW + 1e-9:\n", + " return False # falls into the gap between bucket windows\n", + " counts[idx] += 1\n", + " # Welford-style incremental mean (numerically stable, no accumulator overflow)\n", + " means[idx] += (adc - means[idx]) / counts[idx]\n", + " return True\n", + "\n", + "# === Serial reader thread ===\n", + "# If this cell is re-run, stop the previous reader first so we don't end up\n", + "# with two threads racing on ser.readline().\n", + "try:\n", + " _reader_stop.set()\n", + " _reader_thread.join(timeout=1.0)\n", + "except NameError:\n", + " pass\n", + "\n", + "_reader_stop = threading.Event()\n", + "line_queue = queue.Queue()\n", + "\n", + "def _reader_loop(stop_ev, q, s):\n", + " while not stop_ev.is_set():\n", + " try:\n", + " raw = s.readline() # returns after timeout=0.1s if no data\n", + " except Exception:\n", + " break\n", + " if raw:\n", + " q.put(raw)\n", + "\n", + "_reader_thread = threading.Thread(\n", + " target=_reader_loop, args=(_reader_stop, line_queue, ser), daemon=True,\n", + ")\n", + "_reader_thread.start()\n", + "\n", + "# === UI widgets ===\n", + "stop_button = widgets.Button(\n", + " description='Stop Collection', button_style='danger', icon='stop',\n", + ")\n", + "progress = widgets.IntProgress(\n", + " min=0, max=N_BUCKETS, value=0,\n", + " description='Buckets @target:', bar_style='info',\n", + " style={'description_width': 'initial'},\n", + " layout=widgets.Layout(width='500px'),\n", + ")\n", + "status_label = widgets.Label(value='Starting...')\n", + "plot_out = widgets.Output()\n", + "\n", + "stop_requested = False\n", + "def _on_stop(_b):\n", + " global stop_requested\n", + " stop_requested = True\n", + " stop_button.description = 'Stopping...'\n", + " stop_button.disabled = True\n", + "stop_button.on_click(_on_stop)\n", + "\n", + "display(widgets.VBox([\n", + " widgets.HBox([stop_button, progress]),\n", + " status_label,\n", + " plot_out,\n", + "]))\n", + "\n", + "# === Live plot ===\n", + "plt.ioff()\n", + "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)\n", + "plt.close(fig) # prevent implicit display; we manage display manually\n", + "\n", + "def _draw():\n", + " ax1.clear(); ax2.clear()\n", + " filled = counts > 0\n", + " if filled.any():\n", + " ax1.plot(bucket_centers[filled], means[filled], 'b.-', markersize=3)\n", + " ax1.set_ylabel('Avg A0 ADC')\n", + " ax1.set_title('Calibration curve (filled buckets)')\n", + " ax1.grid(True, alpha=0.3)\n", + "\n", + " colors = [\n", + " '#2ecc71' if c >= MIN_SAMPLES\n", + " else '#f39c12' if c > 0\n", + " else '#e74c3c'\n", + " for c in counts\n", + " ]\n", + " ax2.bar(bucket_centers, counts, width=BUCKET_STEP * 0.9, color=colors)\n", + " ax2.axhline(\n", + " MIN_SAMPLES, color='red', linestyle='--', linewidth=1,\n", + " label=f'target = {MIN_SAMPLES}',\n", + " )\n", + " ax2.set_xlabel('Position (mm)')\n", + " ax2.set_ylabel('Sample count')\n", + " ax2.set_title('Bucket fill progress (green = done, orange = partial, red = empty)')\n", + " ax2.legend(loc='upper right', fontsize=8)\n", + " ax2.grid(True, alpha=0.3)\n", + "\n", + " fig.tight_layout()\n", + " with plot_out:\n", + " clear_output(wait=True)\n", + " display(fig)\n", + "\n", + "_draw()\n", + "\n", + "# === Collection loop ===\n", + "total_parsed = 0\n", + "total_bucketed = 0\n", + "total_malformed = 0\n", + "last_draw = time.monotonic()\n", + "DRAW_INTERVAL = 0.3 # seconds\n", + "\n", + "async def collect():\n", + " global total_parsed, total_bucketed, total_malformed, last_draw\n", + "\n", + " while not stop_requested:\n", + " # Drain whatever came in since last tick.\n", + " try:\n", + " while True:\n", + " raw = line_queue.get_nowait()\n", + " try:\n", + " txt = raw.decode('ascii', errors='replace').strip()\n", + " except Exception:\n", + " total_malformed += 1\n", + " continue\n", + " if not txt or txt.startswith('#'):\n", + " continue # stray marker or blank line\n", + " m = LINE_RE.match(txt)\n", + " if not m:\n", + " total_malformed += 1\n", + " continue\n", + " total_parsed += 1\n", + " mm = float(m.group(1))\n", + " adc = int(m.group(2))\n", + " if update_bucket(mm, adc):\n", + " total_bucketed += 1\n", + " except queue.Empty:\n", + " pass\n", + "\n", + " filled = int(np.sum(counts >= MIN_SAMPLES))\n", + "\n", + " now = time.monotonic()\n", + " if now - last_draw >= DRAW_INTERVAL:\n", + " last_draw = now\n", + " progress.value = filled\n", + " status_label.value = (\n", + " f\"filled={filled}/{N_BUCKETS} \"\n", + " f\"min_count={int(counts.min())} \"\n", + " f\"parsed={total_parsed} \"\n", + " f\"bucketed={total_bucketed} \"\n", + " f\"malformed={total_malformed}\"\n", + " )\n", + " _draw()\n", + "\n", + " if filled == N_BUCKETS:\n", + " status_label.value += ' -- target reached'\n", + " break\n", + "\n", + " await asyncio.sleep(0.05)\n", + "\n", + "await collect()\n", + "\n", + "# Tell the Arduino to stop streaming and return to idle,\n", + "# so the connect cell can restart without re-uploading the sketch.\n", + "try:\n", + " ser.write(b'X')\n", + " time.sleep(0.1)\n", + " ser.reset_input_buffer()\n", + "except Exception:\n", + " pass\n", + "\n", + "# Clean shutdown of the reader thread.\n", + "_reader_stop.set()\n", + "\n", + "_draw()\n", + "filled_final = int(np.sum(counts >= MIN_SAMPLES))\n", + "status_label.value = (\n", + " f\"DONE. filled={filled_final}/{N_BUCKETS} \"\n", + " f\"parsded={total_parsed} bucketed={total_bucketed} malformed={total_malformed}\"\n", + ")\n", + "stop_button.description = 'Stopped'\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Export to Excel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from datetime import datetime\n", + "import pandas as pd\n", + "\n", + "ts = datetime.now().strftime('%Y%m%d_%H%M%S')\n", + "out_path = DATA_DIR / f'a0_calibration_{ts}.xlsx'\n", + "\n", + "has_data = counts > 0\n", + "df = pd.DataFrame({\n", + " 'mm_val': bucket_centers[has_data],\n", + " 'avg_adc': np.round(means[has_data], 3),\n", + "})\n", + "\n", + "df.to_excel(out_path, index=False)\n", + "print(f\"Exported {len(df)} rows -> {out_path.resolve()}\")\n", + "\n", + "short = (counts > 0) & (counts < MIN_SAMPLES)\n", + "empty = counts == 0\n", + "if short.any():\n", + " print(f\" Note: {int(short.sum())} buckets have data but < {MIN_SAMPLES} samples.\")\n", + "if empty.any():\n", + " print(f\" Note: {int(empty.sum())} buckets had no data and are omitted from the export.\")\n", + "\n", + "df.head()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Cleanup (optional — run when done)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " _reader_stop.set()\n", + " _reader_thread.join(timeout=1.0)\n", + "except NameError:\n", + " pass\n", + "\n", + "try:\n", + " if ser.is_open:\n", + " ser.close()\n", + " print(\"Serial port closed.\")\n", + "except NameError:\n", + " pass\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/A0Calibration/calibrate_a0.py b/A0Calibration/calibrate_a0.py new file mode 100644 index 0000000..7ca84b3 --- /dev/null +++ b/A0Calibration/calibrate_a0.py @@ -0,0 +1,447 @@ +#!/usr/bin/env python3 +""" +A0 Calibration — standalone tkinter GUI +Usage: python calibrate_a0.py +Deps: pyserial numpy pandas openpyxl matplotlib +""" + +import re +import time +import threading +from datetime import datetime +from pathlib import Path + +import numpy as np +import tkinter as tk +from tkinter import ttk, messagebox +import matplotlib +matplotlib.use('TkAgg') +from matplotlib.figure import Figure +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import serial +import serial.tools.list_ports +import pandas as pd + +# ── Configuration ───────────────────────────────────────────── +BAUD_RATE = 2_000_000 +MM_MIN = 0.0 +MM_MAX = 20.0 +BUCKET_STEP = 0.05 +BUCKET_WINDOW = 0.02 +MIN_SAMPLES = 50 +DATA_DIR = Path(__file__).parent / 'data' +DRAW_INTERVAL_MS = 300 # GUI redraw period — worker is never throttled by this + +DATA_DIR.mkdir(exist_ok=True) +N_BUCKETS = int(round((MM_MAX - MM_MIN) / BUCKET_STEP)) + 1 +bucket_centers = np.round(MM_MIN + np.arange(N_BUCKETS) * BUCKET_STEP, 4) + +# Bytes regex: avoids decode overhead on every line in the hot path +LINE_RE = re.compile(rb'^\s*(-?\d+\.?\d*)\s*,\s*(\d+)\s*$') + + +class CalibrationApp: + + def __init__(self, root: tk.Tk): + self.root = root + self.root.title('A0 Sensor Calibration') + self.root.minsize(900, 650) + + # ── Shared bucket state (worker writes, GUI reads snapshots) ── + self.counts = np.zeros(N_BUCKETS, dtype=np.int64) + self.means = np.zeros(N_BUCKETS, dtype=np.float64) + self.data_lock = threading.Lock() + + # ── Display stats: written by worker, read by GUI ── + self._stat_parsed = 0 + self._stat_bucketed = 0 + self._stat_malformed = 0 + self._stats_lock = threading.Lock() + + # ── Thread / connection state ── + self.ser = None + self.worker_thread = None + self.worker_stop = threading.Event() + self.collecting = False + + self._build_ui() + self._refresh_ports() + self._schedule_draw() + + # ── UI construction ─────────────────────────────────────── + + def _build_ui(self): + # Connection bar + top = ttk.Frame(self.root, padding=(6, 6, 6, 2)) + top.pack(fill='x') + + ttk.Label(top, text='Port:').pack(side='left') + self.port_var = tk.StringVar() + self.port_cb = ttk.Combobox(top, textvariable=self.port_var, + width=30, state='readonly') + self.port_cb.pack(side='left', padx=(4, 6)) + ttk.Button(top, text='↺', width=3, + command=self._refresh_ports).pack(side='left', padx=2) + self.conn_btn = ttk.Button(top, text='Connect', + command=self._toggle_connect) + self.conn_btn.pack(side='left', padx=2) + self.conn_label = ttk.Label(top, text='● Disconnected', + foreground='#888') + self.conn_label.pack(side='left', padx=8) + + # Control bar + ctrl = ttk.Frame(self.root, padding=(6, 2, 6, 2)) + ctrl.pack(fill='x') + + self.start_btn = ttk.Button(ctrl, text='Start Collection', + command=self._start, state='disabled') + self.start_btn.pack(side='left', padx=2) + self.stop_btn = ttk.Button(ctrl, text='Stop Collection', + command=self._stop, state='disabled') + self.stop_btn.pack(side='left', padx=2) + ttk.Button(ctrl, text='Export Excel', + command=self._export).pack(side='left', padx=(14, 2)) + + ttk.Separator(ctrl, orient='vertical').pack(side='left', + fill='y', padx=8) + + self.progress_var = tk.IntVar(value=0) + self.progress_bar = ttk.Progressbar(ctrl, variable=self.progress_var, + maximum=N_BUCKETS, length=260) + self.progress_bar.pack(side='left') + self.progress_lbl = ttk.Label(ctrl, text=f' 0 / {N_BUCKETS} buckets') + self.progress_lbl.pack(side='left') + + # Stats bar + sbar = ttk.Frame(self.root, padding=(6, 0, 6, 2)) + sbar.pack(fill='x') + self.stats_lbl = ttk.Label(sbar, + text='parsed=0 bucketed=0 malformed=0', + foreground='#555') + self.stats_lbl.pack(side='left') + + # Matplotlib canvas + self.fig = Figure(tight_layout=True) + self.ax1 = self.fig.add_subplot(2, 1, 1) + self.ax2 = self.fig.add_subplot(2, 1, 2, sharex=self.ax1) + + self.ax1.set_ylabel('Avg A0 ADC') + self.ax1.set_title('Calibration curve') + self.ax1.grid(True, alpha=0.3) + + self.ax2.set_xlabel('Position (mm)') + self.ax2.set_ylabel('Sample count') + self.ax2.set_title( + 'Bucket fill progress (green = done, orange = partial, red = empty)') + self.ax2.axhline(MIN_SAMPLES, color='red', linestyle='--', + linewidth=1, label=f'target = {MIN_SAMPLES}') + self.ax2.legend(fontsize=8, loc='upper right') + self.ax2.grid(True, alpha=0.3) + + # Create empty line and bar artists to update later + self.line, = self.ax1.plot([], [], 'b.-', markersize=3) + self.bars = self.ax2.bar(bucket_centers, np.zeros(N_BUCKETS), width=BUCKET_STEP * 0.9) + + self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) + self.canvas.get_tk_widget().pack(fill='both', expand=True, + padx=6, pady=(2, 6)) + + # ── Port management ─────────────────────────────────────── + + def _refresh_ports(self): + ports = list(serial.tools.list_ports.comports()) + self.port_cb['values'] = [p.device for p in ports] + if not ports: + return + for p in ports: + d = (p.description or '').lower() + if ('usbmodem' in p.device or 'usbserial' in p.device.lower() + or 'arduino' in d or 'wch' in d): + self.port_var.set(p.device) + return + if not self.port_var.get(): + self.port_var.set(ports[0].device) + + # ── Connection ──────────────────────────────────────────── + + def _toggle_connect(self): + if self.ser and self.ser.is_open: + self._disconnect() + else: + self._connect() + + def _connect(self): + port = self.port_var.get() + if not port: + messagebox.showerror('No port', 'Select a serial port first.') + return + try: + # timeout=0.05: readline() returns within 50 ms when no data, + # so the worker's stop-event check is always responsive. + self.ser = serial.Serial(port, BAUD_RATE, timeout=0.05) + except serial.SerialException as e: + messagebox.showerror('Connect failed', str(e)) + return + self.conn_btn.config(state='disabled') + self.conn_label.config(text='⏳ Connecting…', foreground='orange') + threading.Thread(target=self._handshake, daemon=True).start() + + def _handshake(self): + """Runs in a daemon thread so the GUI stays responsive during waits.""" + try: + time.sleep(3.0) # cover Arduino reset/boot + self.ser.reset_input_buffer() + self.ser.write(b'X') # stop stream if already running + time.sleep(0.2) + self.ser.reset_input_buffer() + self.ser.write(b'S') # send wake byte + + deadline = time.time() + 15.0 + while time.time() < deadline: + raw = self.ser.readline() + if raw.strip() == b'#READY': + self.ser.readline() # discard potential partial first line + self.root.after(0, self._on_connected) + return + self.root.after(0, lambda: self._on_connect_failed( + 'Did not receive #READY within 15 s.\n' + 'Check sketch upload and baud rate.')) + except Exception as e: + self.root.after(0, lambda err=str(e): self._on_connect_failed(err)) + + def _on_connected(self): + self.conn_label.config(text='● Connected', foreground='green') + self.conn_btn.config(text='Disconnect', state='normal') + self.start_btn.config(state='normal') + + def _on_connect_failed(self, msg): + messagebox.showerror('Connection failed', msg) + try: + self.ser.close() + except Exception: + pass + self.ser = None + self.conn_label.config(text='● Disconnected', foreground='#888') + self.conn_btn.config(text='Connect', state='normal') + + def _disconnect(self): + self._stop() + try: + if self.ser and self.ser.is_open: + self.ser.write(b'X') + self.ser.close() + except Exception: + pass + self.ser = None + self.conn_label.config(text='● Disconnected', foreground='#888') + self.conn_btn.config(text='Connect', state='normal') + self.start_btn.config(state='disabled') + self.stop_btn.config(state='disabled') + + # ── Collection control ──────────────────────────────────── + + def _start(self): + if self.collecting: + return + with self.data_lock: + self.counts[:] = 0 + self.means[:] = 0.0 + with self._stats_lock: + self._stat_parsed = self._stat_bucketed = self._stat_malformed = 0 + + self.collecting = True + self.worker_stop.clear() + self.worker_thread = threading.Thread( + target=self._worker, daemon=True, name='serial-worker') + self.worker_thread.start() + + self.start_btn.config(state='disabled') + self.stop_btn.config(state='normal') + + def _stop(self): + if not self.collecting: + return + self.collecting = False + self.worker_stop.set() + if self.worker_thread: + self.worker_thread.join(timeout=1.0) + self.start_btn.config( + state='normal' if (self.ser and self.ser.is_open) else 'disabled') + self.stop_btn.config(state='disabled') + + def _auto_complete(self): + """Called from worker via root.after — runs on the GUI thread.""" + self._stop() + messagebox.showinfo( + 'Collection complete', + f'All {N_BUCKETS} buckets reached ≥{MIN_SAMPLES} samples.\n' + 'Click Export Excel to save.') + + # ── Serial worker ───────────────────────────────────────── + # + # Design priorities: + # 1. Never miss a byte — reads everything in the OS buffer each wake + # 2. Never wait on the GUI — lock hold-time is a single array update + # 3. Auto-complete in O(1) — tracks filled count incrementally + # + def _worker(self): + ser = self.ser + stop = self.worker_stop + counts = self.counts + means = self.means + lock = self.data_lock + centers = bucket_centers + + parsed = bucketed = malformed = filled_count = 0 + buf = bytearray() + STAT_EVERY = 500 # flush display stats every N parsed lines + + while not stop.is_set(): + waiting = ser.in_waiting + if waiting: + try: + buf += ser.read(waiting) # single syscall for all bytes + except serial.SerialException: + break + + # Process every complete line accumulated in the buffer + while b'\n' in buf: + line, buf = buf.split(b'\n', 1) + line = line.strip() + if not line or line[0:1] == b'#': + continue + + m = LINE_RE.match(line) + if not m: + malformed += 1 + continue + + parsed += 1 + mm = float(m.group(1)) + adc = int(m.group(2)) + idx = int(round((mm - MM_MIN) / BUCKET_STEP)) + + if (0 <= idx < N_BUCKETS + and abs(mm - centers[idx]) <= BUCKET_WINDOW + 1e-9): + with lock: + counts[idx] += 1 + means[idx] += (adc - means[idx]) / counts[idx] + just_filled = (counts[idx] == MIN_SAMPLES) + + if just_filled: + filled_count += 1 + if filled_count == N_BUCKETS: + # Flush final stats then signal the GUI + with self._stats_lock: + self._stat_parsed = parsed + self._stat_bucketed = bucketed + 1 + self._stat_malformed = malformed + self.root.after(0, self._auto_complete) + return + bucketed += 1 + + if parsed % STAT_EVERY == 0: + with self._stats_lock: + self._stat_parsed = parsed + self._stat_bucketed = bucketed + self._stat_malformed = malformed + else: + # Serial quiet — yield briefly so stop_event can be noticed + time.sleep(0.0005) + + # Final stats flush on manual stop + with self._stats_lock: + self._stat_parsed = parsed + self._stat_bucketed = bucketed + self._stat_malformed = malformed + + # ── GUI draw (runs on main thread via root.after) ───────── + + def _schedule_draw(self): + self._draw() + self.root.after(DRAW_INTERVAL_MS, self._schedule_draw) + + def _draw(self): + with self.data_lock: + counts = self.counts.copy() + means = self.means.copy() + with self._stats_lock: + parsed = self._stat_parsed + bucketed = self._stat_bucketed + malformed = self._stat_malformed + + filled = int(np.sum(counts >= MIN_SAMPLES)) + self.progress_var.set(filled) + self.progress_lbl.config(text=f' {filled} / {N_BUCKETS} buckets') + self.stats_lbl.config( + text=f'parsed={parsed} bucketed={bucketed} malformed={malformed}') + + # --- NEW: Update existing artists instead of clearing axes --- + has = counts > 0 + if has.any(): + self.line.set_data(bucket_centers[has], means[has]) + self.ax1.relim() # Recalculate limits + self.ax1.autoscale_view() # Rescale axes + + colors = [ + '#2ecc71' if c >= MIN_SAMPLES + else '#f39c12' if c > 0 + else '#e74c3c' + for c in counts + ] + + # Update each bar's height and color + for bar, count, color in zip(self.bars, counts, colors): + bar.set_height(count) + bar.set_facecolor(color) + + self.ax2.relim() + self.ax2.autoscale_view() + + self.canvas.draw_idle() + + # ── Export ──────────────────────────────────────────────── + + def _export(self): + with self.data_lock: + counts = self.counts.copy() + means = self.means.copy() + if not counts.any(): + messagebox.showwarning('No data', 'No data collected yet.') + return + + ts = datetime.now().strftime('%Y%m%d_%H%M%S') + out_path = DATA_DIR / f'a0_calibration_{ts}.xlsx' + has = counts > 0 + pd.DataFrame({ + 'mm_val': bucket_centers[has], + 'avg_adc': np.round(means[has], 3), + }).to_excel(out_path, index=False) + + short = int(np.sum((counts > 0) & (counts < MIN_SAMPLES))) + empty = int(np.sum(counts == 0)) + msg = f'Exported {int(has.sum())} rows to:\n{out_path}' + if short: + msg += f'\n\n{short} bucket(s) have data but < {MIN_SAMPLES} samples.' + if empty: + msg += f'\n{empty} empty bucket(s) omitted.' + messagebox.showinfo('Exported', msg) + + # ── Cleanup ─────────────────────────────────────────────── + + def on_close(self): + self._stop() + try: + if self.ser and self.ser.is_open: + self.ser.write(b'X') + self.ser.close() + except Exception: + pass + self.root.destroy() + + +if __name__ == '__main__': + root = tk.Tk() + app = CalibrationApp(root) + root.protocol('WM_DELETE_WINDOW', app.on_close) + root.mainloop() diff --git a/A0Calibration/data/Sensor0.xlsx b/A0Calibration/data/Sensor0.xlsx new file mode 100644 index 0000000..4479696 Binary files /dev/null and b/A0Calibration/data/Sensor0.xlsx differ diff --git a/A0Calibration/data/Sensor1.xlsx b/A0Calibration/data/Sensor1.xlsx new file mode 100644 index 0000000..36c0a27 Binary files /dev/null and b/A0Calibration/data/Sensor1.xlsx differ diff --git a/A0Calibration/data/Sensor2.xlsx b/A0Calibration/data/Sensor2.xlsx new file mode 100644 index 0000000..90b4471 Binary files /dev/null and b/A0Calibration/data/Sensor2.xlsx differ diff --git a/A0Calibration/data/Sensor5.xlsx b/A0Calibration/data/Sensor5.xlsx new file mode 100644 index 0000000..1100a88 Binary files /dev/null and b/A0Calibration/data/Sensor5.xlsx differ diff --git a/AltSensorTesting/AltSensorTesting.ino b/AltSensorTesting/AltSensorTesting.ino index 39a5385..49bc145 100644 --- a/AltSensorTesting/AltSensorTesting.ino +++ b/AltSensorTesting/AltSensorTesting.ino @@ -1,111 +1,111 @@ #include #include -// ── ADC Interrupt-driven single-channel read (A0) ──────────── -volatile uint16_t adc_result = 0; -volatile bool adc_ready = false; +// ── ADC Interrupt-driven 3-channel read (A2, A3, A0) ───────── +// Channel index: 0 → A2 (sensor 0), 1 → A3 (sensor 1), 2 → A0 (raw ref) +static const uint8_t adc_mux[3] = {2, 3, 0}; + +volatile uint16_t adc_result[3] = {0, 0, 0}; +volatile bool adc_ready[3] = {false, false, false}; +volatile uint8_t adc_channel = 0; void setupADC() { - // Reference = AVCC (5 V) - ADMUX = (1 << REFS0); - - // Channel = A0 (MUX[3:0] = 0000) - ADMUX &= 0xF0; - - // Prescaler = 16 → 16 MHz / 16 = 1 MHz ADC clock - // Each conversion ≈ 13 ADC clocks → ~76.9 kHz sample rate - ADCSRA = (1 << ADEN) | (1 << ADIE) - | (1 << ADPS2); // ADPS = 100 → /16 - - // Start first conversion + ADMUX = (1 << REFS0) | adc_mux[0]; // AVCC ref, start on A2 + ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2); // /16 prescaler ADCSRA |= (1 << ADSC); } -// ── OOR digital input ──────────────────────────────────────── -#define OOR_PIN 2 // digital pin 2 — HIGH = out of range +// ── OOR digital inputs ─────────────────────────────────────── +#define OOR_PIN_0 12 // HIGH = out of range, sensor 0 (A2) +#define OOR_PIN_1 13 // HIGH = out of range, sensor 1 (A3) -volatile bool OOR; +volatile bool OOR[2]; ISR(ADC_vect) { uint16_t sample = ADC; - // Discard if OOR pin (PD2) is HIGH - OOR = (digitalRead(OOR_PIN)); - if (!OOR) { - adc_result = sample; - adc_ready = true; + uint8_t ch = adc_channel; + uint8_t next = (ch + 1) % 3; + + if (ch < 2) { + // Sensor channels: filter by OOR + OOR[ch] = digitalRead(ch == 0 ? OOR_PIN_0 : OOR_PIN_1); + if (!OOR[ch]) { + adc_result[ch] = sample; + adc_ready[ch] = true; + } + } else { + // A0: no OOR, always store + adc_result[2] = sample; + adc_ready[2] = true; } - ADCSRA |= (1 << ADSC); // kick off next conversion immediately + + ADMUX = (ADMUX & 0xF0) | adc_mux[next]; + adc_channel = next; + ADCSRA |= (1 << ADSC); } -// ── ADC → mm linear mapping ───────────────────────────────── -// ADC 185 → 16 mm, ADC 900 → 26 mm -// #define adcToMM(adc) (16.0f + (float)((adc) - 185) * (10.0f / 715.0f)) -#define adcToMM(adc) (0.0f + (float)((adc) - 0) * (10.0f / 1024.0f)) +// ── ADC → mm linear mappings (raw range: 16–26 mm) ────────── +#define adcToMM0(adc) ((float)map(adc, 178, 895, 1600, 2600) / 100.0f) +#define adcToMM1(adc) ((float)map(adc, 176, 885, 1600, 2600) / 100.0f) -// ── Boundary tracking (in-range only) ──────────────────────── +// Subtract mounting offsets so both sensors share the same position frame: +// Sensor 0 raw 16–26 mm − 16 → 0–10 mm +// Sensor 1 raw 16–26 mm − 6 → 10–20 mm +#define OFFSET_MM0 15.6f +#define OFFSET_MM1 6.2f + +// ── Boundary tracking (in-range only, per sensor) ──────────── #define TRACK_N 10 -uint16_t lowestVals[TRACK_N]; -uint16_t highestVals[TRACK_N]; -uint8_t lowestCount = 0; -uint8_t highestCount = 0; +uint16_t lowestVals[2][TRACK_N]; +uint16_t highestVals[2][TRACK_N]; +uint8_t lowestCount[2] = {0, 0}; +uint8_t highestCount[2] = {0, 0}; -// Insert val into a sorted-ascending array of up to TRACK_N entries -// keeping only the N smallest values seen so far. -static void trackLowest(uint16_t val) { - if (lowestCount < TRACK_N) { - // Array not full — insert in sorted position - uint8_t i = lowestCount; - while (i > 0 && lowestVals[i - 1] > val) { - lowestVals[i] = lowestVals[i - 1]; - i--; - } - lowestVals[i] = val; - lowestCount++; - } else if (val < lowestVals[TRACK_N - 1]) { - // Replace the current largest of the "lowest" set +static void trackLowest(uint8_t s, uint16_t val) { + uint16_t *lv = lowestVals[s]; + uint8_t &lc = lowestCount[s]; + if (lc < TRACK_N) { + uint8_t i = lc; + while (i > 0 && lv[i - 1] > val) { lv[i] = lv[i - 1]; i--; } + lv[i] = val; + lc++; + } else if (val < lv[TRACK_N - 1]) { uint8_t i = TRACK_N - 1; - while (i > 0 && lowestVals[i - 1] > val) { - lowestVals[i] = lowestVals[i - 1]; - i--; - } - lowestVals[i] = val; + while (i > 0 && lv[i - 1] > val) { lv[i] = lv[i - 1]; i--; } + lv[i] = val; } } -// Insert val into a sorted-descending array of up to TRACK_N entries -// keeping only the N largest values seen so far. -static void trackHighest(uint16_t val) { - if (highestCount < TRACK_N) { - uint8_t i = highestCount; - while (i > 0 && highestVals[i - 1] < val) { - highestVals[i] = highestVals[i - 1]; - i--; - } - highestVals[i] = val; - highestCount++; - } else if (val > highestVals[TRACK_N - 1]) { +static void trackHighest(uint8_t s, uint16_t val) { + uint16_t *hv = highestVals[s]; + uint8_t &hc = highestCount[s]; + if (hc < TRACK_N) { + uint8_t i = hc; + while (i > 0 && hv[i - 1] < val) { hv[i] = hv[i - 1]; i--; } + hv[i] = val; + hc++; + } else if (val > hv[TRACK_N - 1]) { uint8_t i = TRACK_N - 1; - while (i > 0 && highestVals[i - 1] < val) { - highestVals[i] = highestVals[i - 1]; - i--; - } - highestVals[i] = val; + while (i > 0 && hv[i - 1] < val) { hv[i] = hv[i - 1]; i--; } + hv[i] = val; } } static void resetTracking() { - lowestCount = 0; - highestCount = 0; + lowestCount[0] = highestCount[0] = 0; + lowestCount[1] = highestCount[1] = 0; } static void printBoundaries() { - Serial.println(F("--- 10 Lowest In-Range ADC Values ---")); - for (uint8_t i = 0; i < lowestCount; i++) { - Serial.println(lowestVals[i]); - } - Serial.println(F("--- 10 Highest In-Range ADC Values ---")); - for (uint8_t i = 0; i < highestCount; i++) { - Serial.println(highestVals[i]); + for (uint8_t s = 0; s < 2; s++) { + Serial.print(F("--- Sensor ")); + Serial.print(s); + Serial.println(F(": 10 Lowest In-Range ADC Values ---")); + for (uint8_t i = 0; i < lowestCount[s]; i++) Serial.println(lowestVals[s][i]); + Serial.print(F("--- Sensor ")); + Serial.print(s); + Serial.println(F(": 10 Highest In-Range ADC Values ---")); + for (uint8_t i = 0; i < highestCount[s]; i++) Serial.println(highestVals[s][i]); } } @@ -115,8 +115,9 @@ bool rawMode = false; // ═════════════════════════════════════════════════════════════ void setup() { - Serial.begin(2000000); - pinMode(OOR_PIN, INPUT); + Serial.begin(115200); + pinMode(OOR_PIN_0, INPUT); + pinMode(OOR_PIN_1, INPUT); setupADC(); Serial.println(F("Send '1' to start sampling, '0' to stop and print bounds, '2' for raw ADC output.")); } @@ -147,37 +148,49 @@ void loop() { // ── Main sample path ──────────────────────────────────── if (!sampling) return; - // Grab the latest ADC value atomically - uint16_t val; - bool ready; - bool newOOR; + uint16_t val[3]; + bool ready[3]; ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { - ready = adc_ready; - val = adc_result; - adc_ready = false; - newOOR = OOR; + for (uint8_t i = 0; i < 3; i++) { + ready[i] = adc_ready[i]; + val[i] = adc_result[i]; + adc_ready[i] = false; + } } - if (!ready) return; // nothing new — come back next iteration + if (!ready[0] && !ready[1]) return; if (rawMode) { - long mm_x100 = map(val, 178, 895, 1600, 2600); - Serial.print(mm_x100 / 100); - Serial.print('.'); - long frac = mm_x100 % 100; - if (frac < 10) Serial.print('0'); - Serial.println(frac); + if (ready[0]) { + Serial.print(adcToMM0(val[0]) - OFFSET_MM0); + Serial.print(F(", ")); + Serial.println(val[2]); + } + if (ready[1]) { + Serial.print(adcToMM1(val[1]) - OFFSET_MM1); + Serial.print(F(", ")); + Serial.println(val[2]); + } return; } - // All values here are in-range (OOR filtered in ISR) - trackLowest(val); - trackHighest(val); - - float mm = adcToMM(val); - Serial.print(val); - Serial.print(", "); - Serial.print(mm, 2); - Serial.print(" mm, "); - Serial.println(newOOR ? "out of range" : "in range"); + // Apply offset for whichever sensor(s) are in range + if (ready[0]) { + float mm = adcToMM0(val[0]) - OFFSET_MM0; + trackLowest(0, val[0]); + trackHighest(0, val[0]); + Serial.print(val[0]); + Serial.print(F(", ")); + Serial.print(mm, 2); + Serial.println(F(" mm (s0)")); + } + if (ready[1]) { + float mm = adcToMM1(val[1]) - OFFSET_MM1; + trackLowest(1, val[1]); + trackHighest(1, val[1]); + Serial.print(val[1]); + Serial.print(F(", ")); + Serial.print(mm, 2); + Serial.println(F(" mm (s1)")); + } } diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index b41f795..0000000 --- a/requirements.txt +++ /dev/null @@ -1,67 +0,0 @@ -alembic==1.18.4 -appnope==0.1.4 -asttokens==3.0.1 -cloudpickle==3.1.2 -colorlog==6.10.1 -comm==0.2.3 -contourpy==1.3.3 -cycler==0.12.1 -debugpy==1.8.20 -decorator==5.2.1 -executing==2.2.1 -Farama-Notifications==0.0.4 -filelock==3.25.2 -fonttools==4.61.1 -fsspec==2026.2.0 -gymnasium==1.2.3 -ImageIO==2.37.2 -imageio-ffmpeg==0.6.0 -ipykernel==7.2.0 -ipython==9.10.0 -ipython_pygments_lexers==1.1.1 -jedi==0.19.2 -Jinja2==3.1.6 -joblib==1.5.3 -jupyter_client==8.8.0 -jupyter_core==5.9.1 -kiwisolver==1.4.9 -Mako==1.3.10 -MarkupSafe==3.0.3 -matplotlib==3.10.8 -matplotlib-inline==0.2.1 -mpmath==1.3.0 -nest-asyncio==1.6.0 -networkx==3.6.1 -numpy==2.4.2 -optuna==4.7.0 -packaging==26.0 -pandas==3.0.1 -parso==0.8.6 -pexpect==4.9.0 -pillow==12.1.1 -platformdirs==4.9.2 -prompt_toolkit==3.0.52 -psutil==7.2.2 -ptyprocess==0.7.0 -pure_eval==0.2.3 -pybullet==3.2.7 -Pygments==2.19.2 -pyparsing==3.3.2 -python-dateutil==2.9.0.post0 -PyYAML==6.0.3 -pyzmq==27.1.0 -scikit-learn==1.8.0 -scipy==1.17.0 -seaborn==0.13.2 -setuptools==82.0.1 -six==1.17.0 -SQLAlchemy==2.0.46 -stack-data==0.6.3 -sympy==1.14.0 -threadpoolctl==3.6.0 -torch==2.10.0 -tornado==6.5.4 -tqdm==4.67.3 -traitlets==5.14.3 -typing_extensions==4.15.0 -wcwidth==0.6.0