Compare commits

...

2 Commits

Author SHA1 Message Date
7c54fe38e3 Calibration supercharged 2026-04-11 21:15:01 -05:00
6b78fd278a altsensortesting press 2 for mm 2026-04-08 22:25:46 -05:00
10 changed files with 1213 additions and 167 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ sim_results/
sim_results_multi/
tuningTrials/
# RL_Trials/
venv/

View File

@@ -0,0 +1,152 @@
// A0CalibrationSketch
// ────────────────────────────────────────────────────────────
// Trimmed-down calibration streamer. Continuously outputs
// "<mm>, <a0_raw>" lines over serial, where <mm> is the
// position measured by one of the two known sensors (A2, A3)
// and <a0_raw> 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: <float_mm>, <int_a0_raw>\n — no other output ever.
// ────────────────────────────────────────────────────────────
#include <Arduino.h>
#include <util/atomic.h>
// ── 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: 1626 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 → 010 mm, sensor 1 → 1020 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]);
}
}

View File

@@ -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 020 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 020 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 \"<float>, <int>\" 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
}

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,123 +1,125 @@
#include <Arduino.h>
#include <util/atomic.h>
// ── 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: 1626 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 1626 mm 16 → 010 mm
// Sensor 1 raw 1626 mm 6 → 1020 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]);
}
}
// ── State ────────────────────────────────────────────────────
bool sampling = false;
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."));
Serial.println(F("Send '1' to start sampling, '0' to stop and print bounds, '2' for raw ADC output."));
}
void loop() {
@@ -128,10 +130,16 @@ void loop() {
if (cmd.charAt(0) == '1') {
sampling = true;
rawMode = false;
resetTracking();
Serial.println(F("Sampling started."));
} else if (cmd.charAt(0) == '2') {
sampling = true;
rawMode = true;
Serial.println(F("Raw ADC output started."));
} else if (cmd.charAt(0) == '0') {
sampling = false;
rawMode = false;
Serial.println(F("Sampling stopped."));
printBoundaries();
}
@@ -140,27 +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;
// All values here are in-range (OOR filtered in ISR)
trackLowest(val);
trackHighest(val);
if (rawMode) {
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;
}
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)"));
}
}

View File

@@ -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