Skip to content
Merged

2.8.0 #117

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/image-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ env:
# in it adds arm64 here. A run that builds fewer platforms than the tags it
# moves never takes an image away from a box on another architecture:
# build/publish-tags.sh keeps what X.Y and X already hold for the rest.
PLATFORMS: "amd64 arm64"
PLATFORMS: "amd64"

jobs:
build:
Expand Down
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ What that means on a bad night:
It runs your existing flows unchanged, too. That's [further down](#bring-your-flows),
because it's a feature, not the point.

Current release is `2.7.1`. The image is `ghcr.io/hotloop-io/hotloop-flow:2.7.1`
for amd64 and arm64, the Helm repo is `https://hotloop.io/hotloop-flow/`, and
Current release is `2.8.0`. The image is `ghcr.io/hotloop-io/hotloop-flow:2.8.0`
for amd64 (arm64 follows in 2.8.1), the Helm repo is `https://hotloop.io/hotloop-flow/`, and
[docs/deploying.md](docs/deploying.md) has Kubernetes, Podman and Quadlet. About
104,000 lines of Go, a little under 40 percent of it tests, 79 node types for messages and 21 for FlowLab diagrams, and the race
104,000 lines of Go, a little under 40 percent of it tests, 79 node types for messages and 28 for FlowLab diagrams, and the race
detector comes back clean on every package. Home and lab use is free, so is use by schools, nonprofits and government, and
business use is free through EmberNET, under HotLoop's Business Source License.

Expand Down Expand Up @@ -108,25 +108,25 @@ re-measured below. Three runs each, and the range is what's printed.

| | |
|---|---|
| Binary, linux/amd64, static | **41.4 MB** |
| Image, `2.7.0` amd64, as `podman image inspect` reports it | **45.34 MB** |
| Binary, linux/amd64, static | **41.7 MB** |
| Image, `2.8.0` amd64, as `podman image inspect` reports it | **45.64 MB** |
| Start to answering its first request | **0.05 s** |
| RSS, idle | **16.4 to 16.7 MiB** |
| RSS, peak under the HTTP load below | **26.4 to 28.5 MiB** |
| HTTP, five-node flow, 8 connections | **13.0k to 15.3k req/s** |
| HTTP latency p50 / p99 | **0.39 to 0.41 ms / 1.78 to 4.03 ms** |
| Engine, five-node chain, no I/O | **2.20 to 3.11 µs a message**, 3.9 KB allocated |

The binary row is the 2.7.0 tree built the way the table's heading says,
`CGO_ENABLED=0 go build -trimpath -ldflags="-s -w"`, on 2026-11-01: 41,402,530
bytes, on The Beast. The 2.5.0 number, 41,144,480, was taken on another machine,
so I don't read the gap between them as growth. On The Beast, FlowLab added about
370 KB to the binary: 41,025,698 before it and 41,398,434 after. The image row is the
`2.7.0` amd64 image built from the release tree the way the release workflow
builds it, `podman build --platform linux/amd64 --build-arg VERSION=v2.7.0 .`, read
The binary row is the 2.8.0 tree built the way the table's heading says,
`CGO_ENABLED=0 go build -trimpath -ldflags="-s -w"`, on 2026-11-01: 41,693,346
bytes, on The Beast. The 2.7.0 number, 41,402,530, was taken on the same machine the
same way, so the 290 KB between them is what the statistics, integral, derivative and
filter nodes, the DAQ client, the MQTT output and the TDMS log added. The image row is the
`2.8.0` amd64 image built from the release tree the way the release workflow
builds it, `podman build --platform linux/amd64 --build-arg VERSION=v2.8.0 .`, read
on 2026-11-01 with `podman image inspect <that image> --format '{{.Size}}'`
(45,342,406 bytes). It was not read from ghcr.io, because the tag wasn't pushed
yet. It was 44.95 MB at `2.5.0`, 44.8 MB at `2.3.0`, 39.1 MB at `2.2.0`, 32.1 MB at
(45,635,783 bytes). It was not read from ghcr.io, because the tag wasn't pushed
yet. It was 45.34 MB at `2.7.0`, 44.95 MB at `2.5.0`, 44.8 MB at `2.3.0`, 39.1 MB at `2.2.0`, 32.1 MB at
`2.1.0` and 25.4 MB at `2.0.5`. The other rows are still the 2026-10-02 build.

The start, memory and HTTP rows are one command against
Expand Down Expand Up @@ -1273,7 +1273,7 @@ constructor-chain escape to prove it reaches nothing.

## The palette

**79 node types for messages, and 21 more for FlowLab diagrams.**
**79 node types for messages, and 28 more for FlowLab diagrams.**

| Category | Nodes |
|---|---|
Expand All @@ -1285,7 +1285,7 @@ constructor-chain escape to prove it reaches nothing.
| Storage | file, file in, watch, influxdb out, postgres |
| Discover | scan, netinfo, for inventorying an OT segment |
| Config | mqtt-broker, websocket-listener, websocket-client, influxdb, postgres, modbus, s7, ethernetip, opcua, sparkplug, hotloop, mcp-server |
| FlowLab | constant, math, compare, logic, wait, bundle, field, build array, while loop, for loop, body in, body out, serial port, serial read, serial write, parse reading, stable value, log to text file, control, indicator, chart |
| FlowLab | constant, math, compare, logic, wait, bundle, field, build array, while loop, for loop, body in, body out, serial port, serial read, serial write, parse reading, stable value, log to text file, log to TDMS file, statistics, integral, derivative, filter, read DAQ task, mqtt out, control, indicator, chart |

The FlowLab row is a different kind of node. It runs on a tab that is a diagram, where
a node runs once when every input has a value, and never takes a message. What
Expand Down Expand Up @@ -1985,10 +1985,10 @@ podman run --rm -p 1880:1880 -v hotloop-flow-data:/data \
-e HOTLOOP_FLOW_ADMIN_USER=admin \
-e HOTLOOP_FLOW_ADMIN_PASSWORD_HASH='<bcrypt hash>' \
-e HOTLOOP_FLOW_CREDENTIAL_SECRET="$(openssl rand -hex 32)" \
ghcr.io/hotloop-io/hotloop-flow:2.7.1
ghcr.io/hotloop-io/hotloop-flow:2.8.0
```

Generate the hash with `podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.7.1
Generate the hash with `podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.8.0
hash-password -password 'something-long'`. The image is distroless nonroot with
no shell in it, so there is nothing to `exec` into and nothing for anybody who
gets code execution to pivot with.
Expand Down
12 changes: 6 additions & 6 deletions docs/deploying.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Deploying HotLoop Flow

Three ways in, all from the same image, `ghcr.io/hotloop-io/hotloop-flow`, built
for amd64 and arm64:
for amd64, with arm64 following in 2.8.1:

| You have | Use |
|---|---|
Expand Down Expand Up @@ -65,7 +65,7 @@ To choose your own instead:

```bash
helm install line3-flows hotloop-flow/hotloop-flow \
--set auth.passwordHash="$(podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.7.1 hash-password -password 'something-long')" \
--set auth.passwordHash="$(podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.8.0 hash-password -password 'something-long')" \
--set hotloopFlow.credentialSecret="$(openssl rand -hex 32)"
```

Expand Down Expand Up @@ -186,7 +186,7 @@ so the unit can be copied around, committed and reviewed without carrying
anything that matters.

```bash
printf '%s' "$(podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.7.1 \
printf '%s' "$(podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.8.0 \
hash-password -password 'something-long')" | podman secret create hotloop-flow-admin-hash -
printf '%s' "$(openssl rand -hex 32)" | podman secret create hotloop-flow-credential-secret -
```
Expand All @@ -205,7 +205,7 @@ After=network-online.target

[Container]
ContainerName=hotloop-flow
Image=ghcr.io/hotloop-io/hotloop-flow:2.7.1
Image=ghcr.io/hotloop-io/hotloop-flow:2.8.0
PublishPort=1880:1880
Volume=hotloop-flow-data:/data
Environment=HOTLOOP_FLOW_ADMIN_USER=admin
Expand Down Expand Up @@ -266,9 +266,9 @@ To try it, not to run a line on:
```bash
podman run --rm -p 1880:1880 -v hotloop-flow-data:/data \
-e HOTLOOP_FLOW_ADMIN_USER=admin \
-e HOTLOOP_FLOW_ADMIN_PASSWORD_HASH="$(podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.7.1 hash-password -password 'something-long')" \
-e HOTLOOP_FLOW_ADMIN_PASSWORD_HASH="$(podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.8.0 hash-password -password 'something-long')" \
-e HOTLOOP_FLOW_CREDENTIAL_SECRET="$(openssl rand -hex 32)" \
ghcr.io/hotloop-io/hotloop-flow:2.7.1
ghcr.io/hotloop-io/hotloop-flow:2.8.0
```

Then open <http://localhost:1880>. That credential secret is new every time you
Expand Down
2 changes: 1 addition & 1 deletion docs/diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ value arrives with none.

### A target that goes down

A durable `dia-mqtt-out` sends straight to the broker while it answers. When it doesn't, the message is committed to Flow's durable store before the node finishes, and a pump delivers the queue, oldest first, when the link is back. Anything put in while others wait goes behind them, so nothing overtakes. Delivery from the queue is at least once: the one message in flight when the connection was cut can arrive twice. The queue lives in the same store and under the same disk quota as a durable wire, and it drains after a restart with no run and no browser. Without durable, a message the broker misses is an error on the error wire.
A durable `dia-mqtt-out` sends straight to the broker while it answers. When it doesn't, the message is committed to Flow's durable store before the node finishes, and a pump delivers the queue, oldest first, when the link is back. Anything put in while others wait goes behind them, so nothing overtakes. Delivery from the queue is at least once: a message whose send timed out because the client had not noticed the link was gone can still be delivered when the client reconnects, so it can arrive twice, once for each send that timed out. The queue lives in the same store and under the same disk quota as a durable wire, and it drains after a restart with no run and no browser. Without durable, a message the broker misses is an error on the error wire.

### Serial

Expand Down
38 changes: 38 additions & 0 deletions docs/releases/2.8.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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
Binary file modified docs/screenshots/debug-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/debug.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/editor-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/review-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/screenshots/review.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading