From 7c54fe38e35209a9cc847cdf74e5701c5d9a9a67 Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Sat, 11 Apr 2026 21:15:01 -0500 Subject: [PATCH] Calibration supercharged --- .gitignore | 3 +- .../A0CalibrationSketch.ino | 152 ++++++ A0Calibration/calibrate_a0.ipynb | 483 ++++++++++++++++++ A0Calibration/calibrate_a0.py | 447 ++++++++++++++++ A0Calibration/data/Sensor0.xlsx | Bin 0 -> 10866 bytes A0Calibration/data/Sensor1.xlsx | Bin 0 -> 10868 bytes A0Calibration/data/Sensor2.xlsx | Bin 0 -> 10834 bytes A0Calibration/data/Sensor5.xlsx | Bin 0 -> 10833 bytes AltSensorTesting/AltSensorTesting.ino | 223 ++++---- requirements.txt | 67 --- 10 files changed, 1202 insertions(+), 173 deletions(-) create mode 100644 A0Calibration/A0CalibrationSketch/A0CalibrationSketch.ino create mode 100644 A0Calibration/calibrate_a0.ipynb create mode 100644 A0Calibration/calibrate_a0.py create mode 100644 A0Calibration/data/Sensor0.xlsx create mode 100644 A0Calibration/data/Sensor1.xlsx create mode 100644 A0Calibration/data/Sensor2.xlsx create mode 100644 A0Calibration/data/Sensor5.xlsx delete mode 100644 requirements.txt 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 0000000000000000000000000000000000000000..4479696f71cb577d5e24a0d0738792cfcefca8d8 GIT binary patch literal 10866 zcmZ{K2Rxha*SAg0*s=Gl5nG~0?4n9hGpJEPjM|je-kZ|ef>vwS9<_o}HG-{SP3&kAGUCgZyVq;-Z{?8C>-8}4XVkk-J(nWztBdzRrK6sCJ zORB*{vSmZUd$<9O{Ra*3drUm)dy9Du2o@q&=GE&X2lKb1+6Y|vs7V8B?I-BgmKdrl z2w42-Ex>`M?neZB7RP*Jmm_Q5GM44)T{fcFJ654wlj^mR`Y$hLURtCRIFwuyop6JQ z4AY7txuSJC6YPmae$Rk-BxW?^3}93CFz^$UI?iGqtJUjugE`envIj`PFe+R}*DyxW z!_D@}ujVx^pMMJ`o03v<=gU{5IUkz2Hnc{m()!ov9JLxo!}PUkQh%2HM4uF0^Lka( zJpDznYE&S2cH*`}{cmPoS>$j52(Ykn_^`03G0gZOM7`klF7|)E#csISGc)&`m8E(1 zqiWR8;nansPdcQbj@r}FXRhv5_?|9X1f@v=!rv|>{CS6lG+;1;Y%ld*OZFW+q3vJ8 zY8sncvi?WkMQpYrqtg%ltY<{$CR-at7rz>db5lY=3~#FTJBdqlEbGl2u~4!>1JqQg z`Or8bZ9kg9NsF)MjG|nVJ9Tpl55pZ1g+L8E>mW-ZxYDShzL}d9d)+#l#2M|NVS;@T zHnlO7w>i4*@Ep;5IlHJ}Ke7#B5f5<%F`2MGA9j1QrfU|xY}nn?>ExjJ(D@^JOfxui zASfNbC@TDl-KVP4DOHr3|iXFhUX9`nBO0F_FWiHjOnWw^&@LPxF3b8jOi;I z_1bmh<-4kMF5aetE32>W22jz~uPcqSW0|=7Onx52&9D@>@J?r(VQF1}TI`#A2ggwU z?(4;ybR1*IkW}}`O~xn3lIcO~=ml0m8)Cn9r;b@9PmT!G>oF2#^@p5jgmU!GS6!WC zwhk0a1mp%mhMm)&JC^G8v9u5#2K5M~GE zz>(9_loB5Ckss&8ySjHA*dAt7!$wBd^5Q+XS{F`Df==&ixN=Rs?X2aTN>}Gr%kLL% zS)-J#ze^@gtw@v``O-;`8K?M`SVm#wSzTW_WnKno*SNM#4o}Ub@ZuG+k8=`2`zRea7pr=lu+-a-;2(Jikv$v)Ko+1Bm~gNOm1GP zx8P^@G^u~zE}8wEGp@9sF3NqOm6GIG%k{>KcW6t3u}i&*$vX-@b|>IB{&|iRF>?wN z3yuR*tvW}zW9vR^6?bA*MK|SbWd$1sX-1EFx{!t^Z^Nz4sQS=;x?^>em!{=XX$sc; z--c4WhbL3WDN`+FFt)>S$&?+ek+ND@d&nmycvpFrsJmTTp@WaFnw~{|kw6F~{?wSD z*IJ|CeWSQ7E=gFFWqRvF_(P}scTtiGrL5w__y^6b3hM!W#4;IuZF-fVPqDAjvohUf zXQ7?o=ORHPB|n;v!6HAq+%;dUlaH92M zYQ3tNzv+(o+ICf;mx9p9`5mjxx*-w?l|~Qkord)GdR|$ra?YJ&2x$_X&yq77>#N!W=-B9P-y?%@<;2FH;pqIujo6~6Y zugxy~>Y^WaGFU!FA0j!v7e>AuL8(OfKec-4@S;yolEwv5&Mlj!=L2xJ<4bCXvQLZl zHC;w5BSz+j)$LC5b+o!8zfCLp^GPXn=}4$FZPV#rYax#q4B9tu-|aDy?NF0wpvnZc zt!{jLqHEAZ<y zpDuhQ4V)-5E3vyxsi;jD$d*cA8*hFrX_Z7{Q}Mc{qHk7=)IZqdh_vvr>{66k7Nf+M zxN>j;u^aS@(z@c@1R4C4Op-YT-%{*VgLKiFF7ciD-#^HzyL%Fs_u@LR|Gj{OJh!Qw z1Y%){MPp%MPB#mPubYRnC*0oN%Tx59(?6?7Lhgbq^FT_3mD*Qa8VSzBsbsuw(x2eKlfx(Xz?ecl93JTYwR|`^a z46A;)xpr87c>e3s?YV#8_384|(Zb%;xE1TtovVQ7&yUVJ6m!3`K5J`5mMvW`S2gB$ ziUEE|+_~oBccZ_sIp%$j7-~<5O?uqN?0j?_+M`is@>ZkKy`SVnpmZ z=;_6Q-S5lCzaff>cP_=R582q3U?nqU;|tp;f8J*Fb&%rKftRn0r=!*HbMx7f_?Tkv)_!fv3>vj?AO8N`hrnMz|q0+_Qq7}krmss%k$$oH=|RL-`g93 zN5A)0QAsjMM^i!9r>INRr6yV16-0D=X*lWPN8`oa&7VJiUY|Yl{CRp&9`y3s<7&Av z|4iT_Ci_SJ^^>Cm+~586&%O@6ImCQC6VHu;p65&ZKfPLb7V1(`!4u#Z<#m{<3xb&* z&OBZ$ez6*O&SMoB=zlk=;=|FCmc!Ia>z4Yb(|5FztJ>Gf_HmtCh>er96m+L7$E0)` zH&7c7`<}G_POU;wOz^;>zL#)(-0stE3E7_fPIl>So}4uHY>@9VAtQWL$6Qojor}|~ z5}lRGZeQ8OqS>lU#4|P(Px{lVQog?_E?kdi!F#M>P`%4VvsH)a1!yvyfyyI6WUZ3M ztiJlSQ1bhvt*OOc<$@@we3#GWJ6(Dzt!AamWZzn*+3f|SG@D5x^{6Y;Si}>VgCtrX z!}TL}i(N#S4EDe5S2C)b?x#{_4N8YDW%;5(v7u86%1;+Oju)zHXuZfcZ#Oi%q$8IC z=*dVPd*#w-nsPUB_#2ZsFq2NkGl)+z#xJ#%Mc$}{rU&3>%P^^UkrnZW{j&TZ{6%k6 zDU*Z5=;D`$Y3HIBJHBLMPH=A)gf5dl$%F5lHZ&=otca-`p0h)H1gJ&wBaeV&IlXr7?BZTbJ$-;|-YNE71DvJ$Mp9OPe#_>I6cH z1OB?O$Qwkp4wLgY?P?NHZWR$Tb2olm+c%IrF6>cY`Ua3vvz4D{rOg9>0?E)zIuLam|1f^cs(IGuMJ<9wFAu^tp3{4>O7*>Ak?8?j(% z!Iz){Q!gF>DN8ScrXV=yT&Xo(SnZ*u4?gYf6mL*>A2K%H6$w-9-BD@#W!<{?jcR@V z!~n8pu|Sxe{cIJTH&~f34l3E*=*(GWEd74*7Q@JQ+##@wfZ5wGFvS<3lDVOpJ^xCj z(778eJ7803H0MO3sM&qt#Ndf%Cm2xYtQ%`*>3d~3&t}3xlyx-fN|0LK8Z7voLY6Sp zXGlO82zfW-f}3!*Cp~9jSQlbG0fW-gVjkUW+=X3S`_YY52LVWCww=Ij41Mv943j~z$8RHVt%gIK47kQ!W!i=L`c@`ww6O=lO%ZIt}P82 zfd`z&ut&L|U$eGwH+v>@KjJI+Fn}kaajsff+yEhBdW-DYDOs+%opZ^tc&Ql6sZhZG0P>*ia9<_a(!K+ z?Nm^l^8zZDLL3~H;`1$w5wu_~(fye9ZJ1i^2}awA7?A_kQE$W8ga<#06k{LBZ<>$l zlxx4(a(8_hW^zZ;qYr-%!ZWLhOnYZ{-}Yb)ej=NMyk!@wpkL0$I^^>3q z@SA)1;}_pUn>-Se#Q9&LX7LU^G9va2>sq-k$3DMuQBDBQbz=os_C+j65jNhkZ@8ds zNE2CCulsO+AZL-+FzZN&RBvhW00ep5@D;akg84jnE3C3Zy-tU_fKAVd@C;Y8;@wAe zl#D4}sGd_|NU+ZszHY(>t{s?2`K>9V8JwmY(N(mGD{G6nYU1QoM!tMjGsJ_TCt`5*{dq`f6#ec?1#pnsJbv)g; z+epy?6S-V^t=V z6cIjHry--_ZdAfv{hhRbpp~!%r#cT_T$4XgEPApSpD+D-ba>nm9kfc1Ju8#Dot?c1 z_|@e0CG5oB=|-SpiVB5^{9-q*NT0;NBTgE|>Icx{Y5t=Z&k}j9oHeRPk7uEFlo)B! zH*X$ypwA_#bbB_Wh$9<)P8$cpBe2K2$|>SNqBnUF+(asm^wg(EAClFZ{wqW0;4KDHagV zm&_r-Uo@6e0&OjBe!}E4^^dLu8eGh*$gdPRS`ez`xeBb)-l;-}0=2@31&7QTJs5Ys zDN~xeXp#w*N;)|9J5?vzqF<3SnEg>7G9sPd?h677X=$=NB~_&-rk=`r3(=Wr6td{; zx|e2brnuQrMb5JqNL-dgcy5>Pes^yj8f7v}uD!SZvi9mqL=mdcM2@qQXP!jj zaCF|0ba{653(s46k<%yQT~Ik*&o&HDlOiqi23fA2OZ&yt(wHxl`+|XMo=mEg$jI| zYo1Te6Ek|bSKi*rUc9;S;Uo{kGW1lu16sp_b)1waw=@Vu3nLGYrxYmLo6A{VwcL{&J)kGD8HAvkQx$febrOMpHvq??lqrA9>Qqb7T zJMfBGE>}QHBkv0H{gRa8v>_ZZK$!*!LYCrL=cJ}dxW1&4vzDoYBK_hB_@puNq=#ND zZZj}o9qcVV|Bu*)%SF6B{a~((s5&s*f02=r-f9FFm-znLLlI~k0uEH4UN7h&RHvzg z1cxWhXTPbY^gXCo02ORmxQA2JGqFaFBUP>?rKuz!4CQEDVkY*R3qP4L7alaSy3kZ} z*vJY)jwx)pB+}A&6Z3LpG)c#iC~p)ku9irRMgOF{Z5JeF8$V_t62PTFz9OF=A z6d|QYX||WEhYqQ2#tUQ_)?(WNn=`{&IDq<2Q@a3x;bgJ+wYVzQQ0$WJEgJm?h*6;W z;d((Nk$QTcc>>)AVmF%o%0urio_5S8IfLf z@sqcN8gfREU^h?AaZm@640o_vD9ckiN|J1|qM;h4?)e@*JjZ+}Y0&`h$R-m;nfUf3 zbd6d#_=oJ9ON64jxd>m}1tE2}6d*O$VS}IywuAM%pn@GQDelafeerl6O>(DnYt`IR zW4JoO%SS@0Dk1*;)ZIyNw)gN;{6YXQe zLSF@v!gP-ti1XmM_2OA2VlHoiJNr%tDMZiePdJUsvF;b zd3f>wRG@1qKL0_Sn+%sW#NIA!%)Qs)XWI!>X|GBN3$eve`F`z$gW;k#m}9neU*Bdke%Z=!d|lu;7ddry?_Ugb71s+(z3nR8`xXI+l61Q+)m=l`v{A z0@j2*f~4gJ&EZyN)jCP(nBl&eeX)7E0dQtm@XQijWi}}l$|WVo7aZ;P-M#4t{3Xye zZY^gJs+9c;vg{3tb&gwU8-S`7F^7FZ$N~w*BIdEMhg>R0ilStjW^5|io5EUd1JR{B zpQT_q)eNMO*q$6#Pa`r{Zc~>NiAJZeT8nt;1>q|@zO_JYfb8^#CnlhR8WCd@ zaie;8XVWN@gDxIk-WYV8PQpYS8RE=gm4J`Sm|3BI31r%j2yre9OBDj5n|hO8z2~q3 z_R}5qV(@(^xM zCLyR(PJ=}s&W>%^4(!U2isvLfP#q_`+t3cqNOqr$!mikwkeRgrpz4V{gb7b9K*$#k zJu9isaZn39OtqiI$2HrYO9oX51SUogLS5+N;r0j~!B^Y_2ueKjz$f&i@Y?T@`|H{^ zv4vdI9(tSjTpJq4Tj$NQVy=r&%^!A5%+D;pn`lKe6z(vW%u?QdVyC{KFl= zSe%bn#MV;rW3espu<3not9WKphz#_8Ndiu&0ldNdIAN3(FZ?n!NOw4+Th0VQz|lPH zZ9O6Ni0BsP^pLLTNiSW-b`?}K;4Zxt5dp3l(?RB6@=eY!8ABLZ z7^TD$6Dz}SaTxKS{t?J}gDkE&E{KWZy$E6NBhU)?i7Z&UJg!`H;fZwA7h7I?`u61h zYLU?LL8w149&U>eU=|`<=G1IDy8r<}M;U_@c+rnwc&3?Q55$1zw90JGue$GpmT4ye z$B~5C^7`?wVmK`DZe@nqVARxZ!jQW(DosRGBJF0R1jpRwIg_QLn5p=Tkg|o}$ck9B zTHnfYV)K$5=Ryd@?1}^IRy#y0{O|>!({v%oHUj6A_t7I{ZUE5uy3&N&Q4we_ZxFLE9k*V0wL@-5f|!x|ERw;lp*nKDEYy+!A&{_ z*0yJrLNTk}txy=AKK=6{X!ub`fjftd9zArTgw2orVfHIc&r-Uu79ylbdobpfW3tqv z8vjwR7le|kf9_dM%ll*fl%j`9f|`W5L*^fALL*xcazv%UT^N%B7-tyud-3YlCOfGy zmC^MAT@1EPIG`9xy2VMg6Am> zMChmNlku-;^}5N7!a6j~!?$uMocrL5?p*UM9B;CS>09Qv5iM{c1(dJhNEp z+10zCYe7vn<_{tD!Z8|#GWST2IQ!eqW<`Oc;3)f`UUt#Y{z2#-ef&#*&n9O#C3@(n zz26+Bipm5k{E3ly;%Qg3g4Gp+rL{^6QK|@P&T^CIN594iZ^nJlCzXx$f>{!EugYa~ z28ShH487GieB29e$8Hx=l{O+O(bZ0@Oc{#pS&(2VXQUq`w`E`iJ?Tq-^QJJTX!`BA z)3&{3x#*^kMgctl(J}fx@Z4_chkhB-J-p0|kq8qE?(zBodPv47TMJcvW%%AOkzs?5- zt%fO<7GaLV`E0|RQNK17pMe(w@5m2hxm+LbyFWW0tbf*-6!p<6PjT()`meo^G5iDd z2RK+*8pK#wWPkQTJiYu8_MSJ}=nD_$V&_1BE810x(zKUZP2B#a9D4An6H* zEluy+xUVjpxHlWJS07wF%>Mcc43Tgs78`!&%Quiuk<+?XTKs)#yQ}%TLB>fdu5tOb zHKbC0jIr$HQRRX|kOSB`fK%X=@Z(xe1D!3e+Oe=%!6`mD33#KfgJY`8!wU4&=$!=Y zjtqgkHt}ZN+q`NnY8Ic?T zlL&|k(m9eM9F%{8uR{3dA?sN)7)ULKt2lb&C!XHv<2u~xwq zN&8C$l(W61&$JoOSw@L)lZ?|Eu0!uP`&xW*fK;*V!9r}adlIh4!sXv2j=VOO$`Tve z(hHgM5{|0lTy zRytdeZ2krbl?zi{I&I5?iN@A#5e>^Y06R5+5?;5N9)X=2Yx#-dX?;(S8bHuOAu{7- zeNTD+QzE0~$a<`@V|sYA&`}%z*VBhzY#C_hQOE)|O(I$NGZyzz`AUUpW%e!&wC*X%l` z!=Eh;b#=Gg=lOK>9uE+rq@Je|Z8?35=o32!;M36=N6?Y{;P6uOw~MJKM@;|Th-=i? zjqSuF9UUeCNib(C4|{~CsL0KCZmPB`ju@#@6_ve}&f6XwvGVC^m1vTtn9ZVBJt?+5 z$EOL?o$PQ*?d!Ze;VKHr0v~42xe`PlsK;XFp;QIdduIXbyeCK#K`Ct|;X=ve(UWO_ z(8du@zc9*mvQfwL^6oKl%5IBioxLwH%?b8dB`S+cOT}#FNw9?W_0aqX@kCZ`Rr$Be zH&XkrKOdd2CnGh~+=Y%)ign_A(yJ8m7TSxCN^EWF{Fb(^k48&rS$4DIXyfMQd^67ylF>J-(3Nnl?c9%Q>cpc~RCRLMB%!GU$!g#q;9m#SguoOR{LOr`ez0Ws0c^8{i6GFWimzjwagIBDn)P zVQC5K4hNpAC3?lyJk~YmH$w7K6jttbCE<;#LGo;Y`z^oK6LT0Dz4R0blas=d-x2dA z@T9z+l1ZEU+Agyp0vG)r<8@_=Xxlj?`8T-|yQ?h~7*c;>2xk04?w>^ZpA`R-RP`zH zxmqhm%7*(-uEZ!=nIoybIiWpmQtvA zmLz*X0SYWgN_Uy1@+X(@?^;~E*DVenq@olha7?P*PJ13uq8J66 z-!pEO8qsK~;P`sHyYg3%*^$cOOqc+taQ`$tnX9;W)Q6aT7q%Z^aVxsj z{)yV;bL|_k8V951cr+tV%<9V56AG;ZHU4Ig{;ob%3wPzbnA&KLq~0zxTLq_y+FJ3qiu(wHbA&a}y|j`C$&W4E)a zT!<-W>hYd$<|4Ms;`)`7`z^q0;dXm7eBp~v$S^x9_s_VBefIHst3R_H`R4OonKSxR z6kh#!I$2ec)_0#QdRe|o5wqrDCe@~_V_!;BH c!utPAEIn;}OuHc#)-B9i0aL+Q`ER8E10^%HMF0Q* literal 0 HcmV?d00001 diff --git a/A0Calibration/data/Sensor1.xlsx b/A0Calibration/data/Sensor1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..36c0a2777f08d0471ace3de435441f922358a971 GIT binary patch literal 10868 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 JorM6v{{ib|d(r>^ literal 0 HcmV?d00001 diff --git a/A0Calibration/data/Sensor5.xlsx b/A0Calibration/data/Sensor5.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1100a88b446e0c214944630b7dc1695ff5503c14 GIT binary patch literal 10833 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 #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