diff --git a/public/synced/flow/docs/screenshots/debug-dark.png b/public/synced/flow/docs/screenshots/debug-dark.png index 2018c86..a817c0b 100644 Binary files a/public/synced/flow/docs/screenshots/debug-dark.png and b/public/synced/flow/docs/screenshots/debug-dark.png differ diff --git a/public/synced/flow/docs/screenshots/debug.png b/public/synced/flow/docs/screenshots/debug.png index d49cb67..0da5cee 100644 Binary files a/public/synced/flow/docs/screenshots/debug.png and b/public/synced/flow/docs/screenshots/debug.png differ diff --git a/public/synced/flow/docs/screenshots/editor-dark.png b/public/synced/flow/docs/screenshots/editor-dark.png index 6b5863f..83bc54c 100644 Binary files a/public/synced/flow/docs/screenshots/editor-dark.png and b/public/synced/flow/docs/screenshots/editor-dark.png differ diff --git a/public/synced/flow/docs/screenshots/editor.png b/public/synced/flow/docs/screenshots/editor.png index 630c0f1..9a06fa2 100644 Binary files a/public/synced/flow/docs/screenshots/editor.png and b/public/synced/flow/docs/screenshots/editor.png differ diff --git a/public/synced/flow/docs/screenshots/review-dark.png b/public/synced/flow/docs/screenshots/review-dark.png index d2d3212..84ec42e 100644 Binary files a/public/synced/flow/docs/screenshots/review-dark.png and b/public/synced/flow/docs/screenshots/review-dark.png differ diff --git a/public/synced/flow/docs/screenshots/review.png b/public/synced/flow/docs/screenshots/review.png index 3cc9a8b..6834392 100644 Binary files a/public/synced/flow/docs/screenshots/review.png and b/public/synced/flow/docs/screenshots/review.png differ diff --git a/src/content/docs/cascade/running.md b/src/content/docs/cascade/running.md index f274de6..e2bba6c 100644 --- a/src/content/docs/cascade/running.md +++ b/src/content/docs/cascade/running.md @@ -5,10 +5,6 @@ description: "Run Cascade with Podman Quadlet or Helm on k3s, on SQLite or Postg editUrl: false --- -:::note[Written against main] -This page follows `main` at dd8af77, and it differs from the copy in HotLoop Cascade 1.0.1, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - On a Podman host, `deploy/quadlet` has systemd units. One file and a volume is Cascade on SQLite; `deploy/quadlet/postgres` is the server, a worker and PostgreSQL on a network of their own. Both start at boot, restart on failure and diff --git a/src/content/docs/cascade/status.md b/src/content/docs/cascade/status.md index a09caa5..56d4a08 100644 --- a/src/content/docs/cascade/status.md +++ b/src/content/docs/cascade/status.md @@ -5,10 +5,6 @@ description: "Cascade is released: what is in it, what is not in it yet, and whe editUrl: false --- -:::note[Written against main] -This page follows `main` at dd8af77, and it differs from the copy in HotLoop Cascade 1.0.1, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - 1.0.1 is the release out today, and 1.0.0 is the one before it. 1.0.0 is the first with the identity and governance work, each piece tried against the real server it talks to: SAML (against Keycloak), LDAP and Active Directory (OpenLDAP and a Samba diff --git a/src/content/docs/flow/deploying.md b/src/content/docs/flow/deploying.md index b3307ae..0c5ce00 100644 --- a/src/content/docs/flow/deploying.md +++ b/src/content/docs/flow/deploying.md @@ -6,7 +6,7 @@ editUrl: false --- Three ways in, all from the same image, `ghcr.io/hotloop-io/hotloop-flow`, built -for amd64, with arm64 following in 2.7.1: +for amd64, with arm64 following in 2.8.1: | You have | Use | |---|---| @@ -70,7 +70,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.0 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)" ``` @@ -191,7 +191,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.0 \ +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 - ``` @@ -210,7 +210,7 @@ After=network-online.target [Container] ContainerName=hotloop-flow -Image=ghcr.io/hotloop-io/hotloop-flow:2.7.0 +Image=ghcr.io/hotloop-io/hotloop-flow:2.8.0 PublishPort=1880:1880 Volume=hotloop-flow-data:/data Environment=HOTLOOP_FLOW_ADMIN_USER=admin @@ -271,9 +271,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.0 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.0 + ghcr.io/hotloop-io/hotloop-flow:2.8.0 ``` Then open . That credential secret is new every time you diff --git a/src/content/docs/flow/flowlab.md b/src/content/docs/flow/flowlab.md index 90c4920..c4ba71f 100644 --- a/src/content/docs/flow/flowlab.md +++ b/src/content/docs/flow/flowlab.md @@ -80,7 +80,70 @@ Events for a run go out on the same socket as the editor's, as `diagram//nod | `dia-serial-open`, `dia-serial-read`, `dia-serial-write` | A serial port. Reads end at a terminator or a byte count. | | `dia-parse-reading`, `dia-stable` | Take a number and unit out of a line, and say whether the last few agree. | | `dia-log-text` | Appends a record as a row of a text file, with a time column. | +| `dia-daq-read` | Reads samples from a data acquisition task on NI hardware, through NI's gRPC Device Server. See Data acquisition below. | +| `dia-mqtt-out` | Publishes its value through an MQTT server the flow file configures, the same connection the MQTT nodes use. Mark it durable and a message the broker misses is written to disk and sent, in order, when the broker is back, even across a restart. | +| `dia-log-tdms` | Appends a record to a TDMS file, one channel per field. See below. | | `dia-control`, `dia-indicator`, `dia-chart` | The panel. | +| `dia-stats` | The mean, RMS, smallest, largest and standard deviation of an array. The standard deviation divides by n minus one, so it needs two readings. | +| `dia-integral` | The area under an array by the trapezoid rule, as one number or as the running area at each sample. | +| `dia-derivative` | The slope at each sample of an array. | +| `dia-filter` | A lowpass, highpass, bandpass or bandstop Butterworth filter of order 1 to 10 with a cutoff in hertz. | + +### Filters + +The filter node is a Butterworth filter: flat where it passes, no ripple, and 3 dB +down at the cutoff. It is designed from the analog filter by the bilinear transform +with the cutoff prewarped, so the 3 dB point lands on the frequency you typed, and it +runs as a chain of second-order sections so a high order stays accurate. A bandpass or +bandstop of order n has 2n poles. A cutoff has to be above zero and below half the +sample rate, and a design that cannot be built is listed in the problems before the run. +The sample rate is a property, or an input that wins over it. The filter keeps its +memory from call to call inside a run, so an array cut into pieces by a loop comes out +as if it had been filtered whole, and every new run starts from rest. + +What it can't do: it is not a linear-phase filter, so it delays different frequencies +by different amounts, and it has no zero-phase mode that filters forward and back. Where +the timing between two signals has to line up, or a waveform's shape has to survive, +that is the wrong filter. Integral and derivative take the time between samples from a +`dt` input in seconds, a `rate` input in hertz, or the Samples per second property. + +### Log to TDMS + +`dia-log-tdms` writes the measurements a test bench takes to a TDMS file, the format +that data acquisition and analysis tools open. Wire a record into `row`. Each field +of the record is a channel in one group, so a record of `pressure`, `ok` and +`operator` makes three channels. A number is stored as a 64 bit float, true or false +as a bool, and anything else as text. A field that holds a list of numbers adds the +whole list, which is how a block of samples goes in. Settings: + +| Details | Details | +|---|---| +| `path` | The file. A file that is already there is appended to, so one run after another builds one file. | +| `group` | The group the channels go in. | +| `columns` | The fields to log, in order. Empty logs every field, by name. | +| `properties` | A JSON object of properties for the file, such as `{"title": "bench 4", "station": 4}`, written when the run starts. | +| `sync` | On by default: each row reaches the disk before the node finishes. | + +Each row is one segment, written in one piece. If the power goes in the middle of +one, the file shows where it was cut, a reader returns every row before it, and the +next run repairs the end of the file and goes on. A field's type is set by the first +row of a run. A later row that doesn't fit, such as text in a number column, goes +to the error wire as a bad input and the file is left alone. A missing number is +written as not-a-number. + +To open the file, use any TDMS reader. The library behind the node, `internal/tdms`, +also writes an optional `.tdms_index` beside the file, and reads files without one. +Channel properties such as `unit_string` and the waveform properties `wf_start_time`, +`wf_increment` and `wf_samples` are supported by the library; the node does not set +them yet. + +What the reader accepts: little and big endian segments, interleaved and contiguous +data, meta data left out of a segment when it hasn't changed, a new object list, 8, 16, +32 and 64 bit integers, 32 and 64 bit floats, bools, text and timestamps. What it +refuses, by name and without half reading the file: raw data written by a data +acquisition driver, scaled or compressed data, extended precision floats, fixed +point and complex numbers. What the writer writes: little endian, contiguous +data, and those same types. It does not write big endian. ### The error wire @@ -92,12 +155,73 @@ loop's `err` body out and the loop stops on it, with the error on the loop's err out. An error indicator on the panel goes red on a fault and clears when the next 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: 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 A port that goes away, such as a pulled cable or an unplugged adapter, is a fault on the read's error wire. The next read tries to open it again, once a second, and counts the loss and the return. Flow control lines are not offered. +### Data acquisition + +`dia-daq-read` reads a task from NI hardware, such as a CompactDAQ chassis, through +NI's own open gRPC Device Server. Flow talks to that server over the network, so the +hardware can be on a different machine from Flow, and Flow needs no NI driver and no +vendor library of its own. It stays one static binary. + +**On the machine the hardware is attached to**, install NI's DAQmx driver and NI's +gRPC Device Server (https://github.com/ni/grpc-device, MIT licensed). The server +listens on that machine only (`localhost`, port 31763) until its configuration file +says otherwise. To reach it from Flow on another machine, set `address` in its +`server_config.json` to `[::]` or the interface you want, and turn on TLS by setting +`server_cert`, `server_key`, and, for mutual TLS, `root_cert`, with the files in the +`certs` folder beside the configuration. NI's README has the details for the version +you run. Do not leave the server's default, unsecured port open on a plant network: +anyone who can reach it can run the hardware. + +**In the node**, set: + +* `address` to `host:port` of the server. +* `tls` to `off` (only for a server on the same machine), `on` (Flow checks the + server's certificate against `caFile`, or the system's authorities if that is + empty), or `mutual` (Flow also presents `certFile` and `keyFile`, which the server + checks against its `root_cert`). `serverName` is the name to look for in the + server's certificate when it is not the host in `address`. +* `device` and `channels` to name the hardware, such as `cDAQ1Mod1` and `ai0:3`, or a + list like `ai0, ai2`. `kind` is `voltage`, `current`, or `thermocouple`, with `min` + and `max` for the range you expect and `terminal` for the wiring. Thermocouples take + a `tcType` and a `unit` and use the module's own cold junction compensation. +* `mode`, `rate`, and `samples`. Finite mode takes one acquisition of `samples` per + channel at `rate` on every call, starting it again each time. Continuous mode starts + the task on the first call and each call reads the next `samples`, so a node in a + loop body reads a stream block by block. `trigger` starts the task on a digital edge + at a terminal such as `/cDAQ1/PFI0`. + +`y` is the samples as an array for one channel, or an array of arrays, one per channel +in the order you named them. `dt` is the seconds between samples, one over the rate, so +a block's length in seconds is its sample count times `dt`: 1,000 samples at 1,000 +samples a second cover 1.000 s. The device's own clock times the samples, as it always +does. Flow only moves the data. A refusal from the server or the driver comes out on +`err` with the driver's own code and text, and a server that does not answer in +`callTimeoutSec` is a timeout. The task lives for the run and is cleared on the server +when the run ends, so the hardware is free again. + +What is done: analog voltage in, analog current in, and thermocouple temperature in, +finite or continuous, with a sample clock and an optional digital start trigger. What +is not: digital in and out and counters use different read calls and are not built, and +neither are writes, other trigger types, or scaling beyond what the module does. + +**This has not been run against NI's server.** There is no NI hardware, driver, or +server in our lab. Every test runs against a stand-in server we wrote, which speaks +the same calls and answers with a sine wave. What backs the wire format is a test that +reads NI's own protocol files, kept in `third_party/ni-grpc-device/`, and checks that +every message the client sends and reads has the same field numbers, names, and types. +That shows we say the right words. It does not show NI's server behaves the way the +stand-in does. + ## The panel `/panel/` on a runtime opens a page of the diagram's controls, diff --git a/src/content/docs/flow/install.md b/src/content/docs/flow/install.md index aa4a31d..3f26a20 100644 --- a/src/content/docs/flow/install.md +++ b/src/content/docs/flow/install.md @@ -1,13 +1,13 @@ --- # generated by scripts/sync-docs.mjs title: "Install HotLoop Flow with Podman or Helm" -description: "Run HotLoop Flow 2.7.0 with Podman or Quadlet, on Kubernetes with Helm, or from source. It starts locked, so set an admin password first." +description: "Run HotLoop Flow 2.8.0 with Podman or Quadlet, on Kubernetes with Helm, or from source. It starts locked, so set an admin password first." sidebar: label: "Install" editUrl: false --- -HotLoop Flow 2.7.0 is current. It ships as `ghcr.io/hotloop-io/hotloop-flow:2.7.0`, a 45.34 MB image on amd64, with the arm64 build following in 2.7.1, plus a Helm chart and source you can build yourself. The image is distroless, runs as a non-root user, and has no shell in it, so there's nothing to drop into, for you or for anybody who shouldn't be there. There's no Docker or Compose path on this page: Podman for one box, Quadlet to make it a service, Helm for a cluster. +HotLoop Flow 2.8.0 is current. It ships as `ghcr.io/hotloop-io/hotloop-flow:2.8.0`, a 45.64 MB image on amd64, with the arm64 build following in 2.8.1, plus a Helm chart and source you can build yourself. The image is distroless, runs as a non-root user, and has no shell in it, so there's nothing to drop into, for you or for anybody who shouldn't be there. There's no Docker or Compose path on this page: Podman for one box, Quadlet to make it a service, Helm for a cluster. Flow starts locked. It refuses to run without an admin account, because a flow can run commands. And it refuses to run without a credential secret, which encrypts every broker and database password stored with your flows. Every path below sets both. @@ -18,7 +18,7 @@ From 2.2.0 context and durable wires live in a SQLite file, `state.db`, on the d Make a password hash first. The image has no shell, so hashing is a subcommand of the binary itself, and it refuses anything under eight characters. ```bash -podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.7.0 hash-password -password 'something-long' +podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.8.0 hash-password -password 'something-long' ``` Then make the credential secret, and save it somewhere you'll still find it in a year: @@ -34,7 +34,7 @@ podman run -d --name hotloop-flow -p 1880:1880 -v hotloop-flow-data:/data \ -e HOTLOOP_FLOW_ADMIN_USER=admin \ -e HOTLOOP_FLOW_ADMIN_PASSWORD_HASH='' \ -e HOTLOOP_FLOW_CREDENTIAL_SECRET='' \ - ghcr.io/hotloop-io/hotloop-flow:2.7.0 + ghcr.io/hotloop-io/hotloop-flow:2.8.0 ``` Open `http://localhost:1880/` and sign in as `admin`. `/health` answers with the version once the process is up, and `/ready` says whether the runtime actually started. @@ -64,7 +64,7 @@ After=network-online.target Wants=network-online.target [Container] -Image=ghcr.io/hotloop-io/hotloop-flow:2.7.0 +Image=ghcr.io/hotloop-io/hotloop-flow:2.8.0 ContainerName=hotloop-flow PublishPort=1880:1880 Volume=hotloop-flow-data:/data diff --git a/src/content/docs/flow/overview.md b/src/content/docs/flow/overview.md index cfe6a97..22db7fa 100644 --- a/src/content/docs/flow/overview.md +++ b/src/content/docs/flow/overview.md @@ -40,10 +40,10 @@ What that means on a bad night: It runs your existing flows unchanged, too. That's [further down](/flow/migrating-from-node-red/), because it's a feature, not the point. -Current release is `2.7.0`. The image is `ghcr.io/hotloop-io/hotloop-flow:2.7.0` -for amd64 (arm64 follows in 2.7.1), 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](/flow/deploying/) 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. @@ -57,8 +57,8 @@ re-measured below. Three runs each, and the range is what's printed. | Details | Details | |---|---| -| 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** | @@ -66,16 +66,16 @@ re-measured below. Three runs each, and the range is what's printed. | 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 --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 diff --git a/src/content/docs/flow/palette.md b/src/content/docs/flow/palette.md index 0347b64..6a68fe3 100644 --- a/src/content/docs/flow/palette.md +++ b/src/content/docs/flow/palette.md @@ -5,7 +5,7 @@ description: "Every node type Flow ships, labeled full, partial, divergent or Fl editUrl: false --- -**79 node types for messages, and 21 more for FlowLab diagrams.** +**79 node types for messages, and 28 more for FlowLab diagrams.** | Category | Nodes | |---|---| @@ -17,7 +17,7 @@ editUrl: false | 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 diff --git a/src/content/docs/flow/quick-start.md b/src/content/docs/flow/quick-start.md index 4c4ef47..e2a6cb2 100644 --- a/src/content/docs/flow/quick-start.md +++ b/src/content/docs/flow/quick-start.md @@ -30,10 +30,10 @@ podman run --rm -p 1880:1880 -v hotloop-flow-data:/data \ -e HOTLOOP_FLOW_ADMIN_USER=admin \ -e HOTLOOP_FLOW_ADMIN_PASSWORD_HASH='' \ -e HOTLOOP_FLOW_CREDENTIAL_SECRET="$(openssl rand -hex 32)" \ - ghcr.io/hotloop-io/hotloop-flow:2.7.0 + ghcr.io/hotloop-io/hotloop-flow:2.8.0 ``` -Generate the hash with `podman run --rm ghcr.io/hotloop-io/hotloop-flow:2.7.0 +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. diff --git a/src/content/docs/install/index.md b/src/content/docs/install/index.md index e37943c..df82d99 100644 --- a/src/content/docs/install/index.md +++ b/src/content/docs/install/index.md @@ -25,8 +25,8 @@ There are two ways to run a HotLoop product for real: Helm on k3s or any Kuberne | [HotLoop Edge](/edge/overview/) | 4.18.11 | Yes | Yes | [Install Edge](/edge/install/) | | [HotLoop Gateway](/gateway/overview/) | 4.18.11 | Yes | Yes | [Install the Gateway](/gateway/install/) | | [HotLoop Edge Relay](/edge-relay/overview/) | 4.18.11 | Yes | Yes | [Install the Edge Relay](/edge-relay/install/) | -| [HotLoop Flow](/flow/overview/) | 2.7.0 | Yes | Yes | [Install Flow](/flow/install/) | -| [HotLoop Cascade](/cascade/overview/) | 1.0.1 | Yes, no login needed | Yes, no login needed, see [Cascade with Podman Quadlet](/cascade/quadlet/) | [Running Cascade](/cascade/running/) | +| [HotLoop Flow](/flow/overview/) | 2.8.0 | Yes | Yes | [Install Flow](/flow/install/) | +| [HotLoop Cascade](/cascade/overview/) | 1.0.2 | Yes, no login needed | Yes, no login needed, see [Cascade with Podman Quadlet](/cascade/quadlet/) | [Running Cascade](/cascade/running/) | HotLoop IoT, Edge, Gateway, and the Edge Relay come out of one tag, in lockstep, so they share a version number. Flow and Cascade have their own. The IoT, Edge, and Gateway Quadlet units need Podman 5.0 or newer, and the relay's unit was run on Podman 4.9.3. Raspberry Pi OS Bookworm ships 4.3, which has no Quadlet at all, so a Pi wants Trixie or newer. diff --git a/src/content/docs/releases/cascade-1-0-2.md b/src/content/docs/releases/cascade-1-0-2.md new file mode 100644 index 0000000..b0eedf8 --- /dev/null +++ b/src/content/docs/releases/cascade-1-0-2.md @@ -0,0 +1,31 @@ +--- +# generated by scripts/sync-docs.mjs +title: "HotLoop Cascade 1.0.2 release notes" +description: "What shipped in HotLoop Cascade 1.0.2, what changed on upgrade, and what to read before you touch a running plant." +editUrl: false +sidebar: + label: "1.0.2" +--- + +## What this is + +1.0.2 lets a workflow sign and verify things without a secret ever sitting in item data, and lets a public form on another website call a webhook from the browser. Nothing else changed from 1.0.1. + +## What is new in the Code step + +- **Crypto helpers.** `sha256(text)`, `hmacSHA256(key, text)`, `randomHex(bytes)`, `base64Encode(text)`, `base64Decode(text)` and `safeEqual(a, b)`. They are computed in Go, so the answers are the standard library's, and the script still has no `require`, no network and no files. +- **A credential for the step.** The Code step has a `secret` credential slot. Give it an `HMAC key` credential (a new built in credential type with one secret field, `key`) and the script reads it as `credential.key`. The key lives in the vault like every other secret, and the vault scrubs it from everything the run saves, even if the script returns it. A step with no credential sees an empty `credential` object. + +The reason: a public form 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 is new on a webhook + +- **`corsOrigins`.** A webhook trigger takes a list of web origins (`https://example.com`, or `*`) whose pages may call it from a browser. Cascade answers the browser's preflight itself, without starting a run, and adds `Access-Control-Allow-Origin` to the answer for a listed origin. A webhook that lists none behaves exactly as before: no CORS headers, so a browser page cannot read the answer. An entry has to be an origin, with no path. + +## What was tested + +The full local suite (`test/run.sh`) did not run for this release. Only the targeted tests ran: the Code step's helpers and credential (the step, the runner and the vault), the webhook trigger's CORS settings and preflight, and the catalog and workflow packages, all passing on The Beast. The lab's MQTT broker would not start on that box. + +## Upgrading, and going back + +No migrations. `helm upgrade` to 1.0.2, or let `:stable` roll the Quadlet units. Workflows that never touch the new helpers behave exactly as before, so putting 1.0.1 back is safe for them; a workflow that calls a new helper fails on 1.0.1 with the helper undefined. A backup before any update is still the right habit. diff --git a/src/content/docs/releases/flow-2-7-1.md b/src/content/docs/releases/flow-2-7-1.md new file mode 100644 index 0000000..b82e8fa --- /dev/null +++ b/src/content/docs/releases/flow-2-7-1.md @@ -0,0 +1,29 @@ +--- +# generated by scripts/sync-docs.mjs +title: "HotLoop Flow 2.7.1 release notes" +description: "What shipped in HotLoop Flow 2.7.1, what changed on upgrade, and what to read before you touch a running plant." +editUrl: false +sidebar: + label: "2.7.1" +--- + +HotLoop Flow 2.7.1 is 2.7.0 with the arm64 image added, and nothing else. 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. 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.7.1`, `2.7` and `2` tags hold both. +- The README and the install pages name 2.7.1 and say amd64 and arm64, as they did before 2.7.0. + +## New settings + +None. + +## Upgrade + +A plain `helm upgrade` to the 2.7.1 image and chart, or the new tag in your Quadlet unit. If you are on arm64 and stayed on 2.6.0 while 2.7.0 was amd64 only, this is your move to FlowLab: read the 2.7.0 notes for what it is and what it isn't yet. Nothing happens on first start and nothing needs migrating. + +## What isn't proven + +The arm64 image is built the way it was for 2.6.0, 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 README at this tag has the rest, with pictures: https://github.com/HotLoop-io/hotloop-flow/blob/v2.7.1/README.md diff --git a/src/content/docs/releases/flow-2-8-0.md b/src/content/docs/releases/flow-2-8-0.md new file mode 100644 index 0000000..4b78633 --- /dev/null +++ b/src/content/docs/releases/flow-2-8-0.md @@ -0,0 +1,47 @@ +--- +# generated by scripts/sync-docs.mjs +title: "HotLoop Flow 2.8.0 release notes" +description: "What shipped in HotLoop Flow 2.8.0, what changed on upgrade, and what to read before you touch a running plant." +editUrl: false +sidebar: + label: "2.8.0" +--- + +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 diff --git a/src/content/docs/releases/index.md b/src/content/docs/releases/index.md index 96dc61e..c494504 100644 --- a/src/content/docs/releases/index.md +++ b/src/content/docs/releases/index.md @@ -5,7 +5,7 @@ description: "What shipped in every HotLoop core, HotLoop Flow and HotLoop Casca editUrl: false --- -Every release has its own page, written the day it shipped. A note marked "on main, next release" describes a release that is not out yet, so nothing in it is in what you have installed. The latest HotLoop core release is 4.18.11, the latest Flow release is 2.7.0, and the latest Cascade release is 1.0.1. +Every release has its own page, written the day it shipped. A note marked "on main, next release" describes a release that is not out yet, so nothing in it is in what you have installed. The latest HotLoop core release is 4.18.11, the latest Flow release is 2.8.0, and the latest Cascade release is 1.0.2. ## HotLoop core @@ -19,6 +19,8 @@ IoT, Edge, Gateway and Edge Relay release together from one tag, so they share t ## HotLoop Flow +- [HotLoop Flow 2.8.0 release notes](/releases/flow-2-8-0/) +- [HotLoop Flow 2.7.1 release notes](/releases/flow-2-7-1/) - [HotLoop Flow 2.7.0 release notes](/releases/flow-2-7-0/) - [HotLoop Flow 2.6.0 release notes](/releases/flow-2-6-0/) - [HotLoop Flow 2.5.0 release notes](/releases/flow-2-5-0/) @@ -29,6 +31,7 @@ IoT, Edge, Gateway and Edge Relay release together from one tag, so they share t ## HotLoop Cascade +- [HotLoop Cascade 1.0.2 release notes](/releases/cascade-1-0-2/) - [HotLoop Cascade 1.0.1 release notes](/releases/cascade-1-0-1/) - [HotLoop Cascade 1.0.0 release notes](/releases/cascade-1-0-0/) - [HotLoop Cascade 0.1.2 release notes](/releases/cascade-0-1-2/) diff --git a/src/data/sync.json b/src/data/sync.json index 7b0661f..910c0ae 100644 --- a/src/data/sync.json +++ b/src/data/sync.json @@ -2,7 +2,7 @@ "repos": { "hotloop": { "slug": "HotLoop-io/hotloop", - "ref": "4d5da2f216e276bba9587f9aa5e3343bad13258f", + "ref": "origin/main", "sha": "4d5da2f216e276bba9587f9aa5e3343bad13258f", "release": { "tag": "v4.18.11", @@ -14,26 +14,26 @@ }, "flow": { "slug": "HotLoop-io/hotloop-flow", - "ref": "b1d8cc56826e6466c465d6b2473482b00d804f8e", - "sha": "b1d8cc56826e6466c465d6b2473482b00d804f8e", + "ref": "origin/main", + "sha": "37f730bded1a7e2c0d4aba2bb79b3d59c9835a32", "release": { - "tag": "v2.7.0", - "version": "2.7.0", + "tag": "v2.8.0", + "version": "2.8.0", "prerelease": false, - "publishedAt": "2026-11-01T18:52:15Z", - "sha": "b1d8cc56826e6466c465d6b2473482b00d804f8e" + "publishedAt": "2026-11-02T04:38:59Z", + "sha": "37f730bded1a7e2c0d4aba2bb79b3d59c9835a32" } }, "cascade": { "slug": "HotLoop-io/hotloop-cascade", "ref": "origin/main", - "sha": "dd8af77a63260c4ead704ef801b59865f288a7c6", + "sha": "437c21c21c4d9f1efa97cce5a38e17385009333f", "release": { - "tag": "v1.0.1", - "version": "1.0.1", + "tag": "v1.0.2", + "version": "1.0.2", "prerelease": false, - "publishedAt": "2026-11-01T20:06:03Z", - "sha": "0b6fbd5c0b593ccaf325667c2e0a0c1cb0869dac" + "publishedAt": "2026-11-02T01:09:18Z", + "sha": "437c21c21c4d9f1efa97cce5a38e17385009333f" } } }, @@ -775,7 +775,7 @@ "group": "Start here", "title": "Where Cascade stands", "label": null, - "status": "changed", + "status": "released", "repo": "cascade", "source": "README.md", "release": false @@ -797,7 +797,7 @@ "group": "Use it", "title": "Running Cascade", "label": null, - "status": "changed", + "status": "released", "repo": "cascade", "source": "README.md", "release": false @@ -1022,6 +1022,28 @@ "source": "docs/releases/2.7.0.md", "release": true }, + { + "slug": "releases/flow-2-7-1", + "product": "releases", + "group": "HotLoop Flow", + "title": "HotLoop Flow 2.7.1 release notes", + "label": "2.7.1", + "status": "released", + "repo": "flow", + "source": "docs/releases/2.7.1.md", + "release": true + }, + { + "slug": "releases/flow-2-8-0", + "product": "releases", + "group": "HotLoop Flow", + "title": "HotLoop Flow 2.8.0 release notes", + "label": "2.8.0", + "status": "released", + "repo": "flow", + "source": "docs/releases/2.8.0.md", + "release": true + }, { "slug": "releases/cascade-0-1-1", "product": "releases", @@ -1065,6 +1087,17 @@ "repo": "cascade", "source": "docs/release-notes/1.0.1.md", "release": true + }, + { + "slug": "releases/cascade-1-0-2", + "product": "releases", + "group": "HotLoop Cascade", + "title": "HotLoop Cascade 1.0.2 release notes", + "label": "1.0.2", + "status": "released", + "repo": "cascade", + "source": "docs/release-notes/1.0.2.md", + "release": true } ], "pinned": [ diff --git a/src/pinned/flow/install.md b/src/pinned/flow/install.md index 5ee1e49..b5eca0b 100644 --- a/src/pinned/flow/install.md +++ b/src/pinned/flow/install.md @@ -1,12 +1,12 @@ --- -reviewed: flow=2.7.0 +reviewed: flow=2.8.0 title: "Install HotLoop Flow with Podman or Helm" description: "Run HotLoop Flow {{flow}} with Podman or Quadlet, on Kubernetes with Helm, or from source. It starts locked, so set an admin password first." sidebar: label: "Install" --- -HotLoop Flow {{flow}} is current. It ships as `ghcr.io/hotloop-io/hotloop-flow:{{flow}}`, a 45.34 MB image on amd64, with the arm64 build following in 2.7.1, plus a Helm chart and source you can build yourself. The image is distroless, runs as a non-root user, and has no shell in it, so there's nothing to drop into, for you or for anybody who shouldn't be there. There's no Docker or Compose path on this page: Podman for one box, Quadlet to make it a service, Helm for a cluster. +HotLoop Flow {{flow}} is current. It ships as `ghcr.io/hotloop-io/hotloop-flow:{{flow}}`, a 45.64 MB image on amd64, with the arm64 build following in 2.8.1, plus a Helm chart and source you can build yourself. The image is distroless, runs as a non-root user, and has no shell in it, so there's nothing to drop into, for you or for anybody who shouldn't be there. There's no Docker or Compose path on this page: Podman for one box, Quadlet to make it a service, Helm for a cluster. Flow starts locked. It refuses to run without an admin account, because a flow can run commands. And it refuses to run without a credential secret, which encrypts every broker and database password stored with your flows. Every path below sets both.