HotLoop core 4.18.11 is out. Versions 4.4.0 through 4.15.3 were never published on their own.
They were merged one at a time and never tagged, so there was nothing to download until
4.16.0, which shipped every one of them at once. Each entry is still marked the way
it happened, so nobody has to guess which versions were real releases.
Every message now carries a timestamp from the PLC's clock, and the library installs through the CODESYS Installer from a .package.
Timestamps, on by default. The NetworkMessage and the DataSetMessage each carry the same DateTime, read from the PLC's real-time clock once per message. A message is 17 bytes longer, and the fields and values did not move.
bTimestamp := FALSE sends exactly the 1.0.0 bytes. Pin that if a receiver of yours refuses timestamps. The lab checked it.
The resolution is one second. The clock is read with SysTimeRtc, which is whole seconds, so a receiver sees the same stamp on every message inside a second. Sub-second stamps are on the roadmap.
A new output, diLastClockResult. If the clock cannot be read the message goes out unstamped, which Part 14 allows, and this says why.
A new system library, SysTimeRtc, which the runtime provides. Control for Linux SL 4.22.0.0 does. A target without it cannot use 1.1.0 with timestamps, so stay on 1.0.0.
A CODESYS .package, made by CODESYS's own PackageManagerCLI, that puts the same library into the repository through the Installer. It is the same library as the .library file.
A subscriber that wants to know when a value was sampled now gets it from the packet instead of guessing from when it arrived, and you can install the library through the CODESYS Installer like any other add-on.
What was run, on The Beast against CODESYS Control for Linux SL 4.22.0.0 in a container: the library and the example build with 0 errors and 0 warnings in CODESYS 3.5.22.30. Two publishers, Variant and RawData, with timestamps on, and every value equals what the PLC reports over its own OPC UA server. Every message has both timestamps, the same instant, never going backwards, and over an 11 second capture the stamp moved 11 seconds. open62541 1.4.9 reads both timestamps of all 113 messages of each writer. HotLoop core's subscriber, from its main branch and not in a core release yet, reads both publishers with every reading carrying the publisher's timestamp as its time. The package installs through PackageManagerCLI and the example builds against it. On a 20 ms task with two publishers the block adds about 9.6 microseconds per cycle on average, and the longest cycle in the run was 81 microseconds.
What was not run: any physical controller, any runtime but Control for Linux SL 4.22.0.0, a controller whose real-time clock is unset or wrong (the block would stamp what the clock says), the package in the IDE's own Package Manager window, multicast, a runtime that loses and regains its network, and the end of the two hour demo window.
Not in this release: KeepAlive and Delta Frames, more than one DataSetMessage per NetworkMessage, WSTRING, a per-field status, sub-second timestamps, message security, and the subscriber side. A PLC project pinned to 1.0.0 keeps working and keeps sending the 1.0.0 bytes.
Download `HotLoopTransmitter-1.1.0.library`, the `.compiled-library` or the `.package` from [hotloop.io/hotloop-transmitter/1.1.0/](https://hotloop.io/hotloop-transmitter/1.1.0/HotLoopTransmitter-1.1.0.library), check them against `SHA256SUMS` in the same folder, and see the [install page](https://docs.hotloop.io/transmitter/install/).
The first HotLoop Transmitter release: a CODESYS library that puts a PLC's values on the wire as OPC UA PubSub, from about fifteen lines of Structured Text, with nothing else installed on the controller.
One block, FB_HLTX_Publisher, sends one UADP NetworkMessage per publishing interval, from Structured Text over SysSocket UDP. Every message is a Data Key Frame, so it holds every field.
Variant and RawData encodings, picked per publisher. BOOL through LREAL, STRING and TIME, and arrays of all of those but STRING, up to 128 fields and 1472 bytes a message. A message never fragments.
Unicast or multicast, with publisher id, writer group, dataset writer, interval and versions as inputs.
A non-blocking send. A bad address, a dead socket or a field list that is too big becomes an error state you can read, never a fault in your application.
Run on a container runtime and read back by four different readers, including HotLoop core's subscriber. Not run on a physical controller, and multicast was not run.
Add the library, declare one function block, add the variables you want out, and call it once per cycle. Anything that subscribes to OPC UA PubSub can read it, HotLoop included.
What was run: all of it on The Beast, against CODESYS Control for Linux SL 4.22.0.0 in a container. The library builds with 0 errors and 0 warnings in CODESYS 3.5.22.30. Two publishers, one in each encoding, seventeen fields each, and every value on the wire equals what the PLC reports over its own OPC UA server, with no gap in the sequence numbers. open62541 1.4.9 reads the Variant message in full and the RawData scalars, and cannot read a RawData array or a long RawData string at all, which is its limit and not the library's. HotLoop core's subscriber (from core's main branch, not in a release yet) reads both publishers, including the string, the TIME, both arrays and two counters that move. A second Go decoder reads all 222 captured datagrams. The block adds about 10.6 microseconds per cycle on average on a 20 ms task, a cycle that sends costs 10 to 100, and the longest send in 12 000 cycles took 14.3 ms on a busy shared host, so put the publisher in its own lower priority task if that matters to your control logic.
What was not run: any physical controller, any runtime but Control for Linux SL 4.22.0.0, any CPU but x86-64, multicast, a runtime that loses and regains its network, the end of the two hour demo window, and receivers other than the four above.
Not in this release: timestamps, KeepAlive and Delta Frames, more than one DataSetMessage per NetworkMessage, WSTRING, a per-field status, message security, and the subscriber side. A receiver that insists on a timestamp, a KeepAlive or a Delta Frame will not be satisfied by this version.
Download `HotLoopTransmitter-1.0.0.library` from [hotloop.io/hotloop-transmitter/1.0.0/](https://hotloop.io/hotloop-transmitter/1.0.0/HotLoopTransmitter-1.0.0.library), check it against `SHA256SUMS` in the same folder, and install it with Tools, Library Repository, Install in CODESYS 3.5 SP22 or later. The `.compiled-library` is the same library without its source. The [install page](https://docs.hotloop.io/transmitter/install/) has the rest.
2.8.0 with the arm64 image added, and nothing else. There is no change to the code, the chart, the settings or your flows.
The image is built for linux/amd64 and linux/arm64 again, and the 2.8.1, 2.8 and 2 tags hold both.
Nothing else changed: the README and install pages name 2.8.1.
HotLoop Flow 2.8.1 is 2.8.0 with the arm64 image added, and nothing else. 2.8.0 shipped amd64 only so it could get to production and read stable first, and this is the one small bump that builds both. There is no change to the code, the chart, the settings or your flows.
## What changed
- The image is built for linux/amd64 and linux/arm64 again. The `2.8.1`, `2.8` and `2` tags hold both.
- The README and the install pages name 2.8.1 and say amd64 and arm64, as they did before 2.8.0.
## New settings
None.
## Upgrade
A plain `helm upgrade` to the 2.8.1 image and chart, or the new tag in your Quadlet unit. If you are on arm64 and stayed on 2.7.1 while 2.8.0 was amd64 only, this is your move to the analysis nodes, the data acquisition node, the durable MQTT output and the TDMS log: read the 2.8.0 notes for what they are and what isn't proven. Nothing happens on first start and nothing needs migrating.
## What isn't proven
The arm64 image is built the way it was for 2.7.1, under emulation on the runner, and it starts and reports its version in the release run. It has not been run on an arm64 box for this release.
Image: `ghcr.io/hotloop-io/hotloop-flow:2.8.1`, linux/amd64 and linux/arm64. Chart: `https://hotloop.io/hotloop-flow/`. The README at this tag has the rest, with pictures: https://github.com/HotLoop-io/hotloop-flow/blob/v2.8.1/README.md
A workflow can sign and verify things without a secret ever sitting in item data, and a public form on another website can call a webhook from the browser. Nothing else changed from 1.0.1.
The Code step gets crypto helpers, computed in Go: sha256, hmacSHA256, randomHex, base64Encode, base64Decode, and safeEqual. The script still has no require, no network, and no files.
The Code step takes an HMAC key credential, a new built-in credential type with one secret field. The script reads it as credential.key, the key lives in the vault, and the vault scrubs it from everything a run saves.
A webhook trigger takes corsOrigins, the web origins whose pages may call it from a browser. Cascade answers the preflight itself without starting a run. A webhook that lists none behaves exactly as before.
No migrations.
The reason is a public form that checks a proof of work with an HMAC. Before this, the only way to do that in a workflow was to put the key in an item or a variable, and both end up in stored runs or in plain text. Now the key is a credential and the stored run has the signature and nothing else.
What isn't proven: the full local suite did not run for this release. Only the targeted tests ran (the Code step's helpers and credential, the webhook's CORS settings and preflight, and the catalog and workflow packages), all passing on The Beast, because the lab's MQTT broker would not start on that box.
Upgrade with `helm upgrade` to 1.0.2, or let `:stable` roll the Quadlet units. Workflows that never touch the new helpers behave exactly as before. A workflow that calls a new helper fails on 1.0.1 with the helper undefined, so put 1.0.1 back only for the others.
FlowLab gets analysis nodes, a data acquisition node for NI hardware, an MQTT output that keeps what the broker misses, and a TDMS log. Every flow you have runs the way it did.
Statistics, integral, derivative and Butterworth filter nodes that work on an array of readings.
A data acquisition node that reads NI hardware through NI's gRPC Device Server. It has been run against a stand-in server and not against NI's server or hardware.
A durable MQTT output: a message the broker misses is written to disk and sent in order when the broker is back, even across a restart.
A TDMS log, with a reader and writer written from NI's public format documentation. It has not been run against a file NI's own software wrote.
28 node types in the FlowLab section of the palette, up from 21.
HotLoop Flow 2.8.0 adds four things to FlowLab, the diagram tab for test benches and data loggers: analysis nodes, a data acquisition node for NI hardware, an MQTT output that keeps what the broker misses, and a TDMS log. Flows run exactly as they did, a flow file you have today loads and saves byte for byte, and a tab that isn't a diagram doesn't see any of it.
## What's new
**Analysis.** Four nodes work on an array of readings: statistics (mean, RMS, smallest, largest and standard deviation), integral (the area under the array by the trapezoid rule, as one number or as a running area), derivative (the slope at each sample), and a Butterworth filter of order 1 to 10 as a lowpass, highpass, bandpass or bandstop, with the cutoff in hertz. The code is in `internal/diagram/dsp`, with tests against values worked out by hand.
**A data acquisition node.** `read DAQ task` reads samples from NI hardware, such as a CompactDAQ chassis, through NI's gRPC Device Server. The client is about as small as it can be: Go's own HTTP/2 with hand-written messages, so the binary didn't take on a gRPC library. NI's published protocol definitions are in `third_party/ni-grpc-device` under their license, with a notice in NOTICE.
**An MQTT output that loses nothing.** `mqtt out` publishes through the broker the flow file configures, the same connection the MQTT nodes use. Mark it durable and a message the broker misses is written to disk before the node finishes and sent, oldest first, when the broker is back, even across a restart. Nothing overtakes anything waiting ahead of it. Its sent output says whether the message went straight out.
**A TDMS log.** `log to TDMS file` appends a record to a TDMS file, one channel per field, and the file opens in the tools a lab already has. The reader and writer are in `internal/tdms`, pure Go, written from NI's public format documentation and none of their code. A file whose last write was cut short by a power loss still reads up to the last whole value.
## What changed
- There are now 28 node types in the FlowLab section of the palette, up from 21, and the README says so.
- A test that fails in the CI collector step now prints its output. A step that failed once in the 2.7.1 run printed nothing, and the cause was the collector test comparing a span before the log it reads had all of it; it waits for the whole span now. The OPC UA simulator in CI listens on 4840 and not 50000, a port the kernel hands to outgoing connections. Two more tests were made to wait for what they check. None of that ships.
- The README's pictures were retaken so the top bar says v2.8.0.
- FlowLab grew by about 290 KB in the binary, 41,402,530 bytes at 2.7.0 and 41,693,346 with these four, built the same way on The Beast. The binary and image sizes in the README were read again for this release.
## New settings
None.
## Upgrade
On amd64, a plain `helm upgrade` to the 2.8.0 image and chart, or the new tag in your Quadlet unit. On arm64, stay where you are until 2.8.1, which is this release with arm64 added and nothing else. Nothing happens on first start and there is nothing to migrate.
## What isn't proven
The TDMS reader and writer have not been run against a file written by NI's own software, because I had none. They are checked against the byte layouts in NI's documentation, against my own writer, with fuzzing, and by reading a file this writer made with nptdms 1.13.0, an independent library used as a black box.
The DAQ node has not been run against NI's server or any NI hardware. It is tested against a stand-in server that speaks the same protocol, written from NI's definitions, and a stand-in proves the messages, not the device.
The durable MQTT output can deliver a message twice when a send timed out while the client still believed the link was up, and I forced that on a real broker to check the count. It never loses one that I could make it lose.
What a FlowLab diagram can't do yet: a case structure, records and shared types, waveforms, instrument sessions, sub-diagrams, serial flow control, `hotloop-flow test` and CI checks for diagrams, event handlers, an operator only panel, a panel layout editor, and a diagram as an MCP tool.
Image: `ghcr.io/hotloop-io/hotloop-flow:2.8.0`, linux/amd64 only. I build amd64 first, let it read stable in production, and then one small 2.8.1 with nothing else in it adds arm64. Until then the `2` tag holds the new amd64 image and the arm64 image from 2.7.1, so a box on arm64 that follows `2` or `2.7` keeps what it has, and a box pinned to `2.8.0` needs amd64. Chart: `https://hotloop.io/hotloop-flow/`. The README at this tag has the rest, with pictures: https://github.com/HotLoop-io/hotloop-flow/blob/v2.8.0/README.md
2.7.0 with the arm64 image added, and nothing else. There is no change to the code, the chart, the settings or your flows.
The image is built for linux/amd64 and linux/arm64 again, and the 2.7.1, 2.7 and 2 tags hold both.
Nothing else changed. If you are on arm64 and stayed on 2.6.0 while 2.7.0 was amd64 only, this is your move to FlowLab.
2.7.0 shipped amd64 only so it could get to production and read stable first, and this is the one small bump that builds both. A plain `helm upgrade` to the 2.7.1 image and chart, or the new tag in your Quadlet unit. Nothing happens on first start and nothing needs migrating.
What isn't proven: the arm64 image is built under emulation on the runner, and it starts and reports its version in the release run. It has not been run on an arm64 box for this release.
Image: `ghcr.io/hotloop-io/hotloop-flow:2.7.1`, linux/amd64 and linux/arm64. Chart: `https://hotloop.io/hotloop-flow/`.
The first cut of FlowLab: a second way to run a tab for test benches and data loggers, with loops, an error wire, a serial port, a panel an operator can open on a phone, and a debugger. Every flow you have runs the way it did.
FlowLab diagrams: a tab where every node runs once, when each of its wired inputs has a value, and nodes with no wire between them run at the same time. Add one with the FlowLab button next to the subflow button.
While and for loops that carry a value from one pass to the next, and an error wire: a node with an error coming in does nothing and passes it on, and an error wired into a loop stops it with the source and a code.
A serial port that survives a pulled cable, a reading parser, stable-value detection, and a log to a text file. The example in the repository reads a scale and shows it on a panel.
A panel at /panel/<diagram id> for controls, indicators and charts, a run bar with highlight execution, probes, breakpoints and step, and 21 new node types in the FlowLab section of the palette.
A sessions file that exists but can't be read is no longer overwritten by the next sign-in.
This is the first cut of FlowLab, and it is not all of it. A diagram is a tab where every node runs once, when each wired input has a value, which is what a test bench or a data logger needs: a loop that carries state, an error that travels on a wire of its own, a panel you can open on a phone, and a debugger that can pause a run and show what is on every wire. Flows run exactly as they did, and a flow file you have today loads and saves byte for byte.
The image is amd64 only for this release. I build amd64 first, let it read stable in production, and then one small 2.7.1 with nothing else in it adds arm64. Until then the `2` tag holds the new amd64 image and the arm64 image from 2.6.0, so a box on arm64 that follows `2` or `2.6` keeps what it has. A plain `helm upgrade` is the upgrade on amd64, there is nothing to migrate, and the one new permission, `diagram.run`, matters only if your users have explicit permission lists.
What is not here yet: a case structure, records and shared types, waveforms, TDMS and binary logs, a database or MQTT write, instrument and data acquisition nodes, filters and statistics, sub-diagrams, serial flow control, and checks for diagrams in `hotloop-flow test`. It has been run against a program that writes scale frames on a pseudo terminal, not a physical scale, and not for a day on a Pi. FlowLab does not do hard real time: work with a deadline belongs in a PLC.
One fix. When the database could not be read, sign-in said you were not signed in and cleared your cookie. Now it says to try again and keeps your session.
A session check, an API key, a SCIM token, and a key's user used to read a database that blinked as an invalid login and answer 401, and the session check also cleared the cookie, so a short outage signed everyone out. A store that cannot be read now answers 503 with a Retry-After and the cookie stays.
The second factor had the same flaw: during an outage a good authenticator code, recovery code, or passkey was answered as a wrong code, which can lock a person out of their own account. Those steps answer 503 for a store or server error and keep 401 for a real refusal.
Tests for each of these fail on 1.0.0 and pass on 1.0.1. There are no migrations.
If you run Cascade behind sign-in, this is the update that stops a database hiccup from logging the whole plant out. Nothing else changed from 1.0.0.
Upgrade with `helm upgrade` to 1.0.1, or let `:stable` roll the Quadlet units. Nothing in the database changes, so putting 1.0.0 back is safe. A backup before any update is still the right habit.
A new editor, and the identity and governance work behind it: SAML, LDAP and Active Directory, SCIM, custom roles, outside secrets, log streaming, variables, folders, insights, and redaction policies. The EmberOps steps are gone.
A new editor in HotLoop's own colors, light or dark, still baked into the binary and fetching nothing. Every screen the old one had is here, the canvas included, and your saved links still open the same screens.
Sign-in for a plant with an identity system: SAML tried against Keycloak, LDAP and Active Directory tried against OpenLDAP and a Samba domain controller, and SCIM tried against scim2-tester so a provider can make and switch off accounts.
Custom roles, external secrets in OpenBao or HashiCorp Vault, log streaming of the audit log and finished runs to syslog and OTLP, variables, folders, insights, run annotations, and redaction policies.
S3 and Azure blob stores, tried against Garage and Azurite, and not against AWS S3 or Azure itself.
The EmberOps steps are removed. A saved workflow that uses one fails to validate and says why, and the HTTP step reaches EmberOps's REST API.
Breaking
A workflow that uses an EmberOps step (the nine steps or the trigger) will not start. Delete the step or replace it with an HTTP step and it runs again. An EmberOps credential stays in your list, unused.
Migrations 0018 to 0025 run on the way up from 0.1.2. They only add tables and one column. A rollback puts the old image back and does not undo them, and the old image has not been run on the new schema, so take a backup first.
Pull it with `podman pull ghcr.io/hotloop-io/hotloop-cascade:1.0.0`, or `helm install cascade oci://ghcr.io/hotloop-io/charts/hotloop-cascade --version 1.0.0`. The image is amd64 and arm64.
I'm calling it 1.0.0 because the editor was the last piece I wanted before I put my name under the number. It is not a promise about the workflow file format or the API yet, and the roadmap says what is still not built: Git source control and environments, more connectors, and AI steps. Everything in 0.1.2 works the way it did.
Variables are not secrets: anyone who can read a project reads its variables. External secrets need allowed paths, and a provider with none is refused on purpose. SCIM never shows the instance owner to a provider, and a DELETE turns an account off and hides it. SAML, LDAP and OIDC settings are read from the environment at start, so the sign-in screen shows what is on and does not change it, and the sign-in page itself is still the plain one from before.
A durable wire that stops delivering now says so: a counter you can alert on, and one log line with the numbers. Nothing about how your flows run changes.
A new counter, hotloop_flow_durable_wire_stranded_total, goes up when a durable wire's pump finds nothing to deliver while the wire says messages are waiting, and it stays that way past a recheck. Zero on a healthy wire, so anything above zero is worth a page.
One warning in the runtime log the first time it happens, with how far the pump got, the depth, the rows on disk, what is in flight, and how many were appended, delivered and dropped.
I added it because a drop-oldest wire failed to drain once in a few thousand test runs and I could not catch why. This release is the instrument, not a fix.
If you run durable wires on a plant line, this release gives you a way to know when one has stopped delivering. Nothing about how your flows run changes.
It is a plain image bump from 2.5.0. There is nothing to migrate and no new settings; the counter is on, and there is no switch.
The counter is tested by making the fault by hand, and the test fails with the pump's check removed. It has never fired on a real wire, because I have never seen the real fault, and the cause of the drop-oldest failure is still not found. If it ever fires on your line, the log line is what I need. The full notes are on [the GitHub release](https://github.com/HotLoop-io/hotloop-flow/releases/tag/v2.6.0).
The database is PostgreSQL 17 and every box with 16 data moves itself on its next upgrade. Also SNMP and BACnet/IP read-only, kiosk sign-in, alarm escalation, and a UI that installs on a phone.
PostgreSQL 17, with the upgrade built in. PostgreSQL won't open another major version's data directory, so a bare image change would have left every existing box with a database that can't start. A script copies your data across with pg_upgrade before the database starts, proves the new server opens it, and only then swaps the names. The old directory stays as pgdata.pg16, which is your way back, and it is yours to delete. Proven on real 4.18.10 data for the Gateway, Edge and IoT, on the chart and on Quadlet, with every row kept.
Integrations: point HotLoop at a device you know the address of. One flow for every kind of device. Ask what it needs, look at the real thing, keep the points you want. Nothing it makes is armed, and a setup that fails part way leaves no device behind.
SNMP v1, v2c and v3, read-only, with no default community. BACnet/IP, read-only, for building controllers and meters. HotLoop never sends a SET and never writes a BACnet property. Proven against net-snmp and recorded BACnet frames in the lab, not on a physical switch or controller.
Kiosk sign-in: a panel asks for a PIN or a badge, every write through the gate is that person's, and an idle panel signs out on the page and on the server.
Alarm escalation: an alarm nobody acknowledges goes to the next person, and an acknowledgement from anywhere stops it. Off until you turn it on.
The UI installs on a phone and opens with no connection. It shows the last answers with the time they are as of, and every write is refused before it leaves the page.
Fixed: the live feed closes when its session does, history times come back in UTC everywhere, and an edge node that reconnects announces itself once instead of twice.
Breaking
helm rollback to 4.18.10 will not start its database, because 16 won't open a 17 directory. Nothing is lost. Getting it back is a documented directory swap that drops every write made on 17. Roll forward when you can. Quadlet boxes are not hit, because auto-update only rolls the app back.
The database upgrade needs about as much free disk as your data takes, and the database pod sits in Init for as long as the copy lasts. Take a backup on 4.18.10 before you upgrade.
Ten wrong passwords or PINs in a row now lock that one account for 5 minutes, on every install. A script that retries a wrong password will start getting 429. HOTLOOP_AUTH_LOCKOUT_ATTEMPTS=0 turns it off.
Migration 0025 runs on start. It only adds columns, every one with a default, so 4.18.10 runs on the schema it leaves.
All four products ship from this tag in lockstep, so the Edge Relay is 4.18.11 too, with no database and nothing to do. This is the release where the database engine changes under a running plant, so the first thing to read is the upgrade, not the feature list: [Upgrading from 4.18.10](https://docs.hotloop.io/gateway/upgrading/#upgrading-from-41810) says what the first start does, how long to expect it to take, and the one way back that isn't a plain rollback.
What isn't proven yet. The directory swap back after a rollback is documented, not run by a test. IoT's plain PostgreSQL directory has no TimescaleDB extension in it, so the move up a product on a 17 volume hasn't been run again on 17. SNMP and BACnet/IP have not touched a physical switch, UPS, PDU or controller, so treat the first read of a real one as commissioning and compare the probe's sampled values with the device's own display. Everything the 4.18.10 notes list as unproven still is: Edge and IoT as live Quadlet units under systemd, a real Pi, and the 32-bit arm images on real hardware.
A new editor. Same flows, same engine, same shortcuts, and it looks like it belongs next to the rest of HotLoop. The one cost is a blank page for a moment while it loads.
The editor looks like HotLoop. Same colors and type, light and dark following your system, and orange means you can click it and nothing else.
Debug and runtime moved to a dock under the canvas, so a debug line gets the whole width instead of a skinny column. History, Tests and Trace stay on the right, and the one you are working in comes to the top.
A trace is drawn in blue now, and the review before a deploy marks added, changed, moved and removed nodes in green, amber, blue and red, so a trace and a selection stop sharing a color.
A double-click right after an edit lands. The editor used to redraw every wire after the type check even when nothing changed, and a double-click could hit a wire being swapped out mid-click.
Nothing about your flows changed. A flow file loads and saves byte for byte, and all 60 browser tests pass untouched.
If you wire plants for a living, this is the release where the editor stops looking like it came from a different company. Same flows, same engine, same shortcuts. The difference is a screen that fits what you do all day: put the thing on the canvas, deploy it, and watch it run.
It is a plain image bump from 2.4.0. There is nothing to migrate and no new settings. Reload the editor tab once after the pod restarts so your browser stops showing the old one.
One thing is slower, and I would rather say so. The page is blank for a moment until the editor has loaded, where the old one painted its frame first. With the browser cache off, the sign-in form took about 12 ms longer on one machine, about 100 ms longer over a 20 Mbps link, and about half a second longer over a 1.6 Mbps link with 150 ms of latency. On a plant LAN it is a few milliseconds. On a bad link you will see a white page for a beat.
I have not opened this editor in Firefox or Safari, on a tablet, on a plant HMI screen, or inside the live EmberCORE dashboard frame. The browser tests and the screenshots run in Chrome. The full notes are on [the GitHub release](https://github.com/HotLoop-io/hotloop-flow/releases/tag/v2.5.0).
The EmberCORE dashboard signs people in to Flow, with the role their tenant gave them and no second login. It is off unless the dashboard's key is configured, and nothing that worked before changes.
Sign-in from the dashboard. The dashboard puts a token it signed in X-Embernet-Assertion on every request it proxies to Flow, the editor's websocket included. Flow checks the signature (ES256), the issuer, that the token was made for this Flow in this tenant, and its expiry, about a minute. Wrong on any of those and it is refused, and nothing else on the request rescues it.
Roles come from the tenant. A full-access role gets every permission. read gets the flows, the palette, the settings and the status, and nothing that changes them: no deploy, no inject, no deployment log. Operators and Viewers get read by default, and a role that is neither, or none, is no access. The deployment log and the audit trail show the person as dashboard:<email>.
No session to revoke. The dashboard sends a fresh token on every request, so a role changed there lands on the next request. The editor says to open it from the dashboard instead of offering a sign-in form, and GET /auth/mode is how it asks before anyone has signed in.
Off unless you give it a key. With no key file Flow never reads the header, a helm template of the defaults is what it was, and the admin login and API tokens keep working as the break-glass for staff and automation.
If you run Flow behind the EmberCORE dashboard, this is the one that gets rid of the second login. Someone opens Flow from the dashboard and they are in, with the role their tenant gave them, full access or read-only. A viewer cannot deploy, and nobody has to remember a Flow password.
If you don't, nothing happens. With no key file Flow does not read the header at all, so a plain image bump from 2.3.0 changes nothing on first start. The chart takes the whole thing under embernetSignin, the App Store hands it the dashboard's public key, and the chart mounts it, writes the settings, rolls the pod, and annotates the Service so the dashboard knows to sign people in.
To use it by hand, mount the dashboard's public key (a PEM file) and set auth.assertion.publicKeyFile and auth.assertion.audience, or HOTLOOP_FLOW_ASSERTION_PUBLIC_KEY_FILE and HOTLOOP_FLOW_ASSERTION_AUDIENCE. Flow refuses to start with a key it can't read or with no audience, because a half-configured sign-in that quietly lets everyone through is worse than one that won't boot. The other settings are auth.assertion.issuer, header and hideLogin.
It has not run behind the live dashboard, in a Fleet install, or in a person's browser outside the CI suite. It ran against a real Flow binary behind a signer built from the dashboard's own code, with every refusal exercised and the websocket handshake checked. Treat the first real install as the test that matters. The full notes are on [the GitHub release](https://github.com/HotLoop-io/hotloop-flow/releases/tag/v2.4.0).
Flow talks to HotLoop, an agent can call a flow as an MCP tool, a plugin is a signed WebAssembly file, and a wrong-shaped message gets caught before it does damage.
HotLoop nodes. A connection to a HotLoop is a URL and an account. hotloop in follows the live feed and sends a message for each entity that changes, hotloop state reads one entity, and hotloop script and hotloop recipe run a script or apply a recipe. HotLoop checks every field, runs every write through its write gate and keeps the record, with the reason flow:<node id>.
Writes through HotLoop's gate. The Modbus, OPC UA, S7 and EtherNet/IP write nodes have a Write through HotLoop option. A write the gate refuses never reaches the equipment, and a HotLoop that can't be reached fails the write instead of falling back to writing directly.
MCP in both directions. mcp tool and mcp result serve a flow to an agent at /mcp, and mcp client calls a tool on any MCP server, HotLoop's included. A token is required, the call runs with that token's permissions, every call and refusal is audited under the caller, and an API token can carry a rate limit.
Plugins without npm. A plugin is a WebAssembly module, a descriptor and an ed25519 signature. Unsigned and tampered ones refuse to load, a guest past its memory ceiling or timeout traps to a Catch node, and a plugin that asks for host access nobody granted fails the deploy. With no trusted keys, nothing loads.
Typed wires. Give the json node a JSON Schema and a wrong-shaped value goes to Catch with every bad field named. Nodes can declare what they accept and what each output promises, and a deploy is refused when no message could satisfy both ends of a wire. An inject that sets a literal the next node can never take is refused the same way. The editor draws the type on each wire.
Three durable queue fixes: a removed durable wire refuses a late write and stops waiting on a stuck target, a drop-oldest wire no longer evicts a message twice, and a late hand-back no longer stays charged to the quota.
Nine more node types, 79 in all. The image is 44.8 MB for amd64, built from the release tree and read with podman image inspect, and the README and docs carry Raspberry Pi 5 numbers.
Breaking
The json node used to ignore msg.schema. Now a message that carries msg.schema is checked against it, and a mismatch goes to a Catch node instead of down the wire. A flow that leaves a msg.schema on its messages for some other reason will see that.
From 2.2.0 it's a plain image bump, with no migration and no flag, and a flows.json and config file that ran on 2.2.0 run on 2.3.0. A flow with no schemas anywhere behaves exactly as it did, and a write node with no HotLoop picked writes the way it did in 2.2.0. Going back to 2.2.0 is safe for any flow that doesn't use the new nodes. A HotLoop, MCP or plugin node fails to start there, and a write node with a HotLoop picked writes straight to its equipment on 2.2.0, so take the HotLoop off those nodes before you go back.
MCP is a new way into Flow and it takes a token. Nothing is served until a flow has an mcp tool node, and a call with no token or a made-up one is refused. Plugins are off until you give the runtime a trusted key, and the chart takes all of it under plugins with enabled false by default.
It has never run on a real plant floor. Only the Modbus write was run through a real HotLoop gate, on the published HotLoop 4.18.10 image in CI. The OPC UA, S7 and EtherNet/IP writes send through the same code. The S7 and EtherNet/IP nodes have still only met simulators. The HotLoop reconnect is tested against a stand-in, MCP has no OAuth, stdio, resources, prompts, or sampling, and plugins have never run on a Pi. The schema validator is not Ajv: it refuses a $ref outside the schema and ignores format values it doesn't know. Flow now has Raspberry Pi 5 numbers, and a Pi 4, a Zero 2 W and SD card writes are still not measured. The full notes, with every new setting, are on [the GitHub release](https://github.com/HotLoop-io/hotloop-flow/releases/tag/v2.3.0).
HotLoop stops being one image. IoT and Edge ship for the first time, beside the Gateway and the Edge Relay, all four from one tag with their own images and charts.
Four products, four images, four charts, one tag. hotloop-iot and hotloop-edge are new on ghcr.io and in the chart repository at https://hotloop.io/hotloop. The Gateway keeps ghcr.io/hotloop-io/hotloop and the chart hotloop, so nobody running it changes anything. A product is a build, not a setting: the OPC UA server isn't switched off in the IoT image, it isn't in it.
MQTT discovery: point ESPHome, Zigbee2MQTT, Tasmota or a Shelly at the broker and it shows up, reading. It can't command anything until an admin accepts it and arms each point by hand. Network discovery listens and sends nothing unless you list a network to probe.
Historian targets: every reading HotLoop keeps also goes to the plant's own PostgreSQL, TimescaleDB, SQL Server, InfluxDB or MySQL/MariaDB, the same rows with the same quality.
The database is a setting: SQLite, PostgreSQL or TimescaleDB per product, from the same binary, so a Pi doesn't need a 300 MB database image.
Edge keeps the plant running with the WAN dead, and its store and forward queue survives a boot during an outage, a link that dies without a reset, and a replay that has to outrun the plant.
Every box follows :stable and updates itself, and a release that can't start is rolled back. Migrations only add, and an app refuses to start on a schema two releases ahead of it, so that rollback is always safe.
UniFi on a wall board and a phone: a zooming network map, traces for WAN latency and port traffic, one button that makes the usual alarms, and UniFi events in the logbook under the switch they are about. An agent can ask over MCP for the topology and the console's latest events.
The Edge Relay takes a pushed config live with no restart and no lost Sparkplug session, and discovers devices on a broker like the Gateway does.
The license changed: all four products are under the HotLoop Business Source License 1.1. Home and lab use is free, so is use by schools, nonprofits and government, and business use goes through EmberNet.
Fixed on the way: a discovered device's value could be overwritten by its own placeholder reading that had no value yet, the embedded MQTT broker mishandled packet ids in both directions, and a dashboard that was still loading could put a person back on the one they had just left.
Breaking
Upgrade from 4.17.0 with --reset-then-reuse-values or your values file. Plain --reuse-values renders, but it replays 4.17.0's values and drops every new default.
edgePublish.queueMaxBytes defaults to 64 MiB now, not unbounded. Past it the oldest readings are dropped and counted. Size it from your own reading rate, about 180 bytes a reading, or set it to 0 to keep the old behavior.
Migrations 0016 to 0024 run on start. Migrations only add, and going back to 4.17.0 works, but take a pg_dump first.
Logins made in Settings, MQTT carry topic rules from a profile. Logins from HOTLOOP_BROKER_USERS are untouched. There is a new fleet.update permission, admin only.
IoT, Edge, the Gateway and the Edge Relay all ship from this tag, in lockstep. 4.18.0 through 4.18.7 and 4.18.9 died in the Test job on real bugs, and 4.18.8 passed Test and was canceled by mistake in the middle of publishing, so its four images were pushed as 4.18.8 and no chart and no release ever went out. 4.18.10 supersedes all of them. What isn't proven yet: the IoT and Edge Quadlet units under systemd (only IoT's SQLite unit was run), moving a site up a product as a helm upgrade on a real cluster, discovery against real devices on a bench, and the 32-bit arm images on real hardware.
Flow talks to the PLCs, remembers what it knew when it restarts, and shows you where a message went. It is also the first release under HotLoop's Business Source License.
The license changed. 2.2.0 and every release after it is under HotLoop's Business Source License 1.1. Home and lab use is free, and so are schools, nonprofits and government bodies. Business use is free through EmberNet, which is the only business path. 2.1.0 and everything before it stay under the license they shipped with.
Tracing and replay. Every message gets a bounded trace, and clicking a debug message lights the path it took with timing and the payload at each hop. A trace becomes a flow test, or replays into a draft so you can try a fix on the message that broke it. Hops can go to an OpenTelemetry collector as spans, proven against the real collector image.
State that survives a restart. Flow, global and node context live in a SQLite file, state.db, on the data volume. Mark a wire durable and it spills to disk, in order, only when the node behind it stops taking messages. A real InfluxDB killed for ten minutes, with the runtime killed mid-outage and mid-drain, came back with none missing and none doubled.
Plant nodes. Modbus TCP read and write, OPC UA read, write and subscribe, S7 read and write, EtherNet/IP read and write, and Sparkplug B as an edge node and as a host. S7 and EtherNet/IP have only met simulators.
The editor gets copy and paste, import and export of a selection, subflows with their own canvas, nested groups, search across every flow and debug filters. Dragging a wire onto a node works in Chromium now.
A node whose config node is missing is reported on import, on load and on deploy, and fails to start. Before, it quietly never worked.
The image is built and pushed with Podman. Same tags, 2.2.0, 2.2 and 2, still linux/amd64 and linux/arm64.
Breaking
Context persists by default, so a value set before a restart is there after it. A flow that counted on an empty context at start now has to clear what it expects to be clear. Set store.context.persist to false for the old behavior.
From 2.1.0 it's a plain image bump, with no migration and no flag, and a flows.json that ran on 2.1.0 runs on 2.2.0. The first start creates state.db in the data directory. Going back to 2.1.0 leaves it where it is, because 2.1.0 has no code that reads it. Every write to state.db is a write to an SD card or an SSD, so context changes are batched and a hard kill can lose up to 30 seconds of them.
It has never run on a real plant floor. The S7 and EtherNet/IP nodes have not met a Siemens CPU or a Logix controller, durable wires have never been on a Pi, and Modbus RTU, Modbus over TLS and OPC UA events, history and browse are not done. The full notes, with every new setting, are on [the GitHub release](https://github.com/HotLoop-io/hotloop-flow/releases/tag/v2.2.0).
A build fix and nothing else. The code is the same as 0.1.1, and the workflows that test and publish Cascade run on Podman the whole way through.
Same code as 0.1.1, and so is everything you run: the chart, the Quadlet units, the settings and the database. No migrations and no new settings.
The workflows that test and publish Cascade no longer reach for a Docker engine to start the test services, log in to the registry or load the image into k3s. The test scripts no longer offer Docker as a switch.
The image was already built with Podman. It is built one platform at a time now, and the release reads the manifest list back and refuses to push unless it holds exactly amd64 and arm64.
The image and the chart are public and pull with no login. The source repository is still private.
Pull it with `podman pull ghcr.io/hotloop-io/hotloop-cascade:0.1.2`, or install the chart with `helm install cascade oci://ghcr.io/hotloop-io/charts/hotloop-cascade --version 0.1.2`. The image is amd64 and arm64, and the tags are 0.1.2, 0.1, 0, latest and stable.
Upgrading from 0.1.1 means nothing to do. Run `helm upgrade` to 0.1.2, or let `:stable` roll the Quadlet units. Your key and your workflows stay where they were.
What isn't proven: the workflows on a real GitHub runner. I ran the workflow's own steps under Podman 4.9.3, the version the runner ships, on a box cut down to two CPUs, 7 GB of RAM and a runner-sized disk, with a local registry that needed a login. The tag run was the first time ghcr.io saw any of it.
The first Cascade release. A workflow engine in one static Go binary where every step is checkpointed, so a kill -9 in the middle of a run picks up where it left off.
The engine: every step checkpointed, durable waits, retries, error ports and error workflows. Expressions, and a Code step that runs JavaScript in its own jail in its own process.
Webhooks with a durable inbox, schedules that fire each slot exactly once no matter how many mains are looking, MQTT, and a trigger for files that land in a directory. An HTTP step for anything with an API, and connectors for HotLoop, Postgres, EmberOps, email, ntfy, Gotify and Discord. No others, on purpose.
A credential vault with key rotation, OAuth2 for every grant a plant needs, sign-in with passwords, TOTP, passkeys or your OpenID Connect provider, enforced MFA, projects and roles, an audit log you can verify, every save kept as a version, and a scoped API with an OpenAPI document.
Metrics, traces and probes, so somebody besides me can tell when it's stuck.
SQLite on one box or Postgres with workers when you need more. The editor is baked into the binary, so there is nothing else to run.
Podman with Quadlet units that follow :stable and roll an update back if it doesn't come up healthy, or k3s with the Helm chart, standalone or split into server and workers.
Pull it with `podman pull ghcr.io/hotloop-io/hotloop-cascade:0.1.1`, or install the chart with `helm install cascade oci://ghcr.io/hotloop-io/charts/hotloop-cascade --version 0.1.1`. The image is amd64 and arm64, and the tags are 0.1.1, 0.1, 0, latest and stable. The registry was private for this release, so it wanted a `podman login ghcr.io` as root first. The image and chart are public from 0.1.2 on and pull with no login.
There was a 0.1.0 tag and it never published anything. Its chart test failed on a hosted runner and I canceled the run, so nothing reached the registry, and a tag stays where it is. 0.1.1 is the same code with the test fixed. The test checked the upgrade by listing pods while the old ones were still stopping, which a quiet laptop never shows.
Migrations only go forward. A rollback puts the old image back and does not un-migrate your database, so take a backup before you turn updates on. Lose the key that seals stored credentials and every stored credential is unreadable, so keep a copy somewhere that isn't the box. Not here yet: SAML, LDAP, SCIM, environments, the HA scheduler with leases, more connectors and AI steps. That is 0.2.0 and on.
Every deploy is a record you can diff, test and put back, and a save restarts only the nodes you changed.
Deploys keep the record: who, from where, when, the note, and the exact bytes of the flow file and its credentials. Any two diff node by node, in the API, on the command line and inside git diff, and any of them rolls back, a deleted node's broker password included.
A save restarts what you changed. MQTT sessions, listeners and Delay queues on everything else keep running. Moving or rewiring a node doesn't restart it.
Flows get tests. flows.test.yaml sits next to flows.json and runs on the real runtime and the real nodes, with time you move and stand-ins for everything outside the process. Turn on tests.gate and a deploy that fails them is refused with the whole report. Rollbacks are never gated.
JSONata runs and is proven against the published suite, 1673 of 1686 cases with the other 13 pinned by name. Inject fires on a crontab on the same minute Node-RED would. MQTT 5 and the last will, TLS on TCP, HTTP and MQTT, WebSocket listener and client, cookies and multipart on HTTP.
Signing in holds up: two-factor sign-in that is free for anybody who wants it, lockouts, sessions that survive a restart, and an audit log of who did what from where.
A broker or database password can live in a mounted Secret instead of the flow. New settings: tests.gate (HOTLOOP_FLOW_TESTS_GATE), secrets.allowedPaths, API tokens from hotloop-flow token.
The image carries LICENSE, NOTICE and THIRD_PARTY_LICENSES, and NOTICE now lists every file ported from other projects with its license.
A WebSocket token goes in the hotloop-flow.bearer.<token> subprotocol. A script that still sends it in the URL keeps working and gets a warning in the log, because the URL ends up in proxy and access logs.
Nothing in this release changes how a flow file is read or written, so a `flows.json` that ran on 2.0.5 runs on 2.1.0. Upgrading is an image bump. The first start creates `sessions.json`, `audit.log` and a `deployments` directory under the data directory, and everyone signs in once because sessions used to live in memory.
Lockout is on by default: five failures for one account from one address, or twenty from one address, locks it for fifteen minutes. `auth.lockout` changes the numbers.
It has never run on a real plant floor. The tests, the race detector, a real Mosquitto and the browser tests are all behind it, and none of that is a plant. The full notes, with every new setting, are on [the GitHub release](https://github.com/HotLoop-io/hotloop-flow/releases/tag/v2.1.0).
Recipes, scripts, blueprints, helpers, the logbook and a real automation language. UniFi, read-only. Device passwords encrypted at rest.
Recipes: grade A's setpoints go out together or not at all, checked whole first, written in order through the gate, and read back from the device. The laminated sheet of numbers can retire.
Scripts: write the CIP cycle once and run it from a screen, a rule, MCP or its own entity, instead of pasting it into four rules and fixing a step in three of them.
Blueprints: a rule written once with typed blanks, filled in per pump. Each rule keeps the version it was made from, so editing the blueprint on Wednesday doesn't change what pump 7 does on Thursday.
Helpers for the values people own (batch target, shift, purge timer), a logbook with every state change, write, alarm and rule run on one timeline, and condition, wait and stop steps plus forSec, so "the press has run for ten minutes" is a trigger instead of a delay and a prayer.
UniFi Network, read-only: switches, ports, PoE, WAN and clients as tags, and a quiet PLC says which switch port it's on. The first real read called a dead 5G backup up, because it trusted the same stale field the console's summary did. It reads the health checks now. A real cable pull and a phone joining SCADA haven't been done on real gear yet.
Every stored device password is encrypted with a key the chart generates and keeps outside the database, so a dump or a backup holds ciphertext.
MCP goes from 16 tools to 25. None of the new ones lets an agent make a rule or apply a recipe that names its own permission.
Fixed: an Edge Relay ignored every config push, persistence off didn't boot, saving a rule in the editor reset its run budget, the command prompt could swallow a Set, a late update could put the Entities screen back in time, the rule editor could leave "name is required" under a rule with a perfectly good name, and App Store deploys lost their tenant labels.
Breaking
Upgrade with --reset-then-reuse-values or your values file. --reuse-values replays 4.16.0's values and the render dies on the new defaults.
The first start seals every stored device password with <release>-secret-key. From then on that Secret is part of your data: back it up somewhere other than next to your backups, because a post-upgrade backup only restores where that key is mounted.
Going back to 4.16.0 on the same database starts but hands devices the sealed text as their password. Take a pg_dump before you upgrade.
The API leaves out a time that never happened, instead of sending year one. If your code tests for 0001-01-01, test for the field being there.
Coming from 4.16.0 it's a plain helm upgrade, and the notes above are the whole list of what bites. Migrations 0009 to 0015 run on the first start, all additive. Operators get helpers.write, scripts.run, recipes.apply and blueprints.apply the moment you upgrade; writing scripts, recipes and blueprints stays admin.
HOTLOOP_FLOW_INSECURE=true finally turns authentication off, and turning discovery on stops handing the pod raw network privileges.
HOTLOOP_FLOW_INSECURE=true never worked. It switched authentication off, the startup check refused any config with authentication off, and the refusal told you to set the variable you had just set. From 0.1.0 through 2.0.4. It works now, and only from the environment: auth.enabled false in the config file still refuses to start on its own, because a config file is where a copy-paste lands.
Running without a login is loud about it. Every boot logs a warning, and the editor shows a NO LOGIN badge where Sign out would be, so whoever opens it next knows the door is open. The credential secret still needs its own opt-out.
The editor asks the runtime whether it needs a login instead of assuming. Before, even a runtime that had started with authentication off would have landed you on a login screen with no users behind it, and the live event stream refused to connect without a token.
The chart no longer adds NET_RAW and NET_ADMIN when discovery is on. The scan node only makes TCP connections, so those were privilege for nothing. Every render keeps all capabilities dropped, and CI fails if one comes back.
The config package had no tests at all, which is how the first bug lived this long. It has them now, and four of them fail on the old check. Both ways, authentication on and off, were driven end to end in a browser against the built binary before this was tagged.
The first published release since 4.3.1. Everything from 4.4.0 to 4.15.3 ships at once, under the HotLoop name, with the Edge Relay beside it.
Entities. Every tag is an entity with an id that never changes, placed on one ISA-95 equipment tree. A motor, a valve or a PID loop is one entity built from its tags, every alarm is an entity, and every entity command goes through the same write gate as a tag write. MCP, automations and the new Entities and Plant screens all use them.
Ask before you send. A service call takes ?dry_run=true and answers with the refusal the real call would give, word for word, and the command prompt says "Would be refused: ..." before anybody confirms.
Paging that reaches a phone: ntfy and a Discord bot with an Acknowledge button, Gotify, and Discord webhooks, on rate limits that fold a flood into one summary instead of dropping alarms.
Fixes that matter on a running plant. Unshelve could wipe an unacknowledged alarm. An OPC UA subscription could go silent forever while the session said connected. A silent Modbus device took up to 15 minutes to go down, and takes about 11 seconds now. A failed webhook logged its URL, which for Discord and ntfy is the password.
Ignition 8.3.9 reads the OPC UA server, verified end to end, and the certificate survives a redeploy now, so Ignition stops distrusting it every time the pod moves.
The Edge Relay ships as its own image and chart, released in lockstep with the Gateway. TimescaleDB 2.30.1, and the extension updates itself on start.
Images: ghcr.io/hotloop-io/hotloop:4.16.0 and ghcr.io/hotloop-io/hotloop-edge-relay:4.16.0, for amd64 and arm64, pulled with no login. Charts hotloop and hotloop-edge-relay 4.16.0 from https://hotloop.io/hotloop, and a Quadlet unit for the Edge Relay.
Breaking
Coming from 4.3.1 it is not a plain helm upgrade. The image is ghcr.io/hotloop-io/hotloop (was ghcr.io/embernet-ai/industrial-iot), the chart is hotloop from https://hotloop.io/hotloop (was industrial-iot), and every IIOT_ variable is HOTLOOP_. The chart name is in the workload selectors, so keep the release name and namespace, pg_dump first, delete the old Deployment and StatefulSet (the database volume stays), then helm upgrade with your old values file, IIOT_ renamed and any image.repository override dropped, not --reuse-values. Checked by rendering both charts side by side, not on a live cluster, which is what the pg_dump is for.
License: HotLoop's own license, replacing the one 4.3.1 and earlier were published under. Today that is the Business Source License 1.1, described at hotloop.io/license: free for home and lab, and free for business through EmberNet.
There is a login, writes default to off, and a timezone that won't load stops startup instead of running on UTC.
API: empty lists are [], never null. History buckets with no good reading are null. An error the API can't place is a 500 with a log ref, and a device that errors on a write or a browse is a 502. alarm.turn_on is a 400, unshelving an alarm that isn't shelved is a 409, and tag JSON always carries minValue, maxValue and hasRange.
Versions 4.4.0 through 4.15.3 below were merged and never published on their own. All of them are in this release. The step by step upgrade from 4.3.1 is at https://docs.hotloop.io/gateway/upgrading/.
The OPC UA server stops opening holes in its address space every time somebody edits the configuration.
Editing a device, even just its description, pulled it and every tag under it out of the address space and put them back. A SCADA client reading an untouched tag in that moment got BadNodeIdUnknown. An edit now swaps the node in one step, so a client finds the old one or the new one and never nothing.
A tag removed while a client was browsing its device failed the whole browse. A removed tag now answers BadNodeIdUnknown and leaves browse at once, and the node itself is deleted a few seconds later.
One test tripped over this about once in sixty runs, and that was the only hint. A new test hammers the server with two clients while devices and tags come and go, and the old code failed it every run.
The chart gets an edgePublish block for Sparkplug edge publish, so it no longer has to be set through extraEnv. The broker password goes through a Secret and the forward queue gets its own volume.
TimescaleDB 2.30.1, and the Gateway now runs the extension update on start when the database is behind the image, instead of leaving an existing database on its old version for good.
Every release from this one on publishes the Edge Relay's image and chart next to the Gateway's. Before, the release only built the Gateway.
Pointed the OPC UA driver at Ignition 8.3.9, and Ignition found a bug. It's fixed, and the driver is verified against it.
An OPC UA device with no tags sent the server nothing, so the server closed the idle session while the device kept saying connected. Every browse then failed with BadSessionIDInvalid until the process restarted.
No tags is every device in the moment between adding it and clicking Discover tags, so this hit anyone who took longer than a session timeout to get there. A device with no tags now reads the server's state on every poll, and a browse that finds the session gone reconnects.
Verified against Ignition 8.3.9: Basic256Sha256, SignAndEncrypt, signed in with a user name, browsed, imported, and read live by subscription.
A stock Ignition 8.3 gateway needs four changes before the Gateway can talk to it, the worst being that its OPC UA server listens on localhost only. They are written down in the Gateway's protocol docs. Still not verified against KEPServerEX, WinCC, Siemens, or Beckhoff.
The chart tells the dashboard what it is and what it looks like, so the deployed tile gets its name and icon right.
The pod and the Service now carry the embernet.ai/app-icon annotation. Before, neither did, so the dashboard's deployed tile went hunting for an icon in the app catalog by name, using the release name, which is never in the catalog. The tile came up without one.
The icon defaults to the chart's own icon. It is served from raw GitHub, which sends Access-Control-Allow-Origin: *, so the dashboard's crossorigin image actually draws it. Set embernet.appIcon to use your own, and make sure wherever it lives sends that header too, or the tile is blank.
The embernet.ai/app-name label is the chart name now, not the release name. That is what the dashboard writes on everything it deploys itself, what it matches catalog icons on, and what features like the Ignition gateway list key on.
Which instance a pod belongs to is still in app.kubernetes.io/instance and the Service name, and no selector changed, so upgrading in place is fine.
Rendered against the published 2.0.3 chart at the same version, the only differences are those two labels and the two icon annotations. The program itself did not change, and the image is rebuilt from the same code.
Device credentials are hidden from every role, for every protocol, and scrubbed out of every log and error that could repeat them.
Before this, any logged-in user, viewers included, could read a device's MQTT broker password, its HTTP and MTConnect credentials, and anything else saved in it through the devices API. Every credential a driver reads now comes back as [redacted], to a viewer, an operator, and an admin alike.
That covers passwords, header values, a body that mentions a credential, and the user info or token inside an address like mqtt://user:pass@broker or ?api_key=. Any key named like a secret is hidden by default, even one no driver reads yet.
Saving [redacted] back, or leaving the key out, keeps the stored value. A device cannot be pointed at a new host while keeping its stored credentials, because the Gateway would hand them to the new host.
A device's last error, the system log, the write audit table, the process log, the MCP tools, the diagnostics endpoint, and site errors are all scrubbed of credentials by value and by shape.
Breaking
Anyone editing a device now sees [redacted] in the secret fields and has to type a secret to change it.
If untrusted users could reach a Gateway running an unreleased build before this fix, rotate the device credentials it holds. There has been no public release since 4.3.1. Backups, the Edge Relay's device file, and fleet config push still carry credentials as stored, because the drivers need them.
The OPC UA driver can log in to a secured server, which is to say most of the ones in production.
The driver used to send an empty identity token policy, which a compliant server refuses in every mode, including the Gateway's own OPC UA server. It now asks the server for its endpoints, picks the one matching the device's security mode and policy, and uses the policy id that endpoint lists.
Basic256Sha256, Aes128_Sha256_RsaOaep, and Aes256_Sha256_RsaPss, in Sign or SignAndEncrypt. The deprecated Basic128Rsa15 and Basic256 are never picked, even when a server offers nothing else.
The server's certificate has to be approved by an operator before anything is sent to it. An unknown certificate is saved for approval and the connection fails with a message saying exactly how.
A password is refused before it is sent unless the channel is secured and something encrypts it. That includes a user name over an unsecured channel whose token policy claims to encrypt the password, because the key comes from a reply nothing authenticates.
A refused login waits ten minutes before trying again, so a wrong password cannot lock an account or eat the server's sessions.
Tested against the Gateway's own OPC UA server and Microsoft's opc-plc. Aes256_Sha256_RsaPss failed against opc-plc, so the driver only uses it when a device asks for it by name. In this version the API still returned the MQTT, HTTP, and MTConnect passwords in the clear. 4.15.1 fixed that.
The chart puts the EmberNet dashboard's tenant labels on everything it renders.
When an app is deployed from the EmberNet App Store, the dashboard passes tenantLabels (the tenant, the app id, who deployed it, and the deployment id) and expects them on everything the chart creates. The chart did not read them, so none of the five were there. It puts them on every resource and on the pod now.
The value is empty by default. A plain helm install renders exactly as before, and the selectors are untouched.
Found by deploying it to a tenant cluster through Fleet and looking at what arrived. It has now been deployed that way to a real cluster: the pod runs, login works, and all five labels are on every resource.
CI checks both directions: a default render carries no tenant label, and a chosen one reaches all seven places it belongs.
Nothing else changed in the program. The fix for the generated admin password, from 2.0.2, is included.
The chart's generated admin password now works. It never did, from 0.1.0 through 2.0.1.
The chart generates an admin password, stores it, and the install notes print how to read it. The login it gave did not work in any earlier version, because the hash the app checks was made from a different random password than the one the chart stored. The hash is made from the stored password now.
A release installed with the bug is fixed in place by upgrading to 2.0.2. The password and the credential secret do not change, so nothing stored with your flows is lost.
CI now renders the chart and checks with bcrypt that the generated password matches its hash. Nothing caught this before because both values are random, and nothing rendered the chart before a tag.
It was found by installing the chart on a live k3s cluster and logging in with the password the chart had stored.
Only the generated password was affected. A password you set with auth.password, or a hash you set with auth.passwordHash, worked all along. The program itself is unchanged since 2.0.1, and the image is rebuilt from the same code.
A credentials file from Emberwire 0.1.0 now says what it is, instead of failing with a parsing error.
Starting on a credentials file written by 0.1.0 used to fail with a JSON parsing error. It now names the file, says it was written by Emberwire 0.1.0 and that 2.x cannot read it, and tells you to move it aside and enter the credentials again.
A plaintext credentials file that has a node called "format" in it still loads, and there is a test for that.
Nothing else changed. The old file still cannot be read, because it is encrypted under the old format, and that is on purpose. This only fixes what the operator is told when it happens.
The first release under the HotLoop Flow name. It was Emberwire 0.1.0 before this.
Renamed end to end. The image is ghcr.io/hotloop-io/hotloop-flow, the chart and the binary are hotloop-flow, and the variables are HOTLOOP_FLOW_*.
The editor uses the HotLoop palette and self-hosted Inter and JetBrains Mono, in light and dark, and its top bar works on a phone. Text on an orange fill is dark ink, because white on that orange measured 3.13:1.
Still one static binary in a 25 MB image, with the same 51 node types.
The editor is type-checked in CI now, which it never was, and its design tokens are checked against the canonical copy.
Breaking
The EMBERWIRE_* environment variables are not read. Use HOTLOOP_FLOW_*.
The InfluxDB and PostgreSQL config nodes are hotloop-flow-influxdb and hotloop-flow-postgres. A saved flow that uses the old emberwire- types will not find them.
WASM modules must export hotloop_flow_process, hotloop_flow_alloc, and hotloop_flow_free. A module built against the old exports will not run.
A credentials file written by 0.1.0 cannot be read. It fails to load with a parsing error, so re-enter the credentials.
Metrics are hotloop_flow_* and not emberwire_*, and the deployment header is HotLoop-Flow-Deployment-Rev.
This is a deliberate clean break, since 0.1.0 had no known users, and nothing keeps answering to the old names. The 0.1.0 image is still where it was, at ghcr.io/embernet-ai/emberwire:0.1.0.
Scheduled reports: historian numbers, crunched on a schedule and delivered as CSV or HTML, by email if you want.
A report names its tags, a window (the previous 24 hours, the previous calendar day, or the previous 7 days), a bucket (the whole window, per hour, or per day), and the aggregations: min, max, avg, first, last, and count. It runs on an interval, daily at a time, or weekly on a weekday at a time.
Schedules are wall-clock times in the plant timezone and are computed as calendar dates, never by adding 24 hours, so a 03:00 report still runs at 03:00 the day after the clocks change.
Only good-quality readings are aggregated, and every row says how many it left out. A tag with no readings in a period says no data. It never says zero.
A run missed while the Gateway was down runs once on start-up, not once per missed slot. A report with recipients and no mail relay records a failed run instead of a quiet success.
Viewers read reports, operators run them, and only admins define them, because a definition decides who receives plant data.
API only, and CSV and HTML only. There is no screen and no PDF in this version. Report definitions and run records are in the database backup, and the output files are not.
The Gateway can be an OPC UA server, read-only, so a third-party client can browse and read its tags.
A read-only OPC UA server exposing the Gateway’s tags as an address space. Nodes are read-only, and there is no write handler at all, so this version has no setting that enables writes.
Secure channels use Basic256Sha256 in Sign and SignAndEncrypt. SecurityPolicy None is an explicit opt-in.
Users log in with their HotLoop user name and password, the same user database and bcrypt hashes as the web login, and must hold tags.read and values.read. Permissions are re-checked on live sessions, so revoking one takes effect without waiting for a reconnect.
Anonymous sessions are an explicit opt-in, and a client’s application certificate has to be approved by an operator before it is trusted.
Writes are deliberately absent. The runtime has one write gate, and an OPC UA write would have to go through it. That deserves its own review, so it did not ride along in this release.
Database backups, taken in pure Go over the existing connection, with a restore that refuses to touch a database that already has data.
A logical snapshot of every configuration and history table, taken inside one REPEATABLE READ transaction so it is consistent. It is table data only, since migrations rebuild the schema from nothing on every start.
No pg_dump. The runtime image is distroless with no shell and no external binaries, and backups do not get an exception to that.
Retention defaults to 7 backups, and 0 keeps everything. An optional interval scheduler runs backups on a plain interval, not cron.
Restore is deliberately not an API endpoint. It is a flag an operator runs by hand against an empty, freshly migrated database, and it refuses a target that already has data.
Not verified, and stated in the backup docs: very large history volumes, where long-transaction and connection-pool behavior have not been measured, and restoring across Gateway versions that changed the schema.
An optional embedded MQTT broker, so a single-box install can be its own Sparkplug hub.
An in-process broker with a plain TCP listener on 1883, plus a TLS listener on 8883 whenever a certificate and key are configured.
Credential authentication, checked in constant time. Anonymous access is an explicit, off-by-default opt-in.
Fleet management, edge publishing, and an edge relay can all point at localhost with nothing else to stand up.
There is no per-topic access control, by design. The broker is the authorization boundary, and for a production deployment at scale you should still run a dedicated broker such as Mosquitto or EMQX.
Alarm notifications by webhook and email, so a 03:00 alarm can reach a phone.
Two provider-agnostic channels. A webhook posts JSON to any URL, which composes with PagerDuty, Opsgenie, ntfy, Pushover, Slack, Discord, or a script of your own. Email goes out over SMTP.
Notifications fire when an alarm is raised, and only then. Cleared, acknowledged, and shelved alarms are deliberately not page-worthy.
A priority floor per channel, so a channel set to high and above stays quiet for a low alarm.
SMTP upgrades to STARTTLS automatically when the server offers it, which authenticated relays generally require. It is tested against a local server configured for STARTTLS and authentication, and not against a hosted provider.
Off by default. A test endpoint, restricted to administrators, lets you verify a channel without waiting for a real alarm.
The rename to HotLoop: module path, container images, charts, and environment variables.
The Go module is now github.com/HotLoop-io/hotloop, and the images are ghcr.io/hotloop-io/hotloop and ghcr.io/hotloop-io/hotloop-edge-relay.
Both Helm charts are renamed to hotloop and hotloop-edge-relay.
Every IIOT_ environment variable is now HOTLOOP_.
Breaking
Every IIOT_ environment variable is now HOTLOOP_. An upgrade in place means renaming every IIOT_ value to its HOTLOOP_ equivalent before the pod comes back up.
The image and chart names changed, so an upgrade in place also means new image references.
The App Store labels and annotations under the embernet.ai namespace are unchanged on purpose. A namespaced key names the system that reads it, and those labels are how the EmberNet App Store finds a chart.
Config push: stage a new device file on a fleet relay without touching it by hand.
The Gateway publishes a device file to a retained MQTT topic per relay, validated before it is published with the same validation the relay uses, so a malformed file never reaches a relay.
A push and a restart are two separate operator actions on purpose. Pushing to a running relay only stages the file for the next restart, and a relay that is offline picks it up automatically when it reconnects.
Fleet management: see registered edge relays, and command them.
Membership is declared, not auto-trusted. A birth message from an unregistered relay is logged and ignored.
Presence comes from Sparkplug birth and death messages, which are outbound from the relay and work through NAT with no inbound connection to a relay ever required.
Rebirth is open to operators. Restart is administrator-only, since it takes a relay down.
The application cannot enforce this part, so the docs say it plainly: the broker’s own access controls should restrict publishing of node commands to the Gateway’s credentials.
A second binary, the Edge Relay, with no database at all, and two ways to deploy it.
The Edge Relay is a separate, lighter binary that polls equipment and forwards it, with no PostgreSQL dependency. About 16.9 MB against the Gateway’s 33.4 MB.
It exposes only /healthz and /status, read-only and unauthenticated, since there is no identity system to guard them and nothing sensitive in the response.
Deployable on k3s with its own Helm chart, or on a box with no cluster as a Podman Quadlet unit that runs as a real systemd service.