484 lines
18 KiB
Plaintext
484 lines
18 KiB
Plaintext
|
|
{
|
|||
|
|
"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
|
|||
|
|
}
|