Delivered remotely for a business based in Auckland, New Zealand. Names withheld by agreement.
Project Overview
Industry: Industrial IoT and utility field operations — remote monitoring and pump/level control for water infrastructure.
Type of solution: An edge automation platform delivered as three connected products: a Python runtime installed on industrial PLC hardware, a desktop/HMI configuration and monitoring application packaged for the same device, and a cross-platform mobile application for offline field data collection.
Business context: An operator responsible for a set of distributed sites faces a familiar trade-off. Manual rounds are expensive and leave the sites unobserved between visits. Conventional automation removes the rounds but locks the logic away in a proprietary toolchain, so the system becomes something only a specialist can change — and therefore something nobody changes. Meanwhile the organisation already runs a control room, and anything new has to feed it rather than replace it.
General users: Site and installation technicians configuring the device, operations staff monitoring live values on a panel display, field operators recording manual readings, and third-party control systems consuming data over a standard industrial protocol.
General purpose: To turn a PLC installation into something that can be configured, calibrated and re-purposed by the people who operate it, while continuing to behave like a well-mannered industrial device to everything around it.
The Business Challenge
Site logic is effectively frozen once installed. Changing a scaling constant, an alarm threshold or a pump start level means a specialist with a proprietary environment and a redeploy. The practical result is that installations drift out of step with how the site actually runs.
Raw sensor readings are meaningless on their own. A current-loop level transmitter arrives at the controller as an integer from an analog-to-digital converter. Turning that into a level in engineering units depends on the sensor's range, the signal conditioning and the site's own conventions — all of which differ from installation to installation.
Some decisions need history, not just the current value. Whether a pump should start can depend on a rolling average, or on how fast a level is rising, not merely on the instantaneous reading. That requires past values available at the device — including after a restart, and at sites where the network is unreliable.
Alarms drive real machinery. An alert that fires an action sequence on every acquisition cycle while a condition persists will chatter a contactor. Sequencing matters too: enabling a drive and setting its speed reference are not interchangeable operations.
Hardware is not uniform. Different controller versions and models expose different I/O, some pins fixed to one signal type, others switchable in hardware. An interface that offers a configuration the installed hardware cannot perform is worse than no interface at all.
The control room already exists. Whatever is installed has to publish into existing SCADA and HMI systems using a standard protocol, in engineering units, with no bespoke integration work on the other side.
Field rounds do not disappear overnight. Even where automation is installed, staff still take manual readings at sites — often with no usable mobile signal — and those readings still have to reach the back office.
It has to run unattended for a long time. A device at a remote site is not restarted by a helpful engineer. It has to survive a missing dependency, a failed sensor read and a power cycle without human attention.
Our Approach
Make the configuration the program. The complete behaviour of the device — hardware model, active I/O and sample rates, per-channel transforms, derived variables, alert rules, action sequences and protocol register mappings — is expressed as a single structured configuration document. The interface is a structured editor for it; the runtime is an interpreter for it. Nothing is compiled, and changing what the device does is a form submission rather than a deployment.
Give every number a defined provenance. Values move through three explicit tiers: the raw port reading, the channel value in engineering units, and the calculated value derived from channels and history. Each tier is separately viewable, chartable, testable and mappable to a protocol register, so when a number looks wrong it is possible to see at which stage it went wrong.
Let non-programmers configure without writing code — and let engineers write code when they need to. Common transformations are generated from parameters: linear scaling between an input and an output range, inverse and exponential curves, rolling averages, sums and min–max normalisation over a history window. Where a site needs something the templates do not cover, a transform can be authored directly. Parameter validation is strict — a missing scaling parameter raises rather than silently defaulting to something plausible and wrong.
Let the technician test before committing. The editor can execute a transform against the running device and show the result, so a calibration is verified during commissioning rather than discovered during an incident.
Emit coherent snapshots, not a jitter of updates. Each pin is sampled at its own interval, appropriate to its sensor. A separate aggregation cycle assembles a complete picture of all variables at a bounded interval, derives channel and calculated values, evaluates alerts, and publishes that one consistent snapshot to the interface, the historian and the protocol registers simultaneously.
Keep history on the device. A local time-series historian holds readings, which is what makes rolling averages and rate-of-change calculations work at a site with no dependable uplink.
Latch alarms and order the actions. Alert state is retained until the condition clears, so an action sequence executes once per crossing. Sequences are explicitly ordered, and reordered by dragging in the interface, because the order in which outputs are driven is a physical property of the plant.
Speak the protocol the control room already speaks. Computed engineering values — not raw counts — are published as standard floating-point register pairs, so an existing SCADA client, HMI or data logger integrates with configuration alone.
Build a full simulation path. When the hardware I/O library is unavailable, the runtime follows the same code path with generated values. The interface can be developed, demonstrated and tested without a controller present, and a commissioning engineer can walk through a configuration before the sensors are wired.
Make installation one action. The desktop application and the edge service ship as a single OS-level package that provisions the runtime environment, installs a service unit under a dedicated account, creates the desktop entry and detects the system user rather than assuming one. The application starts the service, waits until it is answering, and stops it on exit.
Design the field app for no signal at all. Reference data and entered readings are cached locally, unsynced entries are explicitly flagged and counted, and synchronisation happens automatically on reconnect or on returning to the foreground — with manual retry and a visible pending count so the operator is never guessing.
The Solution
Hardware-aware I/O configuration. A port catalogue per controller version and model describes each pin's type, signal capability and whether its mode is switchable, so the interface only offers configurations the installed hardware can actually perform. Each port is activated individually with its own sample interval and variable name.
Channel configuration. Each active input is given a transform producing an engineering value — chosen from parameterised templates or authored directly — along with its unit, display colour and optional live bar chart bounds.
Calculated variables. Derived values computed from channel values and, where requested, a window of historical readings: rolling averages, sums, normalised values and rate-of-change style calculations.
Alerts and action sequences. Each alert names a trigger variable and a condition, optionally over a history window, and carries an ordered sequence of output actions — digital, relay or analog/PWM writes — reordered by dragging. Trigger state is latched so a sequence fires once per crossing.
Industrial protocol mapping. Any channel or calculated variable can be mapped to a register address and unit identifier and served over a standard protocol as a floating-point value, with the mapping editable from the same interface as everything else.
Live operations dashboard. Stat cards for ports, channels and calculated variables, live bar charts with configurable bounds, an alerts table, live protocol register values, and system health indicators for the hardware library and the historian.
Data monitor. A tabular live view of every variable at every tier with direct control over starting and stopping acquisition.
Guided five-step commissioning wizard. I/O ports, channels, calculated variables, actions and alerts, and protocol mapping — with progress tracking and free navigation between completed steps.
Device-local historian. Readings are written continuously to an on-device time-series store that also backs history-dependent calculations.
Panel-ready desktop application. Touch-optimised interaction, collapsible navigation, fullscreen operation and rendering tuned for low-power ARM display hardware, distributed as a native Linux package.
Offline-first field readings app. Operator and region selection, validated reading entry with conditional required fields, historical readings per site with date filtering and search, a local queue with a visible unsynced count, automatic synchronisation on reconnect and local housekeeping of already-synced entries.
Key Features
Configuration-driven control logic The complete behaviour of the device is a structured configuration document authored through the interface and interpreted at runtime — no vendor IDE, no compilation, no redeployment to change what a site does.
Three-tier variable pipeline Raw port readings, engineering-unit channel values and derived calculated values are modelled as distinct tiers, each independently visible, testable and mappable, so every displayed number has a traceable origin.
Parameterised transform templates with strict validation Linear scaling, inverse and exponential transforms, and history-based averages, sums and normalisation are generated from parameters, with missing parameters raising errors rather than silently defaulting.
In-place transform testing Transforms can be executed against the running device and the result inspected before the configuration is committed — commissioning verification for people who do not write software.
Per-pin sampling with coherent aggregate snapshots Every input is sampled at an interval appropriate to its sensor, while a separate aggregation cycle publishes one consistent picture of all variables to the interface, the historian and the protocol registers together.
Latched alerts with ordered action sequences Alert state persists until the condition clears so sequences fire once per crossing, and the sequence order — which matters physically — is expressed as a drag-and-drop list.
Standard industrial protocol server Computed engineering values are exposed as standard floating-point register pairs, so existing control-room systems integrate by configuration rather than by custom development.
Device-local time-series historian Readings persist on the device, enabling history-dependent calculations and local retention at sites with no dependable network.
Full simulation mode The runtime follows the same code path with generated values when the hardware library is absent, enabling interface development, demonstration and pre-wiring commissioning walkthroughs.
Offline-first field data capture Manual site readings are entered, validated and queued locally with a visible pending count, synchronising automatically when connectivity returns.
Technical Architecture
Device runtime. A single Python process on the controller. One asynchronous task per active input runs at that input's configured interval, writing into a lock-protected snapshot. A separate aggregation task assembles port variables, derives channel variables, derives calculated variables — pulling a bounded history window from the local store where a calculation requires it — evaluates alert conditions, executes ordered output actions, and fans the completed snapshot out to registered consumers.
Consumer fan-out. The same snapshot drives three destinations: connected interface clients over the websocket link, points written to the local time-series store, and in-place updates to the protocol server's register block.
Hardware abstraction. Pin access is wrapped so that digital, analog and relay reads and writes are uniform across signal types, with a complete simulation path when the vendor I/O library is unavailable and a per-model port catalogue constraining what the interface can offer.
Operator interface. A React application hosted in a desktop shell on the same device, connecting to the local websocket server. Configuration is held in a central store and written back as partial merges; live values flow into a separate slice consumed through selector-level subscriptions so individual cards update without re-rendering the dashboard. Routing is hash-based for correct behaviour when the page is loaded from the local file system.
Protocol layer. A standard industrial protocol server maintaining a pre-allocated register block, encoding mapped variables as floating-point register pairs with defined byte and word order, started and stopped on demand.
Packaging and service layer. A native Linux package that provisions the runtime environment, installs a service unit under a dedicated account with a restart policy, creates the desktop entry and detects the system user at install time. The desktop application starts the service, polls until it responds, and stops it on exit, so the operator launches one thing.
Field application. An independent mobile client with a local cache and an explicit sync queue, connectivity detection, and reconciliation against a back-office API on reconnect.
Flow: Sensors and field devices → PLC I/O → per-pin sampling → channel transforms → calculated variables → alert evaluation → ordered output actions → aggregate snapshot → operator interface + local historian + industrial protocol registers → existing SCADA/HMI systems
Technology Stack
| Category | Technology |
|---|---|
| Edge runtime language | Python 3 (asyncio) |
| Edge transport | Socket.IO server over aiohttp |
| Industrial protocol | Modbus TCP server (holding registers, IEEE-754 float32 pairs) |
| Time-series storage | InfluxDB, device-local, queried for bounded history windows |
| Hardware access | Linux industrial PLC vendor I/O library, with full simulation fallback |
| Service management | systemd unit with restart policy, PID-based single-instance guard |
| HMI framework | React 18 with TypeScript |
| HMI build tooling | Vite with SWC |
| HMI state | Redux Toolkit and React Redux, with selector-level subscriptions |
| HMI data layer | Socket.IO client behind a service wrapper; React Query provider |
| HMI UI | Tailwind CSS with a Radix UI component library, lucide icons, toast notifications, theming |
| HMI interaction | dnd-kit for drag-and-drop action ordering; hash routing; touch-optimised scrolling |
| Desktop packaging | Electron with electron-builder, producing Linux .deb and AppImage artefacts |
| Device tuning | Chromium GPU rasterisation and touch-input switches for ARM panel hardware |
| Mobile framework | React Native with React Navigation |
| Mobile offline layer | AsyncStorage local cache and sync queue, network state detection |
| Mobile UI | NativeBase component library, vector icons, gesture handler |
Technical Challenges & Solutions
| Challenge | Our Approach |
|---|---|
| Control logic locked inside a vendor toolchain, so nobody changes it | The complete device behaviour — I/O, sample rates, transforms, derived values, alerts, actions and protocol mappings — is modelled as a structured configuration document authored through the interface and interpreted at runtime, removing compilation and redeployment from the change cycle entirely. |
| Raw analog readings meaningless without site-specific scaling | A dedicated channel tier converting raw readings to engineering units, configured either from parameterised templates with strict validation or authored directly, with in-place test execution so the calibration is verified during commissioning. |
| Inputs sampled at different rates but every consumer needs one consistent picture | Independent asynchronous sampling per input into a lock-protected snapshot, with a separate aggregation cycle publishing one complete, coherent set of values to the interface, the historian and the protocol registers simultaneously. |
| Decisions that depend on trend rather than instantaneous value | A device-local time-series historian, with calculated variables and alert conditions able to request a bounded window of past readings — keeping trend-based logic working at sites with no dependable network. |
| Alarms driving physical machinery, where repeated firing is unacceptable | Alert trigger state is latched until the condition clears so an action sequence executes once per crossing, with sequences explicitly ordered and reordered by dragging, because the order in which outputs are driven is a property of the plant. |
| Varying hardware with fixed-function and mode-switchable pins | A per-version, per-model port catalogue describing each pin's capability, driving the interface so only configurations the installed hardware can perform are offered. |
| Feeding an existing control room without custom integration | Computed engineering values published over a standard industrial protocol as floating-point register pairs with defined encoding, so third-party clients integrate through configuration alone. |
| Developing and demonstrating an interface for hardware that is not on the desk | A complete simulation path following the same code as the live runtime when the hardware library is unavailable, enabling development, demonstration and pre-wiring commissioning walkthroughs. |
| Shipping a two-language stack to ARM Linux devices as one installable | A native package that provisions the runtime environment, installs a service unit under a dedicated account, detects the system user rather than assuming one, and is supervised by the desktop application so the operator launches a single thing. |
| Field staff recording readings at sites with no connectivity | An offline-first mobile client with a local cache, an explicitly flagged and counted sync queue, automatic reconciliation on reconnect or foreground, and manual retry — so pending work is always visible rather than implicit. |
Security & Reliability
Least-privilege service execution. The edge runtime is installed as a service under a dedicated account created at install time, rather than running as a privileged user, with a restart policy for unattended recovery.
Single-instance protection. A PID-based guard prevents a second runtime from starting and contending for the same hardware I/O — a real failure mode on a device that may be started by both a service manager and an operator.
Error isolation per loop. Sampling loops, the aggregation cycle, transform execution, alert evaluation and individual actions each contain their own failure handling, so a failing sensor, a malformed transform or one unavailable output does not stop acquisition or prevent the remaining actions in a sequence from running.
Graceful degradation. The runtime starts and remains useful when the hardware library or the historian is unavailable, reporting component health rather than failing to start.
Explicit system health reporting. The interface surfaces the state of the hardware library, historian connectivity, acquisition status and protocol server status, so a technician can tell at a glance which part of the stack is unhappy.
Hardened desktop shell. The application renderer runs with context isolation and without Node integration, and developer tooling shortcuts are suppressed on operational panels.
Configuration resolution and initialisation. The runtime resolves its configuration from a system location where available and falls back to a local copy, seeding a default on first run so a fresh install starts in a known state.
Bounded resource use. Short historian retention, capped history queries, a pre-allocated register block and a bounded aggregate emission rate keep long-term resource consumption predictable on flash-backed embedded storage.
Field data integrity. Unsynced mobile entries are explicitly flagged and counted, synchronisation reconciles against existing records rather than blindly appending, and failed synchronisation surfaces a clear choice to the operator instead of silently discarding work.
Scalability & Performance
Sampling sized to the sensor. Each input runs at its own interval, so a fast-changing signal is not held back by a slow one and a slow-changing signal does not consume cycles it does not need.
One aggregate emission per cycle. Publishing a single complete snapshot rather than an independent message per input bounds websocket traffic, historian writes and interface re-renders regardless of how many points are active.
Bounded history queries. History-dependent calculations request an explicit, limited window rather than unbounded ranges, keeping query cost predictable on device-class storage.
In-place register updates. The protocol server maintains a pre-allocated register block updated in place from each snapshot, so external polling never contends with acquisition.
Selector-level interface subscriptions. Live values flow into a central store consumed through memoised selectors, so a continuous data stream updates individual cards without re-rendering the dashboard.
Deliberately lightweight live charting. A purpose-built chart component keeping a small rolling window is used on the hot path rather than a full charting library, which matters on low-power ARM display hardware.
Rendering tuned for the target device. GPU rasterisation, zero-copy and touch-input settings are configured explicitly for panel hardware rather than left at desktop defaults.
Scaling by replication. Each site is an identical, independently installable unit with its own storage and configuration, so growth is a matter of provisioning devices rather than scaling a shared tier.
Business Outcomes
- Site logic can be changed by the people who run the site, through a guided interface on the panel, without a specialist, a proprietary environment or a redeployment.
- Calibration is verified at commissioning, because a transform can be executed against the live device and its result inspected before it is committed.
- Remote sites are observed continuously rather than only at the moment an operator happens to be standing in front of them.
- Trend-based decisions are possible at the edge, because history lives on the device and does not depend on a working uplink.
- Alarms drive plant predictably, firing once per crossing with an explicitly ordered action sequence rather than chattering while a condition persists.
- The existing control room consumes the data unchanged, through a standard industrial protocol carrying engineering values rather than raw counts.
- Commissioning and demonstration no longer require hardware on the desk, because the runtime simulates the full path.
- A site installs as one package, with the runtime environment, service and desktop application provisioned together and the service supervised by the application.
- Manual field readings reach the back office reliably, captured offline with a visible pending count and reconciled automatically when connectivity returns.
Why it worked
Industrial edge software fails in a specific way: it is built as if the site were a data centre. It assumes a network, assumes a specialist will be available to change things, assumes the surrounding systems will adapt to it, and assumes that if something goes wrong someone will notice. None of those assumptions hold at a remote installation, and software written on them gets bypassed, worked around or left running with logic nobody dares to touch.
Our team built for the site as it actually is. We made the device's behaviour configuration rather than firmware, because the alternative — a specialist and a redeploy for every threshold change — is why so many installations end up frozen. We gave every value a defined provenance across three explicit tiers so a wrong number can be traced to the stage that produced it. We put the historian on the device, because trend-based control that depends on an uplink is not control. We latched the alarms and ordered the actions, because the outputs are wired to real machinery. We published to a standard protocol in engineering units so the control room needed no work at all. And we built a complete simulation path, because commissioning software you cannot run without the hardware in front of you is commissioning software nobody tests.
Our teams work across Python and asyncio services, embedded Linux and industrial hardware integration, industrial protocol implementation, time-series data, React and TypeScript operator interfaces, Electron packaging for ARM devices, and offline-first mobile applications — with the engineering judgement to know which parts of a plant process must be modelled exactly and which can be left to configuration.
Final Summary
Distributed water infrastructure has historically been monitored by visiting it, and automated — where it is automated at all — by logic sealed inside a vendor toolchain. Both problems have the same consequence: the system stops reflecting how the site actually runs, and nobody has an economical way to fix that.
Our team built an edge platform where the device's entire behaviour is configuration. Which inputs are read and how often, how a raw analog reading becomes a level in engineering units, which derived values are computed from history, what raises an alarm, which outputs it drives and in what order, and which values are published to the control room — all of it is authored through a guided interface on a panel display and interpreted at runtime by a Python service on the controller itself. Values move through three explicit tiers so each one is traceable, transforms can be tested against the live device before they are committed, and a full simulation path means the whole system can be exercised without hardware present.
Around that sit the things an unattended industrial installation requires: sampling sized per sensor with coherent aggregate snapshots, a device-local time-series historian so trend-based logic survives an unreliable network, latched alarms with explicitly ordered action sequences driving real plant, a standard industrial protocol server publishing engineering values into existing SCADA and HMI systems, per-loop error isolation and graceful degradation, and a single native package that installs the runtime, the service and the operator application together. A companion offline-first mobile application covers the manual rounds that continue alongside the automation, queueing entries locally and reconciling them when a signal returns.
The result is an installation that the people responsible for it can actually change — which, for infrastructure expected to run for years without a specialist visit, is the difference between a system that stays useful and one that quietly stops matching the plant.