Work / picw-overview
picw-overview
Replacing a Mitsubishi PLC with a Raspberry Pi 4B as master of a dynamic checkweigher (Thames Side T16 / XT1-SO)
Source on GitHub ↗ · Talk to me →
Readme
Replacing a Mitsubishi PLC with a Raspberry Pi 4B as the master of a dynamic checkweigher.
Load cell: Thames Side T16 (viscous damped, 5–35 kg). Transmitter: Thames Side XT1-SO (24-bit, 2400 readings/s, RS485 Modbus RTU). Throughput: 40 items/min.
Source is in a private repository. This page is an overview.
Why replace the PLC
The PLC is scan-cycle bound, can't hold product recipe tables or item history, and has no capacity for signal processing on the weight trace. None of that is fixed by a faster PLC.
Safety
The E-stop and guard interlocks are hardwired through a safety relay that removes power from the conveyor motor and pneumatic actuators directly. The Pi reads E-stop state for display, logging, and interlocking its own outputs. It is never the only element between an operator and a moving part. No part of this software is a safety function.
Regulatory position
Internal quality control only. The machine is unstamped and holds no OIML R51 approval as an automatic weighing instrument — the XT1000's OIML R76 certificate covers non-automatic weighing and approves the transmitter as a module, not the machine.
So there is no legal metrology constraint. If the machine is ever brought into a legally controlled application, the design must be revisited first.
Architecture
One service plus a kiosk browser. The control loop runs on a dedicated thread and is never driven from an HTTP handler — a slow request must not be able to stall a reject.
Machine one tick drives everything
├── RTLoop edges, item queue, desync detection, reject, jitter
├── ScaleService XT1000 over Modbus RTU, reconnect with backoff
├── Scanner+Binder per-item binding, product changeover
└── Store products, items, events, audit (SQLite WAL)
↑ ticks on its own thread
FastAPI REST + WebSocket → chromium --kioskEvery hardware boundary is a protocol with two implementations:
Dual verdict
The XT1000 classifies in hardware from 2400 readings/s and drives its setpoint outputs. The Pi independently classifies from the streamed weight with its own filtering. Both are logged per item; a config key picks which drives the rejector, defaulting to the XT1000.
The point is evidence: prove the Pi's classification is actually better on this machine with this product before trusting it with reject decisions.
Roles, not pins
Runtime code binds to role names — item_entry, reject_actuator, weight_ready — never to GPIO numbers. The setup wizard maps physical channels onto roles, and the machine cannot leave setup mode until every required role is bound exactly once.
Queue desynchronisation
The failure mode the design is built around. If the entry photo-eye counts an item the exit eye misses, the FIFO shifts by one and every subsequent item inherits the previous item's verdict — silently rejecting good product and passing bad. Each queued item carries an expected arrival window derived from belt speed and distance; an exit edge matching no window is treated as a desync, not an item. Default response is to stop the belt.
Status
Complete and running.
327 tests · 90% coverage · ruff clean · mypy strict cleanReal-time core, Modbus driver for the transmitter, SQLite storage with product recipes and an append-only audit log, barcode binding and product changeover, REST + WebSocket API, touchscreen HMI and setup wizard, one-line installer, systemd units, kiosk browser, Samba export, and WiFi priority management.
Every hardware boundary has a simulated implementation behind the same protocol, so the whole machine — packages, weights, barcodes, rejects, faults — runs on a Raspberry Pi with nothing attached.
sudo bash deploy/install.sh # simulated mode
sudo bash deploy/install.sh --hardwareInterface
Five tabs sized for a 10" panel: Run (live weight, throughput, verdict conflicts, jitter, simulator with fault injection), Products (recipe editor with barcode binding), I/O (live input LEDs, guarded output forcing), Setup (role bindings, belt geometry, serial scan with generated udev rules), Diagnostics (jitter histogram, events, audit).
Vanilla JS and CSS — no build step, no npm, no CDN, because it runs in a kiosk browser on a read-only root.
Setup wizard
The commissioning tool, reachable from the HMI and re-runnable step by step as a diagnostic. Serial auto-detect sweep across baud/parity/address; live sensor state indicators you can trip by hand; guarded output forcing; reject-delay learn mode; product recipes bound to barcodes; and a supervised trial run reporting verdict disagreement, reject confirm rate, throughput, and the jitter histogram that decides whether the Python real-time approach holds.
Design notes
- All control timing uses CLOCK_MONOTONIC. An NTP step must never move a reject window.
- Safe state is "stop the conveyor," not "pass everything." Nothing ships unweighed.
- Solid-state outputs, not relays. A relay adds 5–15 ms of switching latency to the
reject path and wears out.
- Boot from USB SSD. SD card corruption on unclean power-down is the dominant field
failure for Pi appliances.
- Immutable data throughout. Frozen dataclasses; state transitions return new instances.
Stack
Python 3.11 · pytest · ruff · mypy (strict) · libgpiod · FastAPI · SQLite · systemd
More work
plc-checkweigher
One-command installer for the Mitsubishi PLC check-weigher system. Python · Raspberry Pi · Mitsubishi PLC · PDF Reports · npm CLI
checkweigher
High-speed industrial check-weigher on the edge. Python · Raspberry Pi 4 · XT1000 · T16 Load Cell
Tovex-CRM
Modular CRM platform with real-time dashboards. TypeScript · TypeScript · Modular Architecture · Real-time Dashboards