Calibration supercharged
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ __pycache__/
|
|||||||
sim_results/
|
sim_results/
|
||||||
sim_results_multi/
|
sim_results_multi/
|
||||||
tuningTrials/
|
tuningTrials/
|
||||||
# RL_Trials/
|
# RL_Trials/
|
||||||
|
venv/
|
||||||
152
A0Calibration/A0CalibrationSketch/A0CalibrationSketch.ino
Normal file
152
A0Calibration/A0CalibrationSketch/A0CalibrationSketch.ino
Normal 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: 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
483
A0Calibration/calibrate_a0.ipynb
Normal file
483
A0Calibration/calibrate_a0.ipynb
Normal 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 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 \"<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
|
||||||
|
}
|
||||||
447
A0Calibration/calibrate_a0.py
Normal file
447
A0Calibration/calibrate_a0.py
Normal 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()
|
||||||
BIN
A0Calibration/data/Sensor0.xlsx
Normal file
BIN
A0Calibration/data/Sensor0.xlsx
Normal file
Binary file not shown.
BIN
A0Calibration/data/Sensor1.xlsx
Normal file
BIN
A0Calibration/data/Sensor1.xlsx
Normal file
Binary file not shown.
BIN
A0Calibration/data/Sensor2.xlsx
Normal file
BIN
A0Calibration/data/Sensor2.xlsx
Normal file
Binary file not shown.
BIN
A0Calibration/data/Sensor5.xlsx
Normal file
BIN
A0Calibration/data/Sensor5.xlsx
Normal file
Binary file not shown.
@@ -1,111 +1,111 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <util/atomic.h>
|
#include <util/atomic.h>
|
||||||
|
|
||||||
// ── ADC Interrupt-driven single-channel read (A0) ────────────
|
// ── ADC Interrupt-driven 3-channel read (A2, A3, A0) ─────────
|
||||||
volatile uint16_t adc_result = 0;
|
// Channel index: 0 → A2 (sensor 0), 1 → A3 (sensor 1), 2 → A0 (raw ref)
|
||||||
volatile bool adc_ready = false;
|
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() {
|
void setupADC() {
|
||||||
// Reference = AVCC (5 V)
|
ADMUX = (1 << REFS0) | adc_mux[0]; // AVCC ref, start on A2
|
||||||
ADMUX = (1 << REFS0);
|
ADCSRA = (1 << ADEN) | (1 << ADIE) | (1 << ADPS2); // /16 prescaler
|
||||||
|
|
||||||
// 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
|
|
||||||
ADCSRA |= (1 << ADSC);
|
ADCSRA |= (1 << ADSC);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── OOR digital input ────────────────────────────────────────
|
// ── OOR digital inputs ───────────────────────────────────────
|
||||||
#define OOR_PIN 2 // digital pin 2 — HIGH = out of range
|
#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) {
|
ISR(ADC_vect) {
|
||||||
uint16_t sample = ADC;
|
uint16_t sample = ADC;
|
||||||
// Discard if OOR pin (PD2) is HIGH
|
uint8_t ch = adc_channel;
|
||||||
OOR = (digitalRead(OOR_PIN));
|
uint8_t next = (ch + 1) % 3;
|
||||||
if (!OOR) {
|
|
||||||
adc_result = sample;
|
if (ch < 2) {
|
||||||
adc_ready = true;
|
// 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 → mm linear mappings (raw range: 16–26 mm) ──────────
|
||||||
// ADC 185 → 16 mm, ADC 900 → 26 mm
|
#define adcToMM0(adc) ((float)map(adc, 178, 895, 1600, 2600) / 100.0f)
|
||||||
// #define adcToMM(adc) (16.0f + (float)((adc) - 185) * (10.0f / 715.0f))
|
#define adcToMM1(adc) ((float)map(adc, 176, 885, 1600, 2600) / 100.0f)
|
||||||
#define adcToMM(adc) (0.0f + (float)((adc) - 0) * (10.0f / 1024.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
|
#define TRACK_N 10
|
||||||
uint16_t lowestVals[TRACK_N];
|
uint16_t lowestVals[2][TRACK_N];
|
||||||
uint16_t highestVals[TRACK_N];
|
uint16_t highestVals[2][TRACK_N];
|
||||||
uint8_t lowestCount = 0;
|
uint8_t lowestCount[2] = {0, 0};
|
||||||
uint8_t highestCount = 0;
|
uint8_t highestCount[2] = {0, 0};
|
||||||
|
|
||||||
// Insert val into a sorted-ascending array of up to TRACK_N entries
|
static void trackLowest(uint8_t s, uint16_t val) {
|
||||||
// keeping only the N smallest values seen so far.
|
uint16_t *lv = lowestVals[s];
|
||||||
static void trackLowest(uint16_t val) {
|
uint8_t &lc = lowestCount[s];
|
||||||
if (lowestCount < TRACK_N) {
|
if (lc < TRACK_N) {
|
||||||
// Array not full — insert in sorted position
|
uint8_t i = lc;
|
||||||
uint8_t i = lowestCount;
|
while (i > 0 && lv[i - 1] > val) { lv[i] = lv[i - 1]; i--; }
|
||||||
while (i > 0 && lowestVals[i - 1] > val) {
|
lv[i] = val;
|
||||||
lowestVals[i] = lowestVals[i - 1];
|
lc++;
|
||||||
i--;
|
} else if (val < lv[TRACK_N - 1]) {
|
||||||
}
|
|
||||||
lowestVals[i] = val;
|
|
||||||
lowestCount++;
|
|
||||||
} else if (val < lowestVals[TRACK_N - 1]) {
|
|
||||||
// Replace the current largest of the "lowest" set
|
|
||||||
uint8_t i = TRACK_N - 1;
|
uint8_t i = TRACK_N - 1;
|
||||||
while (i > 0 && lowestVals[i - 1] > val) {
|
while (i > 0 && lv[i - 1] > val) { lv[i] = lv[i - 1]; i--; }
|
||||||
lowestVals[i] = lowestVals[i - 1];
|
lv[i] = val;
|
||||||
i--;
|
|
||||||
}
|
|
||||||
lowestVals[i] = val;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert val into a sorted-descending array of up to TRACK_N entries
|
static void trackHighest(uint8_t s, uint16_t val) {
|
||||||
// keeping only the N largest values seen so far.
|
uint16_t *hv = highestVals[s];
|
||||||
static void trackHighest(uint16_t val) {
|
uint8_t &hc = highestCount[s];
|
||||||
if (highestCount < TRACK_N) {
|
if (hc < TRACK_N) {
|
||||||
uint8_t i = highestCount;
|
uint8_t i = hc;
|
||||||
while (i > 0 && highestVals[i - 1] < val) {
|
while (i > 0 && hv[i - 1] < val) { hv[i] = hv[i - 1]; i--; }
|
||||||
highestVals[i] = highestVals[i - 1];
|
hv[i] = val;
|
||||||
i--;
|
hc++;
|
||||||
}
|
} else if (val > hv[TRACK_N - 1]) {
|
||||||
highestVals[i] = val;
|
|
||||||
highestCount++;
|
|
||||||
} else if (val > highestVals[TRACK_N - 1]) {
|
|
||||||
uint8_t i = TRACK_N - 1;
|
uint8_t i = TRACK_N - 1;
|
||||||
while (i > 0 && highestVals[i - 1] < val) {
|
while (i > 0 && hv[i - 1] < val) { hv[i] = hv[i - 1]; i--; }
|
||||||
highestVals[i] = highestVals[i - 1];
|
hv[i] = val;
|
||||||
i--;
|
|
||||||
}
|
|
||||||
highestVals[i] = val;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void resetTracking() {
|
static void resetTracking() {
|
||||||
lowestCount = 0;
|
lowestCount[0] = highestCount[0] = 0;
|
||||||
highestCount = 0;
|
lowestCount[1] = highestCount[1] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void printBoundaries() {
|
static void printBoundaries() {
|
||||||
Serial.println(F("--- 10 Lowest In-Range ADC Values ---"));
|
for (uint8_t s = 0; s < 2; s++) {
|
||||||
for (uint8_t i = 0; i < lowestCount; i++) {
|
Serial.print(F("--- Sensor "));
|
||||||
Serial.println(lowestVals[i]);
|
Serial.print(s);
|
||||||
}
|
Serial.println(F(": 10 Lowest In-Range ADC Values ---"));
|
||||||
Serial.println(F("--- 10 Highest In-Range ADC Values ---"));
|
for (uint8_t i = 0; i < lowestCount[s]; i++) Serial.println(lowestVals[s][i]);
|
||||||
for (uint8_t i = 0; i < highestCount; i++) {
|
Serial.print(F("--- Sensor "));
|
||||||
Serial.println(highestVals[i]);
|
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() {
|
void setup() {
|
||||||
Serial.begin(2000000);
|
Serial.begin(115200);
|
||||||
pinMode(OOR_PIN, INPUT);
|
pinMode(OOR_PIN_0, INPUT);
|
||||||
|
pinMode(OOR_PIN_1, INPUT);
|
||||||
setupADC();
|
setupADC();
|
||||||
Serial.println(F("Send '1' to start sampling, '0' to stop and print bounds, '2' for raw ADC output."));
|
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 ────────────────────────────────────
|
// ── Main sample path ────────────────────────────────────
|
||||||
if (!sampling) return;
|
if (!sampling) return;
|
||||||
|
|
||||||
// Grab the latest ADC value atomically
|
uint16_t val[3];
|
||||||
uint16_t val;
|
bool ready[3];
|
||||||
bool ready;
|
|
||||||
bool newOOR;
|
|
||||||
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
|
||||||
ready = adc_ready;
|
for (uint8_t i = 0; i < 3; i++) {
|
||||||
val = adc_result;
|
ready[i] = adc_ready[i];
|
||||||
adc_ready = false;
|
val[i] = adc_result[i];
|
||||||
newOOR = OOR;
|
adc_ready[i] = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ready) return; // nothing new — come back next iteration
|
if (!ready[0] && !ready[1]) return;
|
||||||
|
|
||||||
if (rawMode) {
|
if (rawMode) {
|
||||||
long mm_x100 = map(val, 178, 895, 1600, 2600);
|
if (ready[0]) {
|
||||||
Serial.print(mm_x100 / 100);
|
Serial.print(adcToMM0(val[0]) - OFFSET_MM0);
|
||||||
Serial.print('.');
|
Serial.print(F(", "));
|
||||||
long frac = mm_x100 % 100;
|
Serial.println(val[2]);
|
||||||
if (frac < 10) Serial.print('0');
|
}
|
||||||
Serial.println(frac);
|
if (ready[1]) {
|
||||||
|
Serial.print(adcToMM1(val[1]) - OFFSET_MM1);
|
||||||
|
Serial.print(F(", "));
|
||||||
|
Serial.println(val[2]);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// All values here are in-range (OOR filtered in ISR)
|
// Apply offset for whichever sensor(s) are in range
|
||||||
trackLowest(val);
|
if (ready[0]) {
|
||||||
trackHighest(val);
|
float mm = adcToMM0(val[0]) - OFFSET_MM0;
|
||||||
|
trackLowest(0, val[0]);
|
||||||
float mm = adcToMM(val);
|
trackHighest(0, val[0]);
|
||||||
Serial.print(val);
|
Serial.print(val[0]);
|
||||||
Serial.print(", ");
|
Serial.print(F(", "));
|
||||||
Serial.print(mm, 2);
|
Serial.print(mm, 2);
|
||||||
Serial.print(" mm, ");
|
Serial.println(F(" mm (s0)"));
|
||||||
Serial.println(newOOR ? "out of range" : "in range");
|
}
|
||||||
|
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)"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user