ControlUnit — Software & Laboratory-System Archaeology¶
Second-pass reconciled snapshot, 2026-05-22, against
thread-fix-v2at the tip of 347 commits (Feb 2020 → Apr 2026). The first pass was a blind archaeological reading; this version folds in first-hand developer corrections inline, preserves the original wrong inferences where they reveal real ambiguity in the codebase, and treats the developer's own voice as part of the historical record.
About this document¶
This is an archaeological reading of the ControlUnit repository:
a Raspberry-Pi-based plasma-experiment control and data-acquisition
system that has accreted over six years of active research and
teaching use. It is not a code review and not a roadmap. It is a
forensic reconstruction, then partially un-done by the person who
actually wrote the code.
The document is deliberately layered:
- The base layer is the original first-pass archaeology — what the codebase looks like to someone who has never met the developer.
- Developer Note callouts integrate first-hand corrections.
- Historical Evolution callouts trace lineage back to projects
that predate this one (notably the
EchellePyQtGraph application). - First-pass inference (preserved) notes flag places where the blind reading was wrong but the codebase invited the wrong reading — those are evidence of real ambiguity worth keeping.
Confidence legend¶
Inline epistemic tags are still used where status is non-obvious:
- [confirmed] — directly visible in code, commits, or file listings, or now confirmed by the developer.
- [inferred] — strongly implied by multiple converging pieces of evidence but not stated anywhere.
- [corrected] — initially inferred wrongly; the developer's correction has been folded in.
- [speculative] — still a working interpretation, plausible but not proven.
The final section, Known uncertainties and possible incorrect assumptions, is now much shorter — most uncertainties have been resolved by the developer's pass.
Executive Summary¶
ControlUnit is a Raspberry-Pi-based laboratory instrument written,
extended, taught with, broken, rewritten, and lightly LLM-assisted by
one principal developer (Arseniy / queezz) over six years, with
contributions from students and lab-mates passing through. It is
both software and infrastructure: the box exists, has a front
panel, sits in a multi-chamber plasma lab, and is used today.
What it does today [confirmed by developer]:
- Logs plasma parameters and vacuum from one I²C ADC.
- Controls two MFCs (H₂ and O₂) via a DAC8532.
- Controls plasma current via a galvanically isolated MCP4725 DAC through a feedback PID.
- Synchronises external loggers (QMS and others) via a GPIO sync signal that also drives a front-panel LED.
What it has done in the past but does not do today:
- Closed-loop membrane temperature control via MAX6675 + SSR-driven halogen lamp. Migrated to a Windows machine with National Instruments hardware. The code path is dormant, not deleted.
What it was originally for [corrected]:
- A dedicated hydrogen-permeation rig studying the effect of oxygen
on hydrogen transport through metals under plasma irradiation.
The lab has since grown around it; the experiment is gone, the
instrument is repurposed, the name
ControlUnitis still apt.
Developer Note (Arseniy). "I was exploring and learning. My goal with this RasPi unit was to learn electronics. And I think I've succeeded. And my students probably learned a bit or two."
The codebase reads like a strata: a 2020 monolithic worker.py
copied from an earlier project (Echelle) still imprints the
structure, even after a 2024 split into devices/, ui/, and a
worker base class.
Historical Evolution: from
Echelle. The original threading and PyQtGraph dock layout were ported fromEchelle, a spectrograph-control application Arseniy wrote beforeControlUnit. The_echelle_basevariable incontrolunit/__init__.pyis a literal fossil — the sys.path-append idiom and the earlyWorkerclass were template-copies. A B4 student (Ito-kun, also known by the commit name "Tatsuemon") extended that template — see Threading Evolution below.
Three forces shape the codebase [partially corrected]:
- Hardware reality. The rig actually exists, so channels, addresses, and conversion polynomials are pinned to physical sensors with datasheet URLs in comments.
- Multiple contributors with different conventions. Identified below in the Contributor Key.
- Solo-physicist tempo with breaks for development. Not measurement-campaign-aligned but the opposite — long quiet periods while experiments run, intense development pushes during experimental downtime.
Developer Note (Arseniy). "Development is usually NOT tied to actual experiments. More on the opposite side. Accumulated necessity is implemented when there is a break in experiments and there is time."
The package compiles and runs. It is not abandoned — the latest
commit is from one month ago (settings: debug false). It carries
the scars of three half-completed renames, one half-extracted core,
one stale auto-generated doc site, and several modules that exist as
placeholders for "do this later" intentions that the developer is
honest about.
Contributor key [corrected]¶
This is the corrected mapping of git authors to humans.
queezz/ Arseniy — principal developer. Wrote and rewrote most of the system. Also the unifying voice across all phases.- Ito-kun, B4 student. Commits authored as
Tatsuemon. Liked C. Built the early multi-threaded ADC reading on top of Arseniy's Echelle template. Went to an AI-development master's course after graduating. His threading model (re-opening I²C per channel read and using anEnumto dispatch device types) was the "mess" later untangled. - Miura-kun, lab member. Commits authored as
leprecon-piandpi <hasuo_kuzmin.lab@outlook.jp>— the latter directly from the lab Raspberry Pi. His pass through the codebase coincided with Arseniy's first serious effort to migrate from numpy arrays to pandas DataFrames. - Kurokawa-kun (git handle
Kshora). Lab contributor. PRs #1, #18, #19 — early MFC and MCP4725 work. Also leftkurokawa-dev/PWR.py— a quarantined SCPI driver for a Kikusui PWR-401L bench supply — never integrated intoControlUnit. - Kawabata-kun. Came after Kurokawa. Wrote the final
plasma-current PID loop and helped land the isolated-DAC
integration in April 2026 (
kawdevbranch). codex/*automated bot. Two LLM-authored PRs in August 2025 (#20, #21). Arseniy's first experiment with LLM-assisted design; not tested on the rig at the time.
Developer Note (Arseniy). "When Miura-kun was here I thought about transitioning to pandas for sanity, but I was confused by the Qt thread mess. So it took me a while to finish this. Exactly the kind of LLM job. But I think I've learned a lot. Well, got exposure to classes and signals. Bad code, but real. After this I actually started using classes. I finally got what they are: basically a box, a drawer. So you don't spill and lose your functions."
System Status Inventory¶
A category-tagged inventory of what is implemented, planned, experimental, dormant, or migrated in the current tip.
Implemented and live¶
- ADC acquisition via I²C (
devices/adc.py+devices/adc_setter.py). Channels driven bysettings.yml. 10 Hz default; dynamic STEP batching. - MFC control via DAC8532 (
devices/dac8532.py). - Plasma-current PID via MCP4725 (
devices/mcp4725.py,devices/adc.py: plasma_current_control). Usessimple_pid. - CSV data logging (self-describing header) and HTML event log.
- pyqtgraph dual-pane plot (plasma current + pressures), live.
- Per-host versioned settings override (
~/.controlunit/settings.yml→ falls through to the in-reposettings.yml). - Galvanically isolated MCP4725 hardware path (April 2026).
- LED + sync signal on shared GPIO for external loggers (QMS and others).
Planned but not yet realised¶
core_logic.py— intended extraction of cross-worker signal wiring frommain.py. Class defined, never instantiated.plot_data_handler.py— intended extraction of plot-update methods frommain.py. Class defined, never instantiated, duplicates code still living inmain.py.- Tagged versioning (
v0.4.0is in_version.pybut no git tags exist; the developer wants this but has no system for it yet). - ESP32-based dedicated heater controller reporting to an orchestrator — the developer's intended successor for the membrane-temperature path. Not implemented in this repo.
- "Network of instruments" model — see the Engineering Mindset section.
Experimental branches (live in git, off the main line)¶
kawdev— Kawabata's plasma-current development branch.thread-fix-v2— current development tip; isolated DAC + settings layout cleanup + Ip-PID OFF button.dev— long-lived integration branch.
Dormant capabilities (alive in code, currently disabled)¶
- Membrane-temperature control (
heatercontrol.py,devices/max6675.py,MAX6675.acquisition_loop,MAX6675.temperature_control). Imported but not started, because theMembraneTemperatureentry inMainApp.define_devicesis commented out. - MFC calibration sweep (
DAC8532.do_calibration) — wired in code, has a dock UI, used historically, currently not in active use. - Cathode current and voltage logging channels (
Ci,Cvinsettings.yml) — channels are configured and conversion functions exist, but the cathode current has never actually been measured [corrected] — only prepared in the ADC channel map and on the PCB.
Developer Note (Arseniy) on cathode current. "Never measured cathode current. Wanted to, prepped. But that is not that crucial."
- 7" front-panel touchscreen [corrected]. The physical screen is still on the box, but the touch interface is disabled — it was never well calibrated. Day-to-day operation is via VNC or a Bluetooth mouse plugged into the USB port exposed on the chassis.
analoggauge.pywidget (32 KB, LabVIEW-style analog gauge). Was used briefly to display membrane temperature; abandoned both because the temperature path moved to Windows / NI and because the widget was too large for the 7" screen [corrected].
Developer Note (Arseniy) on the touchscreen. "Yes, touchscreen. But I disabled the touch. Because it is terrible, not well calibrated. I keep the display, it's always nice to have a display. Now it's only for show. I usually use VNC. Or a Bluetooth mouse sometimes. I have a USB port exposed on the box."
Developer Note (Arseniy) on
analoggauge.py. "I used that widget. I wanted a LabVIEW-like appearance. Stopped because temp control moved out, and that widget wasn't ideal. Too big for the tiny screen. And in my workflow I still want the GUI readable on the 7" screen. Just because."
Migrated out of this repo¶
- Membrane-temperature reading and PID control — moved to a Windows machine with National Instruments hardware. Not coming back to the Pi; if it returns, it will be a separate ESP32 unit.
Developer Note (Arseniy). "Yes, moved to NI on a Windows for now. Forever 🙂. And even if I would come back to it, I would make a dedicated ESP32 unit which would report to an orchestrator. I've evolved my understanding of a lab instrument. Into a network."
kurokawa-dev/PWR.py(SCPI driver for a Kikusui PWR-401L bench supply [confirmed by developer]) — Kurokawa's quarantined experiment, predating Kawabata's plasma-current work. Never integrated.
Repository Structure¶
Top-level layout (alive vs fossilised, updated with category tags):
ControlUnit/
├── controlunit/ # the actual Python package
│ ├── __init__.py # sys.path hack — alive but a smell;
│ │ # _echelle_base is a literal fossil
│ ├── _version.py # 0.4.0, Apache-licensed; informal versioning
│ ├── main.py # MainApp(QObject, UIWindow), 785 lines — alive
│ ├── mainView.py # UIWindow mixin — alive
│ ├── settings.yml # ALIVE — de-facto schema AND deliberate doc
│ ├── readsettings.py # ALIVE — YAML loader + AdcChannelProps wiring
│ ├── heatercontrol.py # DORMANT — temperature path is disabled
│ ├── trigger_signal.py # ALIVE — IndicatorLED + GPIO sync signal
│ ├── striphtmltags.py # ALIVE — one-purpose helper
│ ├── start_gpio.py # ALIVE — sudo pigpiod orchestration
│ ├── core_logic.py # PLANNED stub: never instantiated
│ ├── plot_data_handler.py # PLANNED stub: never instantiated
│ ├── devices/ # HAL — renamed from sensors/ on 2024-09-25
│ │ ├── device.py # DeviceThread base class (worker super)
│ │ ├── adc.py / adc_setter.py
│ │ ├── dac8532.py / dac8532_setter.py
│ │ ├── mcp4725.py / mcp4725_setter.py
│ │ ├── max6675.py # DORMANT — currently not started
│ │ ├── adc_channels.py # AdcChannelProps + conversion dispatch
│ │ ├── conversions.py # gauge polynomials with LaTeX in docstrings
│ │ └── dummy.py # off-RasPi shims for smbus, RPi.GPIO, pigpio, spidev
│ ├── ui/ # renamed from components/ on 2024-09-25
│ │ ├── text_shortcuts.py # ANSI + Unicode constants
│ │ ├── docks/ # pyqtgraph DockArea panels
│ │ ├── widgets/ # graph.py — ALIVE; analoggauge.py — DORMANT
│ │ └── buttons/toggles.py # hand-painted QPushButton switches
│ └── icons/
├── docs/ # FOSSIL: pdoc3 HTML from 2022 + Archaeology.md (this file)
├── examples/ # mostly fossilised notebooks; settings_template.yml = snapshot
├── manuals/ # one PDF (ads1113.pdf) — minimal in-repo reference
├── images/ # README screenshots + photos of the physical box
├── tests/ # one example test + one Codex-added test
├── kurokawa-dev/PWR.py # QUARANTINED (Kurokawa): serial driver for PWR401L
├── bin/plasmacontrol # 7-line entry-point script (BROKEN: imports MainWidget)
├── .github/workflows/ # one CI: flake8 syntax + example pytest
├── README.md, requirements.txt, setup.py, settings.yml, .pre-commit-config.yaml
└── data/test/ # empty placeholder
Telling artifacts [confirmed by reading the respective files]:
setup.pystill declarespackages=["controlunit", "controlunit.components"]— thecomponents→uirename (commitc00e7fd, 2024-09-25) was never carried into install metadata.controlunit/__init__.pydoessys.path.append(str(_echelle_base)). This is what letsfrom mainView import ...work without a package prefix. The variable name_echelle_baseis a literal artifact of having template-copied fromEchelle.
Historical Evolution: the
_echelle_basename. [corrected] "Echelle remnant. I made Echelle BEFORE this, using PyQtGraph. I used that as a template. Wow, the remnant survived. Well, because it doesn't break things, and I have better things to do than go Qt diving."
- Triple inconsistent import styles for the same module exist
side-by-side:
from controlunit.devices.adc import ADC(inmain.py),from .device import DeviceThread(indevices/adc.py), andfrom devices.dummy import pigpio(fallback when not on the Pi). All three work only because of the sys.path hack. bin/plasmacontrolwould crash today — it doesfrom controlunit import *; widget = MainWidget(app), butMainWidgetwas renamed toMainAppin commit9907825 restructuring main.py(2024-10).
Documentation Archaeology¶
There are four parallel documentation strata in this project. Only two of them live inside the repo.
1. docs/ — auto-generated pdoc3 HTML (FOSSIL, 2022-06-28)¶
Created in a single commit 4b7dcdc [REORGANIZE] make a package, add
pdoc3 docs and never regenerated. It serves the GitHub Pages site
linked from the README at https://queezz.github.io/ControlUnit/.
The HTML references modules that have since been deleted —
AIO.html, customTypes.html, electricCurrent.html,
ionizationGauge.html, pfeiffer.html, pigpioplug.html,
qmsSignal.html, smbusplug.html, tc.html, thermocouple.html,
timeKeeper.html, worker.html. There is also a self-nested
duplicate docs/components/components/... from what appears to be a
buggy pdoc invocation [speculative].
The generator itself lives in docs/make.py:
# docs/make.py (lines 11–31)
def generatedocs():
""" """
import os, subprocess, shutil, time
# subprocess.check_output('pdoc controlunit -o ./docs/ --html --force')
bpth = "./docs/controlunit"
npth = "./docs/"
clear(bpth)
subprocess.check_output(f"pdoc controlunit -o ./docs/ --html --force")
Status: stale but valuable as a fossil snapshot of the codebase
circa 2022 (single monolithic worker.py, separate customTypes.py,
etc.). For the MkDocs migration, archive as docs/archive/2022-pdoc/
and add a rename map.
2. controlunit/settings.yml — deliberate in-code data dictionary¶
This is the only place inside the repo where the meaning of a signal
name like Pu, Bd, Ip, MFC1 is bound to a channel number, gain,
description, and conversion function. It is also where datasheet URLs
are pinned:
# controlunit/settings.yml (lines 44–50)
# 32 channel 16-Bit ADC based on ADS1115
# https://www.y2c.co.jp/i2c-r/aio-32-0ra-irc/
# https://www.ti.com/lit/ds/symlink/ads1113.pdf
# https://www.nxp.com/docs/en/data-sheet/PCA9554B_PCA9554C.pdf
# https://www.ti.com/lit/ds/symlink/cd74hc4067.pdf
Developer Note (Arseniy) — this was intentional documentation. "I did that to not remember the modules. So I DID DOCUMENT THERE INTENTIONALLY. After years of on-and-off dev."
This is the most important single piece of documentation in the
repository. Treat settings.yml as canonical and generate a
"Channel Map / Conversion Catalogue" page in MkDocs from it.
3. controlunit/devices/conversions.py — LaTeX-in-docstrings¶
Each conversion function carries its physics inline:
# controlunit/devices/conversions.py (lines 7–19)
def pfeiffer_single_gauge(voltage):
r"""
Calculate pressure for Pfeiffer single gauge PKR251.
$Pressure = 10^{1.667 * voltage -11.46}\; Torr$
...
"""
Developer Note (Arseniy) — conversions are from datasheets. "All conversions are based on docs. And they are fine. I might want to improve current stuff, but that's for later."
Render with pymdownx.arithmatex in MkDocs and you get a real
"Sensor Conversion Reference" section for free.
4. aklab-howto — the external documentation hub¶
The hardware-level documentation for this rig lives outside the repository, in the developer's lab-wide how-to repo: https://github.com/queezz/aklab-howto. The directly relevant pages are:
- Control Unit overview
- High-Precision AD/DA Board
- Y-Corp ADC board
- FT232H USB GPIO — the moved-to-Windows-NI replacement
Developer Note (Arseniy). "I have some of my module descriptions in
aklab-howto. That's the hub repo."
When MkDocs is set up here, link to aklab-howto, do not duplicate
it. The hardware story belongs at lab level; this repo's docs
should be about the software that uses that hardware.
5. manuals/ads1113.pdf — one orphan datasheet¶
A single PDF sits alone. The remaining datasheets (PKR251, MKS 627/8,
Celduc SO842074, MAX6675, etc.) live in aklab-howto or are linked
from the README. Either centralise into docs/datasheets/ or remove
this folder entirely.
6. examples/ notebooks — abandoned¶
imports.ipynb: a fossil from when this package was pip-installed ('c:\\…\\miniconda3\\lib\\site-packages\\controlunit'). Useless today.channels.ipynb,pandasrecap.ipynb,settings.ipynb: scratch notebooks for working out the ADC channel multiplexing and the move from numpy arrays to pandas DataFrames in 2023.settings_template.yml: a snapshot ofcontrolunit/settings.ymlat v1.0 — useful only to see the diff (no MFCs, no cathode current, channels still numbered 0–4 instead of 20-something).
What carries durable design intent¶
- The
# ===banner blocks insettings.yml— the developer's memory aid, written deliberately as in-code documentation. - The named-conversion dispatch in
adc_channels.pyandconversions.py. - README's BOM and control-box photos.
- The pdoc HTML for
docs/main.html,docs/worker.html,docs/customTypes.htmlshould be kept as historical context — they document the 2022 ThreadType-enum architecture. - The
aklab-howtoexternal hub for hardware.
What is safe to drop¶
docs/components/components/*— duplicate pdoc output.examples/imports.ipynb— exposes a developer's local paths.- The
[DOCS](https://queezz.github.io/ControlUnit/)link in README until MkDocs is deployed — it currently points to a stale doc site.
Architecture Reconstruction¶
Layered structure (as of today, [confirmed])¶
┌──────────────────────────────────────────────────────────────────┐
│ GUI (main thread) │
│ main.MainApp(QObject, UIWindow) │
│ UIWindow (mainView.py) ─► ui/docks/* + ui/widgets/graph.py │
└──────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
│ │ Qt signals (queued) │ direct method
│ │ data_ready, sigDone, msg │ calls into
│ │ │ ui widgets
┌───────────────┴───┴────────────────────────────────┴─────────────┐
│ Workers (one QThread per device, parented by MainApp.workers) │
│ devices.device.DeviceThread (super) │
│ ├── ADC (devices/adc.py) ◄── simple_pid │
│ ├── DAC8532 (devices/dac8532.py) ── 2 MFC channels │
│ ├── MCP4725 (devices/mcp4725.py) ── plasma-current DAC │
│ └── MAX6675 (devices/max6675.py) ── currently dormant │
└──────────────────────────────────────────────────────────────────┘
▲
│ register-level I/O via *_setter.py
▼
┌──────────────────────────────────────────────────────────────────┐
│ Chip drivers (synchronous, no Qt) │
│ AIO_32_0RA_IRC + ADS1115 + PCA9554 (adc_setter.py) │
│ DAC8532Setter (dac8532_setter.py) │
│ MCP4725Setter (mcp4725_setter.py) │
└──────────────────────────────────────────────────────────────────┘
▲
│ smbus, spidev, pigpio (or devices/dummy.py off-Pi)
▼
Raspberry Pi 4 (8 GB) + custom PCB
— originally Raspberry Pi 3B
Historical Evolution: hardware upgrade. [corrected] The original platform was a Raspberry Pi 3B (as still mentioned in the README). It has since been replaced with a Raspberry Pi 4 8 GB. None of the code references the model; the upgrade was binary-compatible and motivated by RAM headroom for pandas buffers and pyqtgraph rendering.
Threading evolution (the deepest stratum)¶
This is the most interesting story in the repo. The first-pass archaeology got the shape of the evolution right but mis-attributed some of the authorship. The corrected story:
Phase 0 (pre-history). The Echelle template.
Historical Evolution: pre-
ControlUnit. [corrected] The originalWorker(QtCore.QObject)class shape, theThreadTypeenum dispatch, theSTEP-batched numpy buffers, theapp.processEvents()from inside the worker, and thesys.path.appendpackage hack were all ported from Arseniy's earlier project,Echelle— a PyQtGraph spectrograph-control application. Ito-kun (Tatsuemon) inherited that template; he did not invent the Qt-threading shape, he extended it.
Phase 1 (Feb 2020), pre-package, monolithic worker.py.
The initial commit (710c78f) uses one Worker(QtCore.QObject)
class for all devices, dispatched by a ThreadType enum. Methods are
named __plotPresCur and __plotT. Buffers are fixed-shape numpy
arrays of STEP rows. The worker calls self.__app.processEvents()
from a non-main thread — anti-pattern, inherited from Echelle.
Phase 1, the Ito-kun extension. [corrected] Ito-kun was a B4 student who "liked C" [developer wording]. He built the device-reading logic on top of Arseniy's Echelle-port template. Two specific decisions of his became technical debt:
- He opened a fresh I²C connection to the ADC on every channel read. On a multi-channel scan loop this hurt acquisition throughput badly.
- He extended the
ThreadTypeenum to dispatch behaviour for each physical device — so the code path for "read ADC" lived inside a case of "what kind of thread am I?" rather than as its own object. This made it impossible to see what concrete device any given line of code actually talks to.
Ito-kun left the lab for an AI-development master's course shortly after his B4. The code stayed behind.
Phase 1.5 (Mar 2020), "Untangling Ito-kun's threading mess"
(ed7cadb). Single commit, +161 / −107 in worker.py. The commit
is authored as queezz but the help came from another lab member
[corrected]. The fix: rename ThreadType → Signals, factor
read_settings() out of every constructor, rename methods to
readADC / readT, give the worker a per-instance self.sampling.
The threading topology did not yet change.
Phase 2 (Jun 2022), [REORGANIZE] make a package, add pdoc3 docs
(4b7dcdc). Files moved into a controlunit/ directory. No
structural change. pdoc generated HTML for the then-current shape.
Phase 3 (May 2023), ADC-thread tuning (worker: tuning ADC thread
× 8 commits). The acquisition loop was rewritten to read from
AdcChannelProps populated from settings.yml rather than from
hard-coded CHP1/CHP2/CHIP constants; numpy arrays were replaced
with pandas DataFrames; and the STEP batching idea (loop N times,
then emit one signal carrying N rows) was kept and clarified.
The STEP mechanism serves two purposes simultaneously
[confirmed by developer]: it averages noisy ADC samples and it
amortises Qt signal-emission overhead between the worker thread and
the GUI thread. The fact that one parameter does both jobs is why it
has never needed to change:
# controlunit/devices/adc.py (lines 374–379)
if step % (self.STEP - 1) == 0 and step != 0:
# self.calculate_averaged_signals()
self.send_processed_data_to_main_thread()
step = 0
else:
step += 1
Phase 4 (Aug 2024), restructure: worker super class and subclasses
are separated (5326e50). Big-bang split: 662 lines deleted from
the monolithic controlunit/worker.py, replaced with
sensors/{worker.py, worker_adc.py, worker_dac8532.py,
worker_max6675.py, worker_mcp4725.py}. This commit's author field is
pi <hasuo_kuzmin.lab@outlook.jp> — i.e., committed directly from
the lab Raspberry Pi, under Miura-kun's lab account [corrected].
Developer Note (Arseniy) on Phase 4. "When Miura-kun was here I thought about transitioning to pandas for sanity. But I was confused by the Qt thread mess. So it took me a while to finish this. Exactly the kind of LLM job. But I think I've learned a lot. Got exposure to classes and signals. Bad, but real. After this I started actually using classes. I finally got what they are: basically a box, a drawer. So you don't spill and lose your functions."
That last sentence is — verbatim — the moment when this codebase stopped being a Pythonic script and started being a Python program.
Phase 4.5 (Sep 2024), serial renames.
4050c28 Renaming, fix FutureError in pandas.concat:worker_*.py→adc.py/dac8532.py/ etc.a2f50d0 Renaming sensors to devices: directory rename.c00e7fd Rename components to ui: directory rename.b1e6fe5 rename remaining "sensor_name"s to "device_descriptor": terminology.1c426d9 Restructure thread start+86544db streamlined threads and workers start+9907825 restructuring main.py.
Developer Note (Arseniy). "Ah, yes — I finally got up to it, improving sanity again."
A 10-day burst of renaming. Behaviour was untouched; vocabulary became consistent.
Phase 5 (Aug 2025), Codex-bot fixes (PRs #20 and #21).
1cf4611 Handle sensor data in device threads: movedupdate_processed_signals_dataframeout ofmain.pyinto the workers (adc.py,max6675.py).9f5b04c Fix CoreLogic signal declaration: cleaned the (still-unused)core_logic.pystub and addedtests/test_core_logic_signal.py.
Developer Note (Arseniy) on LLM workflow evolution. "Codex PRs — my first test of LLM-assisted design. Was purely software, not tested. After that I went full Cursor on that, and direct tests on RasPi. Just this year, 2026. The Codex PR workflow was bad."
This is meaningful: Phase 5's commits were never tested on the hardware at the time they were merged. They look correct, they are correct enough to not have broken anything, but the developer explicitly considers them an experiment in workflow rather than trusted code. Phase 6 has been different — Cursor-assisted with direct on-rig testing.
Phase 6 (Apr 2026), isolation hardware push. 0b417cf add debug
for mcp4725, isolated DAC for plasma current + dfbc65c upd for
isolated DAC control of the plasma current. The current branch
(thread-fix-v2) has these on top of dev. Kawabata-kun's plasma
PID work landed alongside this [corrected]. The isolation was
critical for plasma-current stability.
Developer Note (Arseniy) on Phase 6. "Kawabata-kun did the final plasma current PID loop. I made and tested one before isolation. Isolation was critical, of course."
GUI architecture (mainView + docks)¶
The GUI is a QMainWindow containing a QTabWidget with two tabs:
- Data tab — a pyqtgraph
DockAreawithControlDock,Plots(aGraph(GraphicsLayoutWidget)with stacked plasma and pressure plots; the temperature plot is commented out everywhere),GasFlowDock(two MFCs with per-decade spinboxes — a "scientific-notation-by-spinbox" UI),CalibrationDock,PlasmaCurrentDock,PlotScaleDock. - Settings tab —
SettingsDock,ADCGaindock,LogDock.
First-pass inference (preserved as ambiguity-witness). The first pass read the hand-painted toggle switches (
MySwitch,OnOffSwitch,QmsSwitch, ...) and the enlargedhitButtonrectangles as evidence of a touchscreen-first design philosophy.Developer Note (Arseniy) — partly right, currently wrong. "Yes, touchscreen — but I disabled the touch. It's terrible, not well calibrated. I keep the display, it's always nice to have a display. Now it's only for show. I usually use VNC. Or a Bluetooth mouse sometimes. I have a USB port exposed on the box."
So the toggles were designed for touch; the rig pragmatically downgraded the input modality without removing the UI design. The code is a fossil of a workflow that was abandoned.
The UIWindow class is mixed in via multiple inheritance
(class MainApp(QtCore.QObject, UIWindow):). Unusual for Qt code,
but a legitimate way to keep the layout code physically separated
from the controller code without giving up direct attribute access
(self.control_dock, self.graph, etc.). This idiom is also
inherited from Echelle.
Signal / data flow¶
Per acquisition cycle (taking ADC, the busiest one):
ADC.acquisition_loop()runs in its ownQThread.time.sleep(self.sampling_time)(default 0.1 s).collect_data()reads N channels viaaio.analog_read_volt(channel, datarate, gain). The PCA9554 mux is reconfigured only if the channel range demands it.- Raw row appended to
adc_valuesDataFrame, converted row appended toconverted_values. - If a plasma-current setpoint is non-zero:
plasma_current_control()runs asimple_pid.PID(0.3, 0.1, 0)againstIpand emitssend_control_voltagetoMCP4725(via a queued signal handled inmain.py: _set_cathode_current). - Every
STEPticks:send_processed_data_to_main_thread()emitsdata_ready.emit([dataframe, device_name]).MainApp.on_worker_steproutes to_adc_step, appends toself.datadict["ADC"], callssave_data(CSV append), and triggers a plot update.
Worker→worker signalling:
# controlunit/main.py (lines 288–294)
def start_cross_connections(self):
"""Connect workers signals directly"""
mfcs_worker = self.workers["MFCs"]["worker"]
adc_worker = self.workers["ADC"]["worker"]
mfcs_worker.send_presets_to_adc.connect(
adc_worker.update_mfcs, type=QtCore.Qt.DirectConnection
)
The DAC8532 worker tells the ADC worker what voltage it just set so
the ADC can log the commanded preset alongside the measured
signal. DirectConnection runs the slot in the emitter's thread —
correct here because update_mfcs only touches self._mfc_presets.
DAQ and logging¶
Two parallel logging paths, both append-only:
- CSV data file in
~/work/cudata/cu_<YYYYMMDD_HHMMSS>.csvwith a self-describing comment header:
# controlunit/main.py (lines 396–409)
def generate_header_adc(self):
return [
"# Title , Control Unit ADC signals\n",
f"# Date , {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n",
f"# Columns , {', '.join(self.config['ADC Column Names'])}\n",
f"# Signals , {', '.join(self.config['ADC Signal Names'])}\n",
f"# Channels , {', '.join([str(i) for i in self.config['ADC Channel Numbers']])}\n",
"# For converted signals '_c' is added\n",
"#\n",
"# [Data]\n",
]
The header survives pandas.read_csv(..., comment='#') and embeds
enough channel metadata that an old CSV can be replayed without
settings.yml.
- Event log file —
~/work/cudata/controlunit.log, append-only.
PID / control logic¶
Two PID implementations live in the code; only one currently runs.
Plasma current (Ip) PID — implemented and live¶
Uses simple_pid with
p=0.3, i=0.1, d=0 and output_limits=(0, 4500) (mV). Setpoint from
the GUI, feedback from the Hall-effect sensor on channel 0, actuator
is the MCP4725 DAC.
Developer Note (Arseniy) on the baseline of 1000 mV. [corrected — Kawabata-kun's contribution] "This looks like the minimum amps for plasma to maybe start. Again, probably Kawabata."
The
baselineparameter is therefore an empirical offset chosen against the external cathode supply's transfer function — not a derived value.
# controlunit/devices/adc.py (lines 312–319)
def plasma_current_control(self):
"""
PID control plasma current
"""
baseline = 1000 #2000 #mV, corresponds to 16A
output = self.pid((self.plasma_current - self.zero_ip) * 1000)
output = output + baseline
self.set_cathode_current(output)
Membrane heater PID — dormant¶
Hand-rolled in MAX6675.temperature_control(), integral-clamp by
if integral < -0.5: integral = 0, asymmetric (only positive e
drives output). Gains Kp=3.5, Ki=0.06, Kd=0. The output is
on-time fraction of a 10 ms cycle, software-PWMed by
HeaterContol.work() against an SSR.
# controlunit/devices/max6675.py (lines 178–181)
"""
Shouldn't the self.sampling_time here be 0.25, not the one for ADC?
TODO: update to simple-pid as in TemperatureControl
https://github.com/queezz/TemperatureControl
"""
Developer Note (Arseniy) on the heater path. "The heater is moved out currently. But I keep the capability. I won't have time to come back to it. And even if I would, I would make a dedicated ESP32 unit which would report to an orchestrator. I've evolved my understanding of a lab instrument. Into a network. I've made a single TC logger but haven't had a chance to move to proper network instruments yet."
This is the most important philosophical movement in the project: from a monolithic instrument that does everything in one box to a network of small instruments that report to an orchestrator.
Isolation / safety design decisions¶
The isolation strategy as it actually exists:
- GPIO isolation via SSR. The heating circuit was mains 100 V × 300 W controlled through a Celduc SO842074 zero-cross SSR. The Pi never touched mains. [confirmed]
- I²C isolation for the plasma DAC. The MCP4725 sits behind a
galvanic I²C isolator on a separate bus. April 2026 work
(
dfbc65c,0b417cf). Critical because plasma transients on the cathode bus were affecting the rest of the I²C tree. [confirmed by developer] - Software safety on quit.
# controlunit/main.py (lines 316–327)
def turn_off_voltages(self):
"""Safely turn off any DAC voltages"""
if not self.workers:
return
self.workers["ADC"]["worker"].set_plasma_current.emit(0)
self.workers["PlasmaCurrent"]["worker"].output_voltage_signal.emit(0)
self._mfc_presets = {1: 0, 2: 0}
self.update_current_values()
self.workers["MFCs"]["worker"].output_voltage_signal.emit(1, 0)
self.workers["MFCs"]["worker"].output_voltage_signal.emit(2, 0)
abort_all_threads → turn_off_voltages first, then thread
termination. The plasma setpoint goes to zero before threads die.
- The QMS-sync LED is only a sync signal. [corrected]
First-pass inference (preserved as ambiguity-witness). The first pass speculated that the LED + GPIO output was a clever "galvanic isolation by photons" — the QMS receiving the sync via an opto-isolated input watching the LED.
Developer Note (Arseniy) — just wrong. "I send a sync signal for different loggers, that's it."
So GPIO 26 emits a digital sync edge consumed by various external data loggers. The LED is a visual indicator on the front panel, not an isolation strategy. The shared GPIO was always a wiring convenience.
- Software-emulated hardware (
devices/dummy.py). Stubs outpigpio.pi,RPi.GPIO,spidev.SpiDev, andsmbus.SMBusto no-op classes so the GUI boots on Windows.
Development Timeline¶
Reframed as development campaigns during experimental downtime, not measurement-campaign signatures.
2019 (pre-history) Echelle PyQtGraph application written.
Becomes the template for ControlUnit.
2020-02 ── Initial commit (queezz). Echelle-ported Worker + ThreadType.
2020-03 ── "Untangling Ito-kuns therading mess". Renames, structural
clean-up while learning Qt. Black/pre-commit added.
2020-06 ── First numpy → pandas attempt (incomplete; will sit for years).
2020–22 ── Long quiet period. Rig is running, dev is teaching.
2022-06 ── [REORGANIZE] package layout + pdoc3 docs generated.
2023-04 ── Multiple contributors arrive (Kurokawa-kun/`Kshora`, leprecon-pi/Miura-kun).
Measurement-campaign labels in commits ("hasizuka", "B2",
"B1") reflect channels being renamed live during runs.
2023-05 ── ADC channel meta moved into AdcChannelProps + settings.yml.
8 commits "worker: tuning ADC thread" — the buffer redesign.
*Development push during downtime.*
2023-06 ── Logging-to-file, sampling-time-from-GUI, gain switching.
customTypes.py, ionizationGauge.py, pfeiffer.py DELETED
(absorbed into conversions.py).
2024-08 ── Worker super class split (commit by Miura-kun from the Pi).
Pandas migration finally lands.
2024-09 ── MFC integration (DAC8532), MCP4725 addition.
Mass rename: sensors/→devices/, components/→ui/.
*Development push.*
2024-09–10 First plasma-current PID by Arseniy. "Ip PID response
detected (hardware)" — the rig actually closes loop.
2025-08 ── Two Codex automated PRs: data-handling moved INTO workers,
CoreLogic signal fix. *Untested on hardware at merge time.*
2026-04 ── Isolated DAC for plasma current (Kawabata-kun's final PID
integration). Settings layout cleanup, Ip-PID OFF button.
*Cursor-assisted with direct on-rig testing.*
2026-05 ── Latest: "settings: debug false". Lab still active.
Hardware: Raspberry Pi 3B → Pi 4 (8 GB), transparent to software.
Two dev-campaign signatures are clear in commit density: the 2023-05 ADC-tuning burst and the 2024-09 / 2024-10 PID burst.
Developer Note (Arseniy). "Two clear measurement-campaign signatures — no, probably two clear dev campaigns. 'One developer fixing real hardware'. Haha. Me. Arseniy. One developer. But true enough."
Experimental Context¶
The first pass guessed at the physics from the signal list. The developer has now told us directly. This section is now mostly confirmed, with the original inference preserved as historical context.
What the rig was originally for [corrected]¶
A dedicated hydrogen-permeation experiment: the effect of oxygen on hydrogen transport through metals under plasma irradiation. A heated thin metal membrane separated upstream (plasma + gas mixture) from downstream (vacuum + permeation flux). Pressure pairs (Pfeiffer + Baratron, upstream and downstream) read the transport. The QMS analysed downstream species and was time-aligned via the GPIO sync.
What the rig is today [corrected]¶
A multi-purpose plasma laboratory with three chambers:
- A plasma-irradiation chamber.
- A thin-film plasma-deposition chamber.
- A pulsed-laser-deposition (PLD) chamber with laser diagnostics.
The ControlUnit box itself is used as:
- a logger for plasma parameters and vacuum across the lab;
- a basic gas-control panel (MFCs);
- a basic plasma-current controller.
The membrane temperature path is gone (moved to NI on Windows). The
permeation experiment is no longer the primary application. The
name ControlUnit is still apt because the box still controls and
logs the basic gas / plasma stack.
Developer Note (Arseniy). "I measure plasma parameters and vacuum. I control gas (MFCs) and now plasma current. I WAS controlling the membrane temp. It was only a permeation experiment. Investigation of the oxygen effect on hydrogen transport through metals under plasma irradiation. Now it's a multi-purpose device with 3 chambers: plasma irradiation, thin-film plasma deposition, laser diagnostics, and a PLD chamber. But I use this block as a logger and basic gas/plasma control. So 'Control Unit' is still a valid name."
Signal channel map¶
Channel-by-channel confirmation from settings.yml:
Ip— plasma current via Hall sensor (hall_current_sensor,5 * (v - 2.52) A).Pu/Pd— upstream / downstream pressure (Pfeiffer single gauge and ionization gauge). The Pfeiffer gauge currently cannot ignite its cold-cathode discharge, so it is operating mostly in Pirani mode [developer addendum].Bu/Bd— upstream / downstream Baratrons (MKS 627 FS = 1 Torr, MKS 628B FS = 0.1 Torr).MFC1/MFC2— H₂ (20 SCCM) and O₂ (10 SCCM).Ci/Cv— cathode current and cathode voltage channels exist in code and on the PCB, but the cathode current has never actually been measured. [corrected] The conversion functions exist for consistency; the measurement was prepped but not deployed.T— membrane temperature (now read off-Pi).QMS_signal— boolean column logging whether the sync trigger is currently active.
Why the original "hydrogen-permeation" inference was correct¶
The first-pass interpretation was directly correct for the rig's original purpose. The signal list invited that reading because the rig was literally that.
First-pass inference (preserved). The first pass concluded: "two MFCs (H₂ and O₂) + heated membrane + upstream/downstream pressure pairs + QMS + cathode-driven plasma = plasma-driven hydrogen permeation experiment". This was correct as historical interpretation. The codebase has outlived its original purpose without changing its signal vocabulary — the archaeology was reading the rig's origin, not its current deployment.
Strong Engineering Ideas¶
These are the durable ideas — patterns worth keeping in any rewrite.
- Settings-driven channel topology with named conversion functions.
# controlunit/devices/adc_channels.py (lines 7–24)
class AdcChannelProps:
def __init__(self, *arg, **kws) -> None:
self.name = arg[0]
self.channel = kws["Channel"]
self.gainIndex = kws["Gain"]
self.description = kws["Description"]
self.conversion_id = kws["Conversion Function"]
self.full_scale = kws.get("Full Scale", None)
self.set_conversion_function()
Adding a new instance of an existing sensor type means editing
YAML, not code. Adding a new type means writing one function in
conversions.py and adding one entry to the dispatch dict.
- Versioned settings with per-host override.
# controlunit/readsettings.py (lines 24–51)
def select_settings(path_to_file="settings.yml", verbose=False):
"""
Check if there is local settings file and
if its version is same as current, load local one.
"""
local_settings = os.path.join(
os.path.expanduser("~"), ".controlunit", "settings.yml"
)
try:
local_config = load_settings(local_settings)
config = load_settings(path_to_file)
if local_config["Settings Version"] == config["Settings Version"]:
config = local_config
The repo carries the canonical settings.yml; the rig has
~/.controlunit/settings.yml with its own channel map; the
version-key check prevents silent drift after a schema bump.
- STEP-batched signalling — averaging and IPC amortisation.
# controlunit/devices/device.py (lines 47–55)
def set_sampling_time(self, sampling_time):
"""Set sampling time"""
self.sampling_time = sampling_time
if sampling_time >= 0.9:
self.STEP = 1
if sampling_time < 0.9:
self.STEP = 3
if sampling_time < 0.1:
self.STEP = 5
Developer Note (Arseniy). "Both — averaging and amortisation. That worked, never touched this."
-
Per-row preset logging. Every ADC row carries the commanded
PresetV_mfc1,PresetV_mfc2,PresetV_cathode,IGmode,IGscale, andQMS_signal. The CSV is fully self-describing. -
CSV header that embeds channel map. Outlives the code that wrote it.
-
Dual-purpose dummy modules.
devices/dummy.pylets the GUI boot on Windows. Crude on purpose. -
Hand-painted touch toggles. Designed for a 7" touchscreen that is no longer touched — but kept because the GUI still has to be readable at 7" over VNC, and the toggles read clearly at that size.
-
Settings.yml as deliberate documentation. The developer wrote the long header-comments as a memory aid for future-self after long gaps in work. The file is therefore both schema and prose documentation.
-
Hardware-emergency-stop semantics.
turn_off_voltages()runs before thread termination.
Developer Note (Arseniy). "Reading
_mfc_presetsto zero in three different places, havingturn_off_voltages()callable any time including whenself.workersis empty — yes, guilty. It may still be somewhere in the Qt signals. But probably the last LLM edits fixed this. At least during my short tests I've seen no issues."
- LaTeX in conversion docstrings. Physics inline with the code that uses it.
Technical Debt and Scaling Limits¶
A. Three half-completed renames¶
setup.pystill installscontrolunit.components.bin/plasmacontrolimportsMainWidget(nowMainApp).controlunit/__init__.pyreferencestree/pack(a deleted branch) and the variable_echelle_base(fromEchelle).
B. The sys.path hack¶
# controlunit/__init__.py (lines 9–11)
# temporarily add this module's directory to PATH
_echelle_base = pathlib.Path(__file__).parent.absolute()
sys.path.append(str(_echelle_base))
Inherited from Echelle. Makes from mainView import UIWindow work
alongside from controlunit.devices.adc import ADC. Robust in
practice; breaks editable installs, type-checkers, and pytest
collection from a different cwd.
C. Module-import-time select_settings() calls¶
# controlunit/heatercontrol.py (lines 5–13)
from readsettings import select_settings
config = select_settings(verbose=False)
CHHEATER = config["Heater GPIO"]
heatercontrol.py, trigger_signal.py, ui/docks/control.py,
ui/docks/gas_flow.py all read settings.yml at import time.
Configuration leaks into module-level state.
D. Aspirational ghost classes¶
core_logic.py and plot_data_handler.py are planned extractions
that have never been instantiated. The Codex-PR added a test for
CoreLogic. The developer is honest about these being intentions,
not abstractions. The first pass said: "either finish them or move
them aside." The developer is comfortable with their current limbo
state; it serves as documentation of next-refactor intent.
E. DataFrame-append-in-loop¶
# controlunit/main.py (lines 432–438)
self.datadict[device_name] = pd.concat(
[
self.datadict[device_name],
self.newdata[device_name].astype(self.datadict[device_name].dtypes),
],
ignore_index=True,
)
O(n) per cycle. Fine at 10 Hz, painful at 100 Hz.
F. CSV file handles opened-and-closed per write¶
# controlunit/main.py (lines 466–467)
data = self.newdata[device_name]
data.to_csv(savepath, mode="a", header=False, index=False)
Fine in practice; bad if anyone ever wants 100 Hz.
G. Tests are essentially absent¶
tests/test_sample.pyliterally testsinc(3) == 4.tests/test_core_logic_signal.pycovers a class that nothing else uses.- CI runs
flake8 syntax + pytestagainst the dummy file only.
Developer Note (Arseniy) on calibration tests. [corrected] The first pass said: "the example calibration tests against the PKR251 datasheet are circular because the datasheet isn't in
manuals/.""Tested, haha. It is from a manual. Just not in the code. And it's fine. Anyway, my old gauge can't ignite cold-cathode discharge. So that one is mostly Pirani now."
So the conversion polynomials are not circular; they were hand-fitted against real datasheets that simply live outside this repo (in
aklab-howto).
H. CI is from the Python 3.8 era¶
.github/workflows/pythonapp.yml pins Python 3.8 (2019).
requirements.txt says numpy>=1.17.4. The repo dodges a pandas
FutureWarning with astype(dtypes) — from a much newer pandas. CI
doesn't see the drift because it doesn't exercise the GUI path.
I. The kurokawa-dev/ quarantine (Kurokawa)¶
A 50-line SCPI driver for a Kikusui PWR-401L bench supply
[confirmed], tab-indented, in its own top-level folder. Kurokawa's
work — earlier than Kawabata's kawdev / plasma-current PID pass.
Never imported. A parking spot for an integration that may or may not
happen.
J. Inconsistent error-handling¶
DAC8532.init() has a bare except: GPIO.cleanup(). Silent
recovery, no log. If the DAC fails to initialise, the worker is
still spawned and signals are still connected.
K. The _init_cocnnections typo¶
A long-lived typo. Proof that no automated symbol-rename has touched this code.
Modernization Suggestions¶
These remain opinion. The developer has indicated that the LLM workflow has shifted (Codex-PR experience was "bad"; Cursor-assisted on-rig testing is now standard) and that the philosophical direction is toward a network of small instruments. Read the suggestions through that lens.
Tier 0 — zero-risk cleanup (an afternoon)¶
- Bring
setup.pyandbin/plasmacontrolin sync.
packages=["controlunit", "controlunit.devices",
"controlunit.ui", "controlunit.ui.docks",
"controlunit.ui.widgets", "controlunit.ui.buttons"],
entry_points={"console_scripts":
["plasmacontrol=controlunit.main:main"]},
And in bin/plasmacontrol:
from controlunit.main import main; main().
-
Pin a modern Python in CI. Bump from 3.8 to 3.11. Drop the CodeShip badge (CodeShip shut down May 2024).
-
Rename
_echelle_baseto_PACKAGE_DIR. Or — and this is more honest — keep the name and add a single-line comment saying it's a fossil from theEchellepredecessor project. The name itself becomes a tiny piece of in-code documentation. -
Decide on
core_logic.pyandplot_data_handler.py. Leave them; document them as planned extractions in this file (now done). -
Adopt git tags. Tag the current tip as
v0.4.0to match_version.py. Tag future commits when they touch hardware integration or change the on-disk CSV schema.
Developer Note (Arseniy) on versioning. "Yes, I have no idea how to iterate versions. So it's just there. I would benefit from better versions now, and from some tags in git."
Tier 1 — MkDocs migration¶
Use mkdocs-material + mkdocstrings[python]. Proposed layout:
docs/ (MkDocs source)
├── index.md (project intro + README photos)
├── Archaeology.md (this document)
├── status-inventory.md (extracted/expanded from this file)
├── hardware/
│ └── overview.md (link out to aklab-howto, do NOT duplicate)
├── operation/
│ ├── starting-acquisition.md
│ ├── plasma-current-pid.md
│ ├── sync-signal.md
│ └── safety.md
├── architecture/
│ ├── threading-evolution.md (the 6-phase narrative from this file)
│ ├── signal-flow.md
│ ├── echelle-lineage.md (the pre-history)
│ └── settings-versioning.md
├── reference/
│ ├── channel-map.md (hand-curated from settings.yml)
│ └── conversions.md (auto-rendered from conversions.py)
├── api/ (mkdocstrings auto-generated)
├── archive/
│ └── 2022-pdoc/ (the entire current docs/*.html)
│ └── README.md (rename map: AIO → adc_setter,
│ customTypes → conversions+adc_channels,
│ worker → devices/*)
└── changelog.md
Notes:
- Do not duplicate hardware docs. Hardware lives in
aklab-howto. This repo's docs are about the software that uses that hardware. - Archive, do not delete, the 2022 pdoc HTML.
- Disable the stale
[DOCS]link in README until MkDocs is up.
Tier 2 — finish the extractions stubbed by core_logic.py and plot_data_handler.py¶
Optional. The current main.py works. The extractions would benefit
the next major refactor — not the day-to-day.
Tier 3 — DataStore performance ceiling¶
Only do these when 100 Hz becomes a real requirement:
- Replace per-cycle
pd.concatwith adequeof small DataFrames concatenated lazily on plot request. - Open the CSV file once at
create_file, hold the handle.
Tier 4 — Testing the parts that matter¶
conversions.py is pure-function and physics-critical. Calibration
constants come from external datasheets [confirmed]. Add a
tests/test_conversions.py whose fixtures cite the datasheet source
(URL or page reference) rather than re-deriving from the conversion
formula itself.
Tier 5 — Hardware abstraction unification¶
Promote the _setter.py pattern into a real interface. A Chip ABC
with init(), read(), write(), cleanup() would let
devices/dummy.py become a real configurable simulator. Unlocks
headless integration tests.
Developer Note (Arseniy) on tool choice. "Still learning. Use whatever makes me happy. The Codex PR workflow was bad."
Engineering Mindset Reconstruction¶
This section is the most subjective. It is also the section where the developer's first-hand voice changes the most. [partially corrected and partially confirmed]
Reading the commit messages, file names, and the shape of changes over six years, plus the developer's own reflections, the working mindset has these features:
Code as instrumentation, not product¶
Commit messages like [fix] currentvalues B1,P1,B2,P2 -> Bu,Bd,
Pu,Pd, [fix] more B1s to Bds. Need to automate this., B2, B1
value :.1e, B1 value :.3f are signs of someone iterating with the
experiment running — channel names evolve as the upstream /
downstream meaning gets clearer.
Idempotent stop semantics ("I clicked stop, did it actually stop?")¶
Reading _mfc_presets to zero in three different places, having
turn_off_voltages() callable any time including when self.workers
is empty, if not self.workers: return guards everywhere — this is
someone who has been bitten.
Developer Note (Arseniy). "Haha — yes, guilty. It may still be somewhere in the Qt signals. But probably the last LLM edits fixed this."
Explicit-but-ugly, trending toward more Pythonic¶
The first pass said: "prefers explicit-but-ugly over
clever-but-fragile" — set_sampling_time is a chain of three if
statements; _mfc_presets = {1: 0, 2: 0} is a two-key dict instead
of a list; spinboxes-per-decade for MFC voltages. The developer's
own qualification:
Developer Note (Arseniy). "Yes and no. I wrote what I could. I prefer understanding, and yes, transparency. But I always disliked a lot and tried to get more Pythonic. Now LLMs can fix and guide me. I only welcome that. Still, like with Ito-kun, I don't want too much of what I don't really understand. Some of that is still required, but I'd like to minimise the fog."
This is the operative principle: understanding > cleverness, but not at the cost of being ugly forever. LLMs are tools for raising the floor of style without raising the fog.
Rename without breaking semantics¶
The 2024-09 storm of renames moved hundreds of references; meaning didn't change. That discipline is rare.
Learning by building (and now: teaching by enforcing)¶
The first pass said: "Tolerates collaborators with different styles." The developer added:
Developer Note (Arseniy). "Yes — but now I educate. And enforce my style, or close enough."
So: Phase 1 was tolerance for variety while learning; today is style enforcement while teaching. That is a real evolution and it explains why Phase 4.5 (the renames) had to happen.
Trust hardware feedback over theory¶
"Ip PID response detected (hardware)" was a commit message and a
celebration. PID gains 0.3, 0.1, 0 are not derived; they were
tuned. The membrane PID's one-sided clamping is what worked, not
what's textbook.
From monolithic instrument to network of instruments¶
The membrane temperature path was migrated to NI on Windows.
Developer Note (Arseniy). "Even if I would come back to it, I would make a dedicated ESP32 unit which would report to an orchestrator. I've evolved my understanding of a lab instrument. Into a network. I've made a single TC logger, but didn't have a chance to move to proper network instruments yet."
ControlUnit was the developer's electronics-learning project. It
worked. The next iteration of the same philosophy will not be a
single Pi-with-everything box but a small constellation of
ESP32-class devices reporting upward.
Tool use without dogma¶
Pre-commit + Black ran 2020–2022. CI was set up but never extended. Codex was let near the code twice in 2025, deemed a bad workflow afterwards. pdoc was run once. Cursor was adopted in 2026 with direct on-rig testing. None of these became dogma — they were experiments in workflow alongside experiments in physics.
Developer Note (Arseniy). "Still learning. Use whatever makes me happy."
The "Why this addresses?" mindset¶
# controlunit/devices/adc.py (lines 96–101)
def prep_adc_board(self):
"""
Initiates an instance of AIO_32_0RA_IRC from AIO.py
Address: 0x49, 0x3E
Why this addresses?
"""
Developer Note (Arseniy). "Me again. Arseniy. I can't know it all. I need papers to publish, lectures to teach, kids to raise."
That single sentence is the whole repo's epistemic position. The code is good enough to do the work; the work is the experiment, not the code; the developer is one human with a finite working budget and is honest about it. The "Why this addresses?" docstring is not a bug — it is a hand-written tag saying "I will return to this question if I ever need to; meanwhile, the experiment runs."
Known uncertainties and possible incorrect assumptions¶
This section is now much shorter. Most of the first-pass uncertainties have been resolved by the developer's pass. The remaining open items below are flagged because they still depend on interpretation or external knowledge.
Still open (or partially open)¶
- The exact authorship line on
ed7cadb"Untangling Ito-kuns threading mess". The commit is signedqueezz. The developer has indicated that "another lab staff helped to untangle". Whether that means a third person co-authored, that Arseniy committed work done by someone else, or that the help was advisory only, is not recorded.
- The exact physical isolator chip used for the MCP4725 in Phase
6. The commit message says "isolated DAC" and the developer
confirms the isolation is real and critical. The chip identity
itself is not in the repo; it lives in the hardware build of the
rig. (Likely documented in
aklab-howto; should be linked once the MkDocs migration runs.)
- The current state of
core_logic.pyandplot_data_handler.py. The developer is comfortable leaving them as planned-extraction documentation. Whether they will ever be filled in is genuinely unresolved.
- Whether the membrane-temperature path will ever return to the Pi. The developer says "forever 🙂" but also describes an ESP32 network successor. The honest answer is: not in this repo.
- The exact behaviour of the Pfeiffer gauge in its current
Pirani-only mode. The conversion function in
conversions.pyassumes full PKR251 operation (cold-cathode + Pirani crossover). If the gauge can no longer ignite the cold-cathode discharge, the conversion is only correct over the Pirani range. This is a known unknown that the developer has signalled but has not yet encoded inconversions.py.
Resolved by developer pass (preserved for context)¶
- The "hydrogen-permeation experiment" inference was historically correct; the rig has since become a multi-purpose plasma lab.
- The "QMS galvanic isolation by photons" inference was wrong; the shared LED+GPIO is just a sync signal for various loggers.
- The "touchscreen-first design" inference was historically correct but currently disabled; VNC and Bluetooth mouse are the primary input.
- The "Kikusui PWR-401L" identification was correct.
- The
_echelle_basevariable is a literal artifact of the predecessorEchelleproject — not a name collision, not template-copy ambiguity. - The "Ito-kun wrote the initial threading" attribution was wrong:
Arseniy ported the Worker shape from
Echelle; Ito-kun extended it (and introduced the I²C-per-channel-read bottleneck). - The "Phase 4 commit by hasuo_kuzmin.lab" was Miura-kun committing from the lab Pi during a pandas-migration push.
- Kurokawa (git handle
Kshora) and Kawabata are two separate contributors in sequence: Kurokawa submitted PRs #1, #18, #19 asKshora(early MFC / MCP4725 work) and leftkurokawa-dev/PWR.py; Kawabata came later withkawdevand the plasma-current PID / isolated-DAC work. - The "Kawabata" name attached to the plasma-current PID and isolation work is correct.
- The "baseline = 1000 mV corresponds to ~16 A" value reflects an empirical minimum-ignition setpoint, likely Kawabata-kun's.
- The calibration tests proposed in Tier 4 are not circular: the polynomials come from datasheets that simply live outside this repo.
A closing note in the developer's voice¶
The first-pass archaeology ended with:
"If you correct any of the uncertainties above, please date the correction and keep the original claim visible — the history of what was thought is itself part of this document's value."
To which the developer's reply was, quite reasonably:
Developer Note (Arseniy). "I'm generating this now. And I'm the dev, I can correct now, can't I 😁"
Yes. And the fact that the developer is doing so — actively, voluntarily, with humour, and with substantive memory about contributors going back six years — is exactly what makes this codebase a living laboratory instrument rather than a software artefact. The document above is therefore not a report on a dead system. It is a snapshot of an instrument and of the person who keeps it running.
The next archaeological pass will find things this one missed. That is good. Until then, this is what we know.
End of second-pass reconciled snapshot. Ecosystem context follows.
Related Projects and Ecosystem Evolution¶
The first two passes treated
ControlUnitas if it were an island. It is not. It sits in a small constellation of repositories that share the same author, the same lab, overlapping hardware, and a visibly evolving philosophy of what a laboratory instrument should look like. Reading those repositories alongside this one turns several "loose ends" of the archaeology into "edges that point outside the frame". This section adds that context without rewriting the previous archaeology — those edges were always there, we just did not name them.
Why this section exists¶
A repeated pattern in the first two passes was: "this looks
half-finished" → developer correction: "yes, because the rest moved
elsewhere." The membrane temperature path, the hardware documentation,
the in-code datasheet links, the half-stubbed extraction classes, the
quarantined kurokawa-dev/PWR.py — each is a place where the local
archaeology hit a dead end that turned out to be a gate into a
neighbouring project.
Naming the gates:
- The
Echellereferences and the_echelle_basevariable point backward to a predecessor. - The MAX6675 heater path and its
TemperatureControllink in a TODO comment point sideways to a migration target. - The
aklab-howtoURLs the developer has mentioned point outward to an external documentation hub. - The developer's stated "network of instruments" goal and his
"single TC logger" remark point forward to
tclogger. - A second project running on the same Pi (
pihtivacuum) points adjacent — a peer process on shared hardware.
The constellation is small but it is a real lineage. The rest of this section reconstructs it.
The constellation¶
1. echelle_spectra — the predecessor (≈ 2018–2019)¶
Repository: https://github.com/queezz/echelle_spectra
A spectroscopic GUI and data-processing application developed during
the developer's work at NIFS on the LHD (Large Helical Device). It
predates ControlUnit and is the direct ancestor of several of this
project's most stubborn architectural fossils.
What Echelle gave ControlUnit:
- pyqtgraph + DockArea layout. The dual-pane plot,
GraphicsLayoutWidget, theDock-per-panel pattern, and the custom-painted toggle aesthetic all originate here. - Threading template. A
QtCore.QObjectsubclass parameterised by anEnum, driven by anapp.processEvents()-from-the-worker loop, communicating with the main thread viapyqtSignal. The shape ofWorkerin the first-passcontrolunit/worker.pywas template-copied from this. - Package-init sys.path hack. The
sys.path.append(__file__'s parent)idiom that letsfrom mainView import UIWindowwork alongsidefrom controlunit.devices.adc import ADC. The variable name_echelle_baseis, literally, the original variable name fromEchelle— preserved unchanged across the copy. - General "Qt application as scientific instrument" feel. Big
fonts, large hit areas, single-tab + a
Settingstab, HTML-formatted log dock, datetime-axis plots. The visual grammar.
Historical Evolution (Echelle → ControlUnit). [confirmed] The
_echelle_basevariable, theWorkershape, the touch-sized toggle aesthetic, and the multiple-inheritanceUIWindowmixin all descend fromechelle_spectra. When Ito-kun took the template and extended it for the ADC, he extended Echelle code in ControlUnit clothing. The first-pass archaeology read those fossils as "Qt-newcomer choices" — they are, but the newcomer was the developer himself two projects ago, not the B4 student.
Echelle is also where the developer first hit Qt threading, first
used pyqtgraph seriously, and first built a real GUI for an
experiment. The "I finally understood what classes are: drawers" line
in the Phase 4 Developer Note is, in retrospect, a delayed payment on
patterns that were imported in Echelle and only understood in
ControlUnit Phase 4.
2. pihtivacuum — the co-resident peer (concurrent with ControlUnit)¶
Repository: https://github.com/queezz/pihtivacuum
A companion project running on the same Raspberry Pi as
ControlUnit, dedicated to vacuum-system orchestration for the PIHTI
rig. Its presence is invisible inside ControlUnit's git history but
materially shapes how ControlUnit actually runs on the lab Pi.
Implications for the archaeology of this repo:
- The Pi is not a
ControlUnit-exclusive box. It is a small multi-process orchestrator on whichControlUnitis one of at least two long-running processes. - Several decisions in
ControlUnitthat look defensive in isolation make sense when you remember another process is sharing the box: - The unique CSV filename per run
(
cu_<YYYYMMDD_HHMMSS>.csv) avoids collision with another writer. [inferred] - The
~/work/cudata/data path is configurable via~/.controlunit/settings.ymlprecisely because another process might own a different path on the same disk. [inferred] - The
start_gpio.py: start_pigpiod()routine kills any existingpigpiodand restarts it. That is aggressive for a single-tenant app — and exactly what you need on a multi-process Pi where another program might have leftpigpiodin a bad state. [inferred]
Historical Evolution (peer process). [inferred from ecosystem; flagged for developer confirmation] The
pihtivacuumproject shares the Pi withControlUnit. They coexist by convention rather than by IPC: each owns its GPIO pins and its CSV namespace. There is no shared bus or message broker — the orchestration is human-managed and file-system-mediated. This is "network of instruments, no network protocol yet".
pihtivacuum is the first sign that the box (the physical Pi) had
already begun to function as a small operating environment hosting
multiple instrument processes — even before the developer
articulated the "network of instruments" philosophy. The intent
arrived after the practice.
3. TemperatureControl — the migration target (≈ 2024 onward)¶
Repository: https://github.com/queezz/TemperatureControl
A Windows + National Instruments DAQ implementation of the
thermocouple-reading and heater-control logic that used to live in
ControlUnit's MAX6675 + HeaterControl path. Referenced inline in
this repo as a TODO target:
# controlunit/devices/max6675.py (lines 178–181)
"""
Shouldn't the self.sampling_time here be 0.25, not the one for ADC?
TODO: update to simple-pid as in TemperatureControl
https://github.com/queezz/TemperatureControl
"""
What TemperatureControl represents in the constellation:
- A migration, not a refactor. The heater PID was not
re-implemented in place. It was lifted out of the Pi and rebuilt
on a platform with stronger analogue front-ends (NI DAQ) and a
different OS (Windows). The
ControlUnitMAX6675 path was left in place as a dormant capability rather than deleted. - A first deliberate decoupling. Before
TemperatureControl, the Pi did everything. After, the Pi did plasma + gas + vacuum and a Windows machine did temperature. The boundary was drawn along a physical-signal-class line (the thermocouple), not along a software-architecture line. - A reference implementation of the "use
simple_pidproperly" move. The TODO comment makes it explicit: when the Pi heater PID eventually returns (or is replaced by ESP32), it will look likeTemperatureControl, not like the hand-rolledMAX6675.temperature_control()integral-clamped loop.
Historical Evolution (heater path). [confirmed by developer in §System Status Inventory] Heater migration is described elsewhere in this document as "forever 🙂".
TemperatureControlis the destination of that migration — the project that absorbed a capabilityControlUnitused to own. Reading theControlUnitrepository alone gives you only the departure; the constellation gives you the arrival.
4. aklab-howto — the external documentation hub¶
Repository: https://github.com/queezz/aklab-howto Live site: https://queezz.github.io/aklab-howto/
A lab-wide knowledge base. The pages directly relevant to
ControlUnit:
- Control Unit overview
- High-Precision AD/DA Board
- Y-Corp ADC board
- FT232H USB GPIO (the moved-to-Windows-NI replacement)
What it does for the archaeology of ControlUnit:
- It explains what is missing from
manuals/. The first-pass said "one orphan datasheet" and recommended adocs/datasheets/folder. The correct reading is: the datasheets do live somewhere, but somewhere else. The repository's apparent thinness in hardware docs reflects a deliberate externalisation, not neglect. - It absorbs hardware build notes that have no place in the
Python code. Board layouts, signal-conditioning circuitry,
cabling, pin numberings, panel layouts — all of these are
hardware artefacts that change on a different timescale than the
software does. Keeping them in
aklab-howtoletsControlUnit's git history stay focused on software. - It is itself an MkDocs site. When
ControlUnitmigrates to MkDocs, the natural pattern is cross-link, do not duplicate.
First-pass inference (corrected). [corrected] The first pass said the
manuals/folder was sparse and recommended populating it. Withaklab-howtoin the picture, the correct recommendation is the opposite: emptymanuals/further, migrateads1113.pdfout intoaklab-howto, and replace the in-repo folder with a one-line pointer.
5. tclogger — the future-direction successor (recent)¶
Repository: https://github.com/queezz/tclogger
An ESP32-based thermocouple-logging project. The "single TC logger" the developer mentioned in the Phase 4 / heater-path Developer Notes.
What it represents:
- A break with the Pi-as-everything model. ESP32 is small,
cheap, single-purpose, and natively network-aware. A
tcloggerunit is not a control panel — it is a sensor that publishes. - A first move toward "the orchestrator does not own the
sensors". In
ControlUnit, the Pi owns the ADC, the DACs, the thermocouple, the GPIO sync line — everything is wired into the one box. In atclogger-style architecture, the orchestrator is decoupled from the sensors by a network boundary; sensors can be added, removed, or replaced without touching orchestrator code. - A different software shape entirely. Not PyQt5 + pyqtgraph +
pandas. Different language likely, different deployment model
(firmware flash, not
python -m), different update cadence.
Historical Evolution (forward arrow). [confirmed] The "network of instruments" philosophy that appears verbatim in the Engineering Mindset section of this document is operationalised for the first time in
tclogger. The membrane-temperature-via-ESP32 design the developer described is the natural next step fromtclogger.ControlUnitis therefore the terminal form of the "single Pi does everything" lineage;tcloggeris the initial form of the lineage that replaces it.
Cross-repository timeline¶
Anchoring ControlUnit's development against the others gives a
fuller chronology:
≈ 2018–2019 Echelle Spectra at NIFS / LHD.
pyqtgraph + Qt threading + the "scientific Qt app"
visual grammar. The template-source.
2019 Worker / mainView / __init__.py patterns template-copied
forward into a successor lab.
2020-02 ─── ControlUnit initial commit. Echelle-ported Worker.
2020-03 ─── Ito-kun's I²C-per-read extension and the "Untangling"
commit. Numpy buffers, ThreadType enum, custom toggles.
2020–22 ControlUnit running. Long quiet period. Lab in operation.
(pihtivacuum likely co-resident on the Pi; precise dates
would come from that repo's history.)
2022-06 ─── ControlUnit pdoc3 docs generated. Visual snapshot of
"old" architecture preserved.
2023-05 ─── ControlUnit ADC-thread tuning storm. settings.yml
becomes deliberate documentation.
2024-08 ─── ControlUnit worker super class split (Miura-kun's
pandas-migration commit from the lab Pi).
2024-09 ─── ControlUnit serial renames. sensors→devices, components→ui.
2024 onward TemperatureControl on Windows + NI.
Heater path migrates *out* of ControlUnit.
ControlUnit MAX6675 path becomes dormant.
2025-08 ─── ControlUnit Codex PRs. Software-only experiment in
LLM-assisted development.
≈ 2025–2026 tclogger experiments.
ESP32-based single-sensor instruments. First operational
move toward a "network of instruments" architecture.
2026-04 ─── ControlUnit isolated DAC for plasma current
(Kawabata-kun). The latest serious in-repo work.
2026-05 ─── ControlUnit "settings: debug false" — the present.
aklab-howto runs continuously alongside all of this as the
documentation hub. Its pages on ControlUnit hardware predate and
postdate any single commit in this repo.
The dates inside ControlUnit are precise (git-confirmed). The dates
outside ControlUnit are approximate from the user's descriptions
[speculative on exact years] — the corresponding repo histories
would tighten them.
Philosophical evolution across the constellation¶
Reading the five repos as one body of work, four trajectories are visible.
From inherited patterns to chosen patterns¶
Echelle → ControlUnit (early phases) → ControlUnit (Phase 4 onward).
The early ControlUnit inherited Qt-application patterns it did
not yet understand (the Worker shape, the sys.path hack, the
app.processEvents() from a worker thread). Through 2024–2026, the
developer's commit notes and the structural changes show those
patterns moving from received to chosen — kept because they
work, modified where they do not, named in vocabulary the developer
controls.
From "one machine does everything" to "boundary along signal class"¶
ControlUnit → TemperatureControl. The first major decoupling cut
the lab apparatus along the boundary of thermocouple measurement +
heater control. Not because the software architecture demanded it,
but because the physical signal class deserved better hardware (NI
DAQ for accuracy, Windows for driver maturity). The boundary is
physical, not architectural.
From "in-code datasheet links" to "external documentation hub"¶
settings.yml URLs → aklab-howto pages. Early ControlUnit
captured hardware knowledge as comments. aklab-howto later
absorbed that knowledge into its own MkDocs site, leaving
ControlUnit free to focus on software. This is the same migration
strategy as TemperatureControl, applied to documentation rather
than to thermocouples.
From "monolithic control panel" to "network of single-purpose nodes"¶
ControlUnit + pihtivacuum (multiple processes, one Pi) →
tclogger (one process, one ESP32, network-facing). The first step
was multi-process on one box; the second step is multi-box. The
"network of instruments" philosophy is therefore not a sudden
insight — it is the next deliberate step in a trajectory that
already started when pihtivacuum began running alongside
ControlUnit on the same Pi.
Developer Note (Arseniy) — paraphrased from earlier in this document, restated here for the trajectory. "I've evolved my understanding of a lab instrument. Into a network."
What the constellation shows is that the understanding came after
the practice — pihtivacuum was already a peer process on the Pi
before the developer named the pattern; TemperatureControl was
already off-loading a physical signal class before the developer
articulated the principle; tclogger is the first project where the
principle was clear before the code was written.
What this means for reading ControlUnit itself¶
Several characteristics of this repository now have external explanations rather than local ones:
| Local observation | External explanation |
|---|---|
_echelle_base variable name |
Direct fossil from echelle_spectra |
Worker / ThreadType / sys.path hack |
Inherited from echelle_spectra |
| Custom-painted touch toggles | Visual grammar from echelle_spectra |
Sparse manuals/ folder |
Hardware docs deliberately live in aklab-howto |
MAX6675 path dormant |
Capability migrated to TemperatureControl |
simple-pid TODO reference |
Points at TemperatureControl's reference implementation |
Aggressive pigpiod restart in start_gpio.py |
Coexists with pihtivacuum on the same Pi [inferred] |
| "Network of instruments" intent in code comments | First realised in tclogger, not in this repo |
kurokawa-dev/PWR.py quarantine |
Kurokawa's PWR-401L driver; predates Kawabata's plasma work |
The right MkDocs structure for ControlUnit therefore is not
a self-contained encyclopedia. It is a regional documentation:
deep on this repository's software, thin where the constellation
already provides the answer, and cross-linked to:
aklab-howtofor hardware,echelle_spectrafor visual / threading lineage,TemperatureControlfor the heater migration,tcloggerfor the network-of-instruments direction,pihtivacuumfor the Pi-as-orchestrator peer context.
Open questions about the ecosystem¶
Items where the ecosystem reading is itself uncertain and would benefit from the developer's pass:
- Exact temporal overlap between
pihtivacuumandControlUnit. The peer-on-the-same-Pi reading is strongly suggested by the developer's description, but the precise time at which the two began co-residing is not in this repository. - Whether
TemperatureControlconsumedControlUnit's old thermocouple data format. A direct CSV-compatibility check againstcontrolunit/main.py: generate_header_temperature()would tell us whether the migration preserved historical data continuity or restarted it. - Whether
tcloggeris intended to replace theMAX6675path insideControlUnitor to bypass it entirely. The Developer Note in §PID stops short of committing — "even if I would come back to it, I would make a dedicated ESP32 unit". Whether that ESP32 reports toControlUnitor around it is a design question yet to be answered. - What "PIHTI" precisely names. The acronym recurs in
pihtivacuumand in the developer's lab vocabulary. Resolving it would help orient the documentation. [speculative — likely a rig / facility designation rather than a technique]
These are not internal questions of ControlUnit. They are
questions about the lab as a software ecosystem. They are listed
here so that the next archaeological pass — whether against
tclogger, pihtivacuum, or the lab as a whole — has a starting
list of edges to chase.
End of ecosystem context. The instrument is, and has always been, embedded.