diff --git a/scripts/sync-manifest.mjs b/scripts/sync-manifest.mjs index 6f48833..3bd046f 100644 --- a/scripts/sync-manifest.mjs +++ b/scripts/sync-manifest.mjs @@ -153,7 +153,8 @@ export const PAGES = [ // Release notes are every file in docs/releases, found on the ref, not listed here. export const RELEASE_NOTES = [ - { repo: 'hotloop', dir: 'docs/releases', slugPrefix: 'releases/hotloop-', label: 'HotLoop core' }, + // 4.19.6 and 4.19.7 were tagged and never published (4.19.8's notes say why and carry both), so they get no page. A note for a release that does not exist is a claim that it does. + { repo: 'hotloop', dir: 'docs/releases', slugPrefix: 'releases/hotloop-', label: 'HotLoop core', skip: ['4.19.6', '4.19.7'] }, { repo: 'flow', dir: 'docs/releases', slugPrefix: 'releases/flow-', label: 'HotLoop Flow' }, // 0.1.0 was tagged and never published anything, so it gets no page. A note for a release that does not exist is a claim that it does. { repo: 'cascade', dir: 'docs/release-notes', slugPrefix: 'releases/cascade-', label: 'HotLoop Cascade', skip: ['0.1.0'] }, diff --git a/src/content/docs/edge-relay/install.md b/src/content/docs/edge-relay/install.md index a6c2a70..0b9b7ca 100644 --- a/src/content/docs/edge-relay/install.md +++ b/src/content/docs/edge-relay/install.md @@ -1,7 +1,7 @@ --- # generated by scripts/sync-docs.mjs title: "Install the Edge Relay with Helm or Quadlet" -description: "Run HotLoop Edge Relay 4.18.11 on a cluster with Helm, or on a Pi or industrial PC as a systemd service with Podman Quadlet. The unit files are here in full." +description: "Run HotLoop Edge Relay 4.19.8 on a cluster with Helm, or on a Pi or industrial PC as a systemd service with Podman Quadlet. The unit files are here in full." sidebar: label: "Install" editUrl: false @@ -57,7 +57,7 @@ A device's `config` block can hold credentials, like an OPC UA `username` and `p helm repo add hotloop https://hotloop.io/hotloop helm repo update helm install press-line-3 hotloop/hotloop-edge-relay \ - --version 4.18.11 --namespace hotloop --create-namespace \ + --version 4.19.8 --namespace hotloop --create-namespace \ --set edgePublish.broker=tcp://gateway.plant.local:1883 \ --set edgePublish.group=Plant1 \ --set edgePublish.node=press-line-3 \ @@ -90,7 +90,7 @@ Those end up in a ConfigMap and in the Helm release record, and anybody who can ```bash kubectl -n hotloop create secret generic press-devices --from-file=devices.json -helm upgrade press-line-3 hotloop/hotloop-edge-relay --version 4.18.11 \ +helm upgrade press-line-3 hotloop/hotloop-edge-relay --version 4.19.8 \ --reset-then-reuse-values --set devicesSecret=press-devices ``` diff --git a/src/content/docs/edge-relay/overview.md b/src/content/docs/edge-relay/overview.md index f7e3e75..12cd68b 100644 --- a/src/content/docs/edge-relay/overview.md +++ b/src/content/docs/edge-relay/overview.md @@ -9,7 +9,7 @@ editUrl: false The Edge Relay is the headless one. It sits on a Pi or a small industrial PC next to the machines, polls them with the same drivers the Gateway uses, and forwards every reading over Sparkplug B to a Gateway somewhere with a real database. When the uplink dies it queues to disk and replays when the link comes back, so the historian upstream ends up with the outage backfilled, not a hole in it. No database, no web UI, no users, no write path, and every one of those missing pieces is something that can't break at 3 AM. -The current release is 4.18.11: `ghcr.io/hotloop-io/hotloop-edge-relay:4.18.11` for amd64, arm64, and arm/v7 (a Pi 3 or a 32-bit Pi OS install is exactly the box a relay ends up on), about 15 MB, pulled with no login, and the `hotloop-edge-relay` chart from `https://hotloop.io/hotloop`. 4.16.0 was the first release that published it, and arm/v7 came with 4.18.10. It shares a version number with the Gateway, and a release refuses to go out unless both charts carry the tag's version and the Quadlet unit follows `:stable`. So the relay gets a new version every release whether its code changed or not. One version number to ask about, and a relay chart can never point at an image nobody pushed. [Install it](/edge-relay/install/). +The current release is 4.19.8: `ghcr.io/hotloop-io/hotloop-edge-relay:4.19.8` built for amd64 alone this time (a Pi 3 or a 32-bit Pi OS install is exactly the box a relay ends up on, and its arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, until the next release builds them all again), about 15 MB, pulled with no login, and the `hotloop-edge-relay` chart from `https://hotloop.io/hotloop`. 4.16.0 was the first release that published it, and arm/v7 came with 4.18.10. It shares a version number with the Gateway, and a release refuses to go out unless both charts carry the tag's version and the Quadlet unit follows `:stable`. So the relay gets a new version every release whether its code changed or not. One version number to ask about, and a relay chart can never point at an image nobody pushed. [Install it](/edge-relay/install/). :::note[New in 4.18.10] **A pushed device file applies the moment it lands.** New devices connect, removed ones let go, changed ones reconnect, and the ones nobody touched keep their sessions. The Sparkplug session stays up, with no NDEATH and the same `bdSeq`. A bad file is refused and the old one keeps running, and `/status` says which file is running and which was refused. On 4.17.0 a push only staged the file until something restarted the relay. diff --git a/src/content/docs/edge/install.md b/src/content/docs/edge/install.md index e8166c9..01da7a9 100644 --- a/src/content/docs/edge/install.md +++ b/src/content/docs/edge/install.md @@ -1,13 +1,13 @@ --- # generated by scripts/sync-docs.mjs -title: "Install HotLoop Edge 4.18.11" -description: "Install HotLoop Edge 4.18.11 on k3s with Helm or on an industrial PC with Podman Quadlet, then turn on the OPC UA server and Sparkplug publishing." +title: "Install HotLoop Edge 4.19.8" +description: "Install HotLoop Edge 4.19.8 on k3s with Helm or on an industrial PC with Podman Quadlet, then turn on the OPC UA server and Sparkplug publishing." sidebar: label: "Install" editUrl: false --- -HotLoop Edge first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-edge:4.18.11`, built for amd64, arm64, and arm/v7, and the chart is `hotloop-edge` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. +HotLoop Edge first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-edge:4.19.8`, built for amd64 alone this release (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), and the chart is `hotloop-edge` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. A cluster gets Helm. An industrial PC or a Pi with no cluster gets Podman Quadlet. The database is a choice you make once: TimescaleDB (the default), plain PostgreSQL or a SQLite file with no database container at all. @@ -16,7 +16,7 @@ A cluster gets Helm. An industrial PC or a Pi with no cluster gets Podman Quadle ```bash helm repo add hotloop https://hotloop.io/hotloop helm repo update -helm install hotloop hotloop/hotloop-edge --version 4.18.11 \ +helm install hotloop hotloop/hotloop-edge --version 4.19.8 \ --namespace hotloop --create-namespace ``` diff --git a/src/content/docs/edge/overview.md b/src/content/docs/edge/overview.md index 3bab07b..f1e6185 100644 --- a/src/content/docs/edge/overview.md +++ b/src/content/docs/edge/overview.md @@ -8,7 +8,7 @@ editUrl: false --- :::note[Edge is out, first released in 4.18.10] -The image is `ghcr.io/hotloop-io/hotloop-edge:4.18.11` for amd64, arm64, and arm/v7, about 37 MB on amd64 and 34 MB on arm, and the chart is `hotloop-edge` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `edge/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/edge/install/). Before 4.18.10 the Gateway could run at the edge and publish Sparkplug upstream, and it has since 4.5.0, but it was the whole Gateway to do it. +The image is `ghcr.io/hotloop-io/hotloop-edge:4.19.8` built for amd64 alone this time (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), about 37 MB on amd64 and 34 MB on arm, and the chart is `hotloop-edge` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `edge/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/edge/install/). Before 4.18.10 the Gateway could run at the edge and publish Sparkplug upstream, and it has since 4.5.0, but it was the whole Gateway to do it. ::: Edge is a box bolted next to a line. It reads the machine, runs the rules on the machine, keeps working when the WAN goes down, and tells the rest of the plant what it saw. Think Ignition Edge, built in Go, with the same write gate as everything else. diff --git a/src/content/docs/gateway/install.md b/src/content/docs/gateway/install.md index e88b7db..eacf661 100644 --- a/src/content/docs/gateway/install.md +++ b/src/content/docs/gateway/install.md @@ -1,13 +1,13 @@ --- # generated by scripts/sync-docs.mjs -title: "Install HotLoop Gateway 4.18.11 with Helm" -description: "Install HotLoop Gateway 4.18.11 from the published Helm chart on k3s or any Kubernetes, sign in, and set what matters before a real plant sees it." +title: "Install HotLoop Gateway 4.19.8 with Helm" +description: "Install HotLoop Gateway 4.19.8 from the published Helm chart on k3s or any Kubernetes, sign in, and set what matters before a real plant sees it." sidebar: label: "Install" editUrl: false --- -HotLoop Gateway 4.18.11 ships as a container image for amd64 and arm64, about 37 MB on amd64 and 35 MB on arm64, and a Helm chart. Both pull with no login. It keeps the names it shipped under through 4.17.0, image `ghcr.io/hotloop-io/hotloop` and chart `hotloop`, so nobody running it has to change a thing. Already running an older release? Don't install over it. [Upgrading](/gateway/upgrading/) has the path from 4.17.0, which is a plain `helm upgrade`, and the paths from 4.16.0 and 4.3.1, which go in hops. +HotLoop Gateway 4.19.8 ships as a container image for amd64 (the arm64 image on `:stable` is 4.19.5's, the last build of every architecture, and the next release builds both again), about 37 MB on amd64 and 35 MB on arm64, and a Helm chart. Both pull with no login. It keeps the names it shipped under through 4.17.0, image `ghcr.io/hotloop-io/hotloop` and chart `hotloop`, so nobody running it has to change a thing. Already running an older release? Don't install over it. [Upgrading](/gateway/upgrading/) has the path from 4.17.0, which is a plain `helm upgrade`, and the paths from 4.16.0 and 4.3.1, which go in hops. Two ways to run it for real: the Helm chart on any Kubernetes, k3s included, and Podman Quadlet on a box that isn't in a cluster, below. There's no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. @@ -16,7 +16,7 @@ Two ways to run it for real: the Helm chart on any Kubernetes, k3s included, and ```bash helm repo add hotloop https://hotloop.io/hotloop helm repo update -helm install hotloop hotloop/hotloop --version 4.18.11 \ +helm install hotloop hotloop/hotloop --version 4.19.8 \ --namespace hotloop --create-namespace ``` @@ -58,7 +58,7 @@ Put these in a values file and install with `-f`. Don't lean on `--set` for anyt | `global.timezone` | `America/New_York` | Schedules, reports and shift changes run on it. It has to be a real zone name (`America/Chicago`, not `CST`), because a timezone that won't load stops startup instead of quietly running on UTC. | | `publicUrl` | empty | The address a phone on the plant Wi-Fi reaches HotLoop at. Without it, notifications have no link back and ntfy gets no Acknowledge button. | | `safety.allowWrites` | `false` | The master switch. Off, nothing writes to a machine, from anywhere. Turn it on once the deployment has been reviewed. | -| `safety.allowMcpWrites` | `false` | Lets agents write through the same gate. It narrows the master switch and never goes around it. Set it without `safety.allowWrites` and the Gateway refuses to start. | +| `safety.allowMcpWrites` | `true` | Lets agents write through the same gate, on since 4.19.4. It follows the master switch: with `safety.allowWrites` off, nothing writes, agents included. Set it `false` and agents can still read the whole plant and never write. The charts always set an MCP token, so `/mcp` is never open. | | `database.postgresql.persistence.size` | `20Gi` | Your history lives here. Size it for your tag count and `historian.retentionDays` (90). | | `logbook.retentionDays` | `90` | Days of entity state changes the [logbook](/iot/logbook/) keeps. `0` keeps everything until the volume fills, and the Gateway warns you about it at start. | | `secretKey.existingSecret` | empty | A Secret you manage holding the device password key. Leave it empty and the chart makes one. Set it if you render the chart with `helm template`. | diff --git a/src/content/docs/gateway/overview.md b/src/content/docs/gateway/overview.md index f71f243..7b96a5c 100644 --- a/src/content/docs/gateway/overview.md +++ b/src/content/docs/gateway/overview.md @@ -7,8 +7,8 @@ sidebar: editUrl: false --- -:::note[4.18.11 is out] -The release where HotLoop stops being one image. [IoT](/iot/overview/), [Edge](/edge/overview/), the Gateway and the [Edge Relay](/edge-relay/overview/) all ship from this tag, each with its own image and chart. The Gateway keeps `ghcr.io/hotloop-io/hotloop:4.18.11` and the chart `hotloop` from `https://hotloop.io/hotloop`, and both pull with no login. The source repo is still private. [Install it](/gateway/install/). On 4.17.0? It's a plain `helm upgrade`, but not with `--reuse-values`, and a couple of defaults moved. On 4.16.0 or 4.3.1? You go through 4.17.0 first. Either way, [read the upgrade first](/gateway/upgrading/). You want to find this out here, not from a stuck rollout. +:::note[4.19.8 is out] +Four products from one tag: [IoT](/iot/overview/), [Edge](/edge/overview/), the Gateway and the [Edge Relay](/edge-relay/overview/), each with its own image and chart. This build is amd64 only, and the arm images on `:stable` are 4.19.5's until the next release builds every architecture. The Gateway keeps `ghcr.io/hotloop-io/hotloop:4.19.8` and the chart `hotloop` from `https://hotloop.io/hotloop`, and both pull with no login. The source repo is still private. [Install it](/gateway/install/). On 4.18.11? It's a `helm upgrade`, and agents can write after it, so read the two things that change. On 4.17.0? It's a plain `helm upgrade`, but not with `--reuse-values`, and a couple of defaults moved. On 4.16.0 or 4.3.1? You go through 4.17.0 first. Either way, [read the upgrade first](/gateway/upgrading/). You want to find this out here, not from a stuck rollout. ::: HotLoop Gateway reads your plant. It polls PLCs and servers in their own protocols, keeps every reading and its quality in a historian, raises ISA-18.2 alarms that can page a phone, runs automations against all of it, and hands the whole plant to agents over MCP. A site runs one chart, the Gateway and its database, instead of a historian, an alarm server, a rules engine, and a pile of glue, each one breaking on its own damn schedule. @@ -19,7 +19,7 @@ It's written in Go, from scratch: one static binary on a distroless base, no sid ## Where to start -The ground floor, in 4.18.11 and in every release back to 4.16.0: +The ground floor, in 4.19.8 and in every release back to 4.16.0: - [Install](/gateway/install/) is Helm from the published chart or Podman Quadlet on a box, the key to back up, and the handful of values to set before a real plant sees it. - [Upgrading](/gateway/upgrading/) is the procedure that keeps your data, from 4.17.0, 4.16.0, or 4.3.1, and every API change a script will trip over. @@ -28,6 +28,8 @@ The ground floor, in 4.18.11 and in every release back to 4.16.0: - [Automations](/iot/automations/) covers triggers, conditions, and actions, and why a rule is compiled when you save it instead of when it fires at 3 AM. - [Model Context Protocol](/iot/mcp/) is the Gateway as an MCP server and client, and how one Gateway reads every site. +New in 4.19.4, and not in 4.18.11: agents write through the same gate as the screen (MCP is on in every product, and the endpoint always has a token), a value written to a scaled tag lands in the register the way you meant it, a CODESYS runtime is read by the names in its Symbol Configuration, a Kafka event stream joins the historian targets, OPC UA PubSub over UADP subscribes and publishes, and `hostNetwork` is a chart setting. 4.19.5 is its multi-arch build, and 4.19.8 adds a Logbook fix and tag pickers that search the whole plant. [The release notes](/releases/hotloop-4-19-4/) say what each one buys you, and what it costs. + New in 4.18.11, and not in 4.18.10: the database is PostgreSQL 17 with an upgrade that runs itself, SNMP and BACnet/IP through one integrations flow, kiosk sign-in with a lockout, alarm escalation, and a UI that installs on a phone. [The release notes](/releases/hotloop-4-18-11/) say what each one buys you, and what it costs. New in 4.18.10, and not in 4.17.0: the four-product lineup, a choice of database, MQTT, and network discovery, historian targets, and boxes that update themselves. [The release notes](/releases/hotloop-4-18-10/) say what each one buys you. @@ -62,12 +64,15 @@ New in 4.17.0, and not in 4.16.0: HotLoop is one codebase shipped as four products, all released together from one tag: IoT, Edge, the Gateway, and the Edge Relay. The Gateway is the top of it: everything in IoT and Edge, plus fleet management, multi-site roll-ups, and scheduled reports. Features split the products. Protocols never will: every product speaks every protocol we have, so nobody has to buy up a tier to read the PLC on their bench. [The lineup](/products/) has what each one carries. -All four are installable today, all 4.18.11. The relay is its own image and chart, about 15 MB against the Gateway's 37, with no database and no UI. The Gateway's fleet management tracks and commands every registered relay, and it's API only (`/api/fleet`): there's no Fleet screen in the UI. +All four are installable today, all 4.19.8. The relay is its own image and chart, about 15 MB against the Gateway's 37, with no database and no UI. The Gateway's fleet management tracks and commands every registered relay, and it's API only (`/api/fleet`): there's no Fleet screen in the UI. ## What changed, release by release Each release has its own notes, and the notes are where the detail lives, so it is in one place and not two. +- [4.19.8](/releases/hotloop-4-19-8/): the Logbook's Older entries button keeps its page, and every tag picker searches the whole plant. It carries 4.19.6's and 4.19.7's changes too, because neither was published. +- [4.19.5](/releases/hotloop-4-19-5/): the multi-arch build of 4.19.4, with nothing else in it. +- [4.19.4](/releases/hotloop-4-19-4/): agent writes and a token on every MCP endpoint, scaled writes that land right, CODESYS by symbol name, a Kafka event stream, OPC UA PubSub over UADP, and `hostNetwork`. There is no 4.19.0 to 4.19.3, and the notes say why. - [4.18.11](/releases/hotloop-4-18-11/): PostgreSQL 17 with an upgrade that runs itself (read this one before you upgrade a running plant), SNMP and BACnet/IP through one integrations flow, kiosk sign-in with a lockout, alarm escalation, and a UI that installs on a phone. - [4.18.10](/releases/hotloop-4-18-10/): the four-product lineup, a choice of database, MQTT and network discovery, historian targets, and boxes that update themselves. There is no 4.18.0 to 4.18.9, and the notes say why. - [4.17.0](/releases/hotloop-4-17-0/): recipes, scripts, blueprints, helpers, the logbook, and UniFi Network, read-only. diff --git a/src/content/docs/gateway/reports.md b/src/content/docs/gateway/reports.md index 01edb18..9c523e4 100644 --- a/src/content/docs/gateway/reports.md +++ b/src/content/docs/gateway/reports.md @@ -5,10 +5,6 @@ description: "Historian numbers on a schedule as CSV or HTML, stored and emailed editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - Added in 4.14.0. `internal/reports` runs operator-defined reports on a schedule: it queries the historian, renders the result as CSV and HTML, stores it on a volume, and optionally emails it. It is the counterpart of Ignition's Reporting diff --git a/src/content/docs/gateway/upgrading.md b/src/content/docs/gateway/upgrading.md index 8617b44..ae572fc 100644 --- a/src/content/docs/gateway/upgrading.md +++ b/src/content/docs/gateway/upgrading.md @@ -1,18 +1,36 @@ --- # generated by scripts/sync-docs.mjs -title: "Upgrading HotLoop Gateway to 4.18.11" -description: "From 4.18.10 the database moves to PostgreSQL 17 by itself, which needs disk and a backup. From 4.17.0 it is a plain helm upgrade. Older, you go in hops." +title: "Upgrading HotLoop Gateway to 4.19.8" +description: "From 4.18.11 it is a helm upgrade, but agents can write now and scaled tags change. From 4.18.10 the database moves to PostgreSQL 17. Older, you go in hops." sidebar: label: "Upgrading" editUrl: false --- -Four ways onto 4.18.11, and they are nothing alike. From 4.18.10 the database engine changes under you, and you want to know that before it happens. From 4.17.0 it's a plain `helm upgrade`, with a couple of defaults that moved. From 4.16.0 you go through 4.17.0 first, and afterwards a new Secret is as much a part of your plant's data as the database is. From 4.3.1 it's a renamed chart that a plain `helm upgrade` can't touch, and you go through 4.16.0 and 4.17.0 on the way. The release notes cover one hop at a time, and none of them covers jumping over a release, so this page doesn't promise that one works. Find the one that's yours and read all of it before you touch a running plant. +Five ways onto 4.19.8, and they are nothing alike. From 4.18.11 it is a plain `helm upgrade`, but two things change what a running plant does, so read them first. From 4.18.10 the database engine changes under you, and you want to know that before it happens. From 4.17.0 it's a plain `helm upgrade`, with a couple of defaults that moved. From 4.16.0 you go through 4.17.0 first, and afterwards a new Secret is as much a part of your plant's data as the database is. From 4.3.1 it's a renamed chart that a plain `helm upgrade` can't touch, and you go through 4.16.0 and 4.17.0 on the way. The release notes cover one hop at a time, and none of them covers jumping over a release, so this page doesn't promise that one works. Find the one that's yours and read all of it before you touch a running plant. :::note[Older charts are fetched by URL] -The chart repository's index lists only the latest chart, 4.18.11, so `hotloop/hotloop --version 4.17.0` won't resolve. The older chart tarballs are still served, and the commands below for 4.16.0 and 4.17.0 name the chart by its URL instead, like `https://hotloop.io/hotloop/hotloop-4.17.0.tgz`. +The chart repository's index keeps the Gateway's charts back to 4.16.0, so `hotloop/hotloop --version 4.17.0` resolves, and the tarballs are served by URL too. The commands below for 4.16.0 and 4.17.0 name the chart by its URL, like `https://hotloop.io/hotloop/hotloop-4.17.0.tgz`, and that works either way. IoT and Edge start at 4.18.10. ::: +## Upgrading from 4.18.11 + +Why bother: agents over MCP can write now, a value you write to a scaled tag lands in the register the way you meant it, a CODESYS runtime is read by the names its programmer gave the variables, HotLoop can send readings and the system log to a Kafka broker, OPC UA PubSub over UADP works in both directions, and a store install signs you in as yourself. [The 4.19.4 release notes](/releases/hotloop-4-19-4/) have the whole list, [4.19.5](/releases/hotloop-4-19-5/) is the multi-arch build of it, and [4.19.8](/releases/hotloop-4-19-8/) is the one to install, with a Logbook fix and tag pickers that search the whole plant. 4.19.0 to 4.19.3, 4.19.6 and 4.19.7 were tagged and never published, so none of them exists to upgrade from. + +```bash +helm repo update +helm upgrade hotloop hotloop/hotloop --version 4.19.8 \ + --namespace hotloop --reset-then-reuse-values +``` + +The same for `hotloop-edge`, `hotloop-iot` and `hotloop-edge-relay`. The database is already PostgreSQL 17 from 4.18.11, and nothing here changes it, so `helm rollback` to 4.18.11 is fine. + +- **Agents can write.** `allowMcpWrites` is on by default in the code and in all three charts. A deployment that has `allowWrites` on and never set `allowMcpWrites` gets agent writes at this upgrade, through the same gate as the screen. A deployment that saved `allowMcpWrites: false` stays off. If you want agents read-only, set it `false` before you upgrade. +- **The MCP endpoint always has a token.** A bare binary or a Quadlet unit that never set one makes its own at first start and keeps it in its database. `hotloop mcp-token` prints it, and an agent pointed at `/mcp` with no token stops working until you give it that one. +- **Scaled tags.** Write 55.5 to a tag scaled by 0.1 and 555 lands in the register, where 55 used to. If anyone typed raw counts to work around it, that now sends ten times too much. Look at every tag with a scale or an offset that somebody writes by hand, from an automation or from a recipe, and change those numbers to engineering values in the same step as the upgrade. +- **Times are `Z`.** Every time the API writes is UTC. A script that parsed your local offset now gets `Z`. +- **One migration,** `0026`, adds an `options` column to the historian targets, empty for every target that exists. + ## Upgrading from 4.18.10 Why bother: the database is PostgreSQL 17 now, and the box moves itself. You also get [integrations](/iot/integrations/) with SNMP and BACnet/IP, kiosk sign-in, alarm escalation, and a UI that installs on a phone. [The 4.18.11 release notes](/releases/hotloop-4-18-11/) have the list. What follows is the part that bites. @@ -21,7 +39,7 @@ PostgreSQL will not open another major version's data directory, so a bare image ```bash helm repo update hotloop -helm upgrade hotloop/hotloop --version 4.18.11 --reset-then-reuse-values +helm upgrade hotloop/hotloop --version 4.19.8 --reset-then-reuse-values ``` The same for `hotloop-edge` and `hotloop-iot`. Take a backup on 4.18.10 first and copy it off the box. HotLoop's own backup is made by the running app through the running database, so it can't be taken in the step that runs before the database starts. @@ -48,7 +66,7 @@ The same for `hotloop-edge` and `hotloop-iot`. Take a backup on 4.18.10 first an ### Quadlet, and the Edge Relay -Copy the new `hotloop-db.container`, `hotloop-db-upgrade.container`, and `hotloop-db-upgrade.sh` from your product's directory in `deploy/quadlet/` into the units directory, run `systemctl daemon-reload`, and restart `hotloop-db.service`. The database unit requires the upgrade unit, so a box that only got the new database unit does not start on 16 data, and says why. The Edge Relay has no database and nothing to do. It ships at 4.18.11 because every product ships in lockstep. +Copy the new `hotloop-db.container`, `hotloop-db-upgrade.container`, and `hotloop-db-upgrade.sh` from your product's directory in `deploy/quadlet/` into the units directory, run `systemctl daemon-reload`, and restart `hotloop-db.service`. The database unit requires the upgrade unit, so a box that only got the new database unit does not start on 16 data, and says why. The Edge Relay has no database and nothing to do. It ships at 4.19.8 because every product ships in lockstep. ### API changes from 4.18.10 @@ -61,11 +79,11 @@ Copy the new `hotloop-db.container`, `hotloop-db-upgrade.container`, and `hotloo Why bother: the Gateway stops being the only image. IoT and Edge are new, the database is a setting, devices that announce themselves over MQTT show up on their own, history can be written out to the plant's own database, and a box that follows `:stable` rolls a bad update back by itself. That last one only works because migrations only add. [The 4.18.10 release notes](/releases/hotloop-4-18-10/) have the list. -It's a plain `helm upgrade`. I rendered the 4.17.0 and 4.18.11 Gateway charts side by side with the same values: the same eight objects, the same selectors, the same volume claims. +It's a plain `helm upgrade`. I rendered the 4.17.0 and 4.19.8 Gateway charts side by side with the same values: the same eight objects, the same selectors, the same volume claims. ```bash helm repo update hotloop -helm upgrade hotloop/hotloop --version 4.18.11 --reset-then-reuse-values +helm upgrade hotloop/hotloop --version 4.19.8 --reset-then-reuse-values ``` `--reset-then-reuse-values`, or your values file with `-f`. Plain `--reuse-values` happens to render this time, but it replays 4.17.0's values and drops every default this release added, which is how you end up on the old queue limit below without meaning to. @@ -93,7 +111,7 @@ The Gateway has Quadlet units now, which 4.17.0 didn't. They're the `gateway/` d ## Upgrading from 4.16.0 -This is the hop to 4.17.0, and from 4.17.0 you go on to 4.18.11 with [the section above](#upgrading-from-4170). Everything below was proven against 4.17.0. +This is the hop to 4.17.0, and from 4.17.0 you go on to 4.19.8 with [the section above](#upgrading-from-4170). Everything below was proven against 4.17.0. Why bother: 4.16.0 has bugs a running plant can trip over, and 4.17.0 fixes them. The command prompt can swallow a Set, so an operator watches the prompt close, figures the setpoint went out, and nothing was written. Saving a rule in the editor, even to rename it, quietly resets its run budget to five minutes. A device file pushed to an Edge Relay never lands, while the Gateway says `config pushed`. And an EmberNet App Store deploy loses its tenant. On top of the fixes you get [recipes](/iot/recipes/), [scripts](/iot/scripts/), [blueprints](/iot/blueprints/), [helpers](/iot/helpers/), the [logbook](/iot/logbook/), the new [automation](/iot/automations/) steps, read-only [UniFi](/iot/unifi/), and [MCP](/iot/mcp/) going from 16 tools to 25. @@ -182,7 +200,7 @@ We proved that by rendering both charts side by side, not by running it on a liv ### API changes from 4.3.1 -If you have a script or an integration against the API, this is the list. Everything else is additions. The 4.16.0 to 4.17.0 changes [above](#what-a-script-against-the-api-will-notice) come on top, and so do the 4.17.0 to 4.18.11 ones [above them](#api-changes-from-4170). +If you have a script or an integration against the API, this is the list. Everything else is additions. The 4.16.0 to 4.17.0 changes [above](#what-a-script-against-the-api-will-notice) come on top, and so do the 4.17.0 to 4.19.8 ones [above them](#api-changes-from-4170). - **Empty lists are `[]`, never `null`,** in every response and every stream event, at any depth. `GET /api/runs`, automations, a device's tags, the write audit, users, alarm events, sparklines, and an empty history window all used to answer `null`. A `null` you still see is never a list; it's a documented nullable field, like an entity's `equipment_id`. - **`/api/history` buckets with no good reading are `null`,** and draw as gaps, and bad readings stay out of trend averages. A trend and a report over the same hour now say the same thing. diff --git a/src/content/docs/install/auto-update.md b/src/content/docs/install/auto-update.md index 3845202..20aad38 100644 --- a/src/content/docs/install/auto-update.md +++ b/src/content/docs/install/auto-update.md @@ -5,10 +5,6 @@ description: "Every box follows the stable tag and Podman moves it. What an upda editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - Every box tracks `:stable` and Podman updates it. That is the rule for all four products, and it is why no unit here pins a release number: a box pinned to `4.17.0` is exactly as old as the day somebody installed it, and nobody goes diff --git a/src/content/docs/install/helm.md b/src/content/docs/install/helm.md index 2f8cbf2..1557ca9 100644 --- a/src/content/docs/install/helm.md +++ b/src/content/docs/install/helm.md @@ -5,10 +5,6 @@ description: "Deploy any HotLoop chart on k3s or any Kubernetes: the values wort editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - The chart deploys two workloads: the gateway (a Deployment, it holds no durable state of its own) and TimescaleDB (a StatefulSet with a volume). That is the default. `database.type` picks another database: plain PostgreSQL, or SQLite, @@ -205,6 +201,21 @@ deliberately unrestricted: this app exists to reach equipment at addresses nobody can enumerate in advance, and an egress policy that silently blocks a poll looks like a broken driver rather than a firewall rule. +### Hardware: USB radios, serial adapters, and Bluetooth + +From 4.19.7 every HotLoop chart (Gateway, IoT, Edge and the Edge Relay) ships with the access hardware needs, on, so a plain install from the App Store or from helm sees what is plugged into the node with no hand edit, on the pod network. It is the `hardware:` block in `values.yaml`: three switches that default on, and one opt-in that defaults off: + +| Value | Default | What it does | +|---|---|---| +| `hardware.privileged` | `true` | The app container runs privileged (`privileged: true`, `allowPrivilegeEscalation: true`) and as root (`runAsUser: 0`, `runAsNonRoot: false`, no capability drop). Root because a USB serial node is `root:dialout 0660` and BlueZ's D-Bus policy lets root and the `bluetooth` group in; a container user in neither opens nothing. The root filesystem stays read-only, and the init container and the bundled database keep their own non-root, all-capabilities-dropped context. | +| `hardware.usbDevices.enabled` | `true` | The node's `/dev` (`hardware.usbDevices.hostPath`) is mounted at `/dev`: USB serial adapters (`/dev/ttyUSB*`, `/dev/ttyACM*`, `/dev/serial/by-id/*`), including one plugged in after the pod started. A privileged container is only handed the devices that existed when it started; the mount is what makes a replugged stick appear. | +| `hardware.bluetooth.enabled` | `true` | The node's D-Bus directory (`hardware.bluetooth.dbusPath`, `/run/dbus`, which holds `system_bus_socket`) is mounted at the same path, so a client reaches the node's BlueZ. It is created empty on a node with no D-Bus, so the pod still starts there. | +| `hardware.bluetooth.hostNetwork` | `false` | The opt-in, off by default, for what needs the node's network: kernel Bluetooth sockets (HCI, L2CAP) exist only in the node's network namespace, so a client that opens one directly, and discovery that listens on the node's own network (mDNS, SSDP, a radio's multicast), need it. A client that goes through BlueZ's D-Bus API needs only the mounted socket and works from the pod network. On, the pod runs on the node's network with the consequences under [Networking](#networking). | + +What HotLoop does with it today: nothing yet. HotLoop has no Bluetooth, Z-Wave or USB serial code of its own (Zigbee comes through a Zigbee2MQTT bridge over MQTT, see [BROKER.md](/iot/broker/)); this is the access a feature, or a sidecar that reads a radio, needs, in place before it is asked for. + +What it costs, on purpose and decided: the pod is root on the node (privileged, with the node's `/dev`), and the NetworkPolicy chart default stays on for what it covers. The pod stays on the pod network. Turn any part off with `--set hardware.privileged=false --set hardware.usbDevices.enabled=false --set hardware.bluetooth.enabled=false`; with all three off the render is, line for line, the pod 4.19.6 rendered (non-root, every capability dropped, read-only root, pod network). For what needs the node's network, set `hardware.bluetooth.hostNetwork=true` (or `hostNetwork=true`): the page then answers on every address of the node at `service.targetPort`, one HotLoop per node per port, a NetworkPolicy does not apply to the pod, and `nodeSelector` pins it to the node whose radio it should see. + ### Edge deployments The app image is built for amd64 and arm64, and TimescaleDB 2.30.1-pg17 is diff --git a/src/content/docs/install/index.md b/src/content/docs/install/index.md index ae9c5af..8c04946 100644 --- a/src/content/docs/install/index.md +++ b/src/content/docs/install/index.md @@ -21,10 +21,10 @@ There are two ways to run a HotLoop product for real: Helm on k3s or any Kuberne | Product | Latest release | Helm | Quadlet | Start here | |---|---|---|---|---| -| [HotLoop IoT](/iot/overview/) | 4.18.11 | Yes | Yes | [Install IoT](/iot/install/) | -| [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 IoT](/iot/overview/) | 4.19.8 | Yes | Yes | [Install IoT](/iot/install/) | +| [HotLoop Edge](/edge/overview/) | 4.19.8 | Yes | Yes | [Install Edge](/edge/install/) | +| [HotLoop Gateway](/gateway/overview/) | 4.19.8 | Yes | Yes | [Install the Gateway](/gateway/install/) | +| [HotLoop Edge Relay](/edge-relay/overview/) | 4.19.8 | Yes | Yes | [Install the Edge Relay](/edge-relay/install/) | | [HotLoop Flow](/flow/overview/) | 2.8.1 | 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/) | diff --git a/src/content/docs/install/quadlet.md b/src/content/docs/install/quadlet.md index 38af714..c5714e2 100644 --- a/src/content/docs/install/quadlet.md +++ b/src/content/docs/install/quadlet.md @@ -5,10 +5,6 @@ description: "Run HotLoop as real systemd services on a Pi or small industrial P editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - For a Pi or a small industrial PC that is **not** part of a k3s cluster. If it is, use the product's chart instead. This directory and the charts are the two supported ways to run HotLoop. diff --git a/src/content/docs/iot/api.md b/src/content/docs/iot/api.md index 932ab9a..2b4d5df 100644 --- a/src/content/docs/iot/api.md +++ b/src/content/docs/iot/api.md @@ -5,10 +5,6 @@ description: "Every call HotLoop answers over HTTP, what it needs, what it takes editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - Every call the Gateway answers over HTTP, what it needs, what it takes and what it gives back. The web UI uses nothing else: there is no private API behind the screens, so anything the UI can do, a script can do with the same calls and the @@ -738,7 +734,7 @@ binary and a Helm install can't) and is left out otherwise. "protocols": ["codesys","ethernet-ip","http","modbus","mqtt","mtconnect","opcua","s7comm","snmp","unifi"] }, "siteId": "docs", - "version": "4.19.0" + "version": "4.19.8" } ``` @@ -788,7 +784,7 @@ A device is one connection over one protocol. What goes in `config` and ### `GET /api/devices` -Every device, with its live connection state. Credentials are `[redacted]`. +Every device, with its live connection state. Credentials are `[redacted]`. `?q=` keeps the devices whose id, name, address or protocol holds the text (case-insensitive), `?limit=` (1 to 5000, anything else is `400`) the first of them; with either, `X-Total-Count` is how many matched. ```json [{"id":"tank-farm","name":"Tank farm","protocol":"mqtt","address":"base-mqtt:1883","enabled":true, @@ -1393,9 +1389,13 @@ it still exist: ignore or delete those first, or switch the broker off with | Query | Details | |---|---| -| `device` | Every tag on one device. When set, `q` and `limit` are ignored | -| `q` | Search: a case-insensitive substring of the device id, tag name or description | -| `limit` | Default 500, at most 1000 | +| `device` | Only this device's tags. With no `limit` it answers every one of them | +| `q` | Search: a case-insensitive substring of the device id, tag name or description. Works with `device` | +| `limit` | A whole number from 1 to 5000. Default 500 with no `device`. Anything else, 5001 included, is `400`: it is refused, never answered with fewer (up to 4.19.5 any limit over 1000 was quietly turned into 200) | +| `writable` | `true` for only the tags marked writable (what the write pickers offer) | +| `offset` | Where the page starts, zero or more. Tags are ordered by device, then name | + +The answer's `X-Total-Count` header is how many tags match in all, so a client pages with `offset` until it has them and knows it did. An empty answer is `[]`. ```json [{"id":5,"deviceId":"tank-farm","name":"level_sp","address":"mqtt:docs-api-example/level_sp","dataType":"float","unit":"%","enabled":true,"writable":true,"maxValue":95,"hasRange":true,"scale":1,"createdAt":"2026-09-27T20:21:39.405164Z","updatedAt":"2026-09-27T20:21:39.405164Z"}] @@ -1609,6 +1609,7 @@ Every entity, orphans included. | `domain` | `sensor`, `switch`, `motor`... | | `device` | Entities on one device | | `orphaned` | `true` for only orphans, `false` for none | +| `q`, `limit` | Search: a case-insensitive substring of the entity id, name or device id, and at most `limit` (1 to 5000, anything else is `400`, never cut quietly) of them ordered by entity id. `X-Total-Count` is how many matched. Left out, the list is whole | ```json [{"id":5,"entity_id":"switch.tank_farm_pump","unique_id":"tag-control:tank-farm/pump","domain":"switch","platform":"tag","name":"pump","device_id":"tank-farm","options":{},"disabled":false,"hidden":false,"created_at":"2026-09-27T20:21:39.153197Z","updated_at":"2026-09-27T20:21:39.153197Z","bindings":[{"role":"command","device_id":"tank-farm","tag_name":"pump"},{"role":"state","device_id":"tank-farm","tag_name":"pump"}],"equipment_id":null,"area_id":4,"labels":[]}] @@ -1854,7 +1855,7 @@ under it. See [EQUIPMENT.md](/iot/equipment/). ### `GET /api/equipment` Every node, ordered by id, each with its `path` of names from the root. -`?level=area` filters. +`?level=area` filters. `?q=` keeps the nodes whose path holds the text (case-insensitive), `?limit=` (1 to 5000, anything else is `400`) the first of them; with either, `X-Total-Count` is how many matched. ```json [{"id":4,"parent_id":1,"level":"area","name":"Utilities","created_at":"2026-09-27T20:21:01.637319Z","updated_at":"2026-09-27T20:21:01.637319Z","path":["Demo Plant","Utilities"]}] @@ -3068,7 +3069,7 @@ Then, as things happen: | Event | Data | When | |---|---|---| -| `values` | An array of readings, the `GET /api/values` shape, newest per tag | Every 250 ms, if anything changed. Coalesced: a plant making five thousand readings a second must not push five thousand messages a second at a browser that repaints sixty times | +| `values` | An array of readings, the `GET /api/values` shape, newest per tag, **only the tags whose reading changed** | Every 250 ms, if any did. A tag whose value, text, unit, quality and reason are the same as the last one sent is left out, and sent again once it has gone 30 seconds unsent, so a steady value's `timestamp` is never more than that old. A driver reads every tag on every poll; 4.19.5 sent them all every time (about 150 KB four times a second for a tester's 2232 tags) and a page on a slow link fell minutes behind | | `entities` | `{"changed": [states], "removed": [entity_ids]}` | Every 250 ms, if any entity state changed. `removed` is an entity disabled, deleted or renamed away from | | `alarm` | `{"alarm": {...}, "event": "shelved"}`, the alarm and what happened to it (`raised`, `acked`, `cleared`, `shelved`, `unshelved`, `suppressed`, `unsuppressed`) | Immediately: alarm transitions are rare and each one matters | | `device` | `{"deviceId":"tank-farm","state":"connected"}` | Immediately: the screen should go red when a device drops, not at the next flush | @@ -3083,9 +3084,13 @@ data: {"deviceId":"tank-farm","state":"connected"} ``` A comment line (`: heartbeat 2026-09-27T20:35:00Z`) every 20 seconds keeps -proxies from closing an idle connection. A client that cannot keep up misses -`values` and `entities` updates rather than stalling everyone else; the next -one carries current values, so nothing is lost for long. The response sets +proxies from closing an idle connection. Each open page is paced on its own: its +writer rests as long as its last send took (at most five seconds), and what +arrives meanwhile is held per tag and per entity, newest only, so a page on a +slow link stays a send or two behind the plant, never minutes, loses nothing, +and takes nothing from the other pages. The stream is gzip-compressed when the +request says `Accept-Encoding: gzip` (a browser does), which takes a tester's +first picture from about 326 KB to about 11 KB on the wire. The response sets `X-Accel-Buffering: no`, because a buffering proxy turns a live feed into a very slow download. diff --git a/src/content/docs/iot/authentication.md b/src/content/docs/iot/authentication.md index dd1fa4d..5a01568 100644 --- a/src/content/docs/iot/authentication.md +++ b/src/content/docs/iot/authentication.md @@ -5,10 +5,6 @@ description: "Logins, roles, tokens and the users screen, and what a deployment editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - Added in 4.4.0. Before it, this gateway had no login of its own: the EmberNet dashboard's reverse proxy and a NetworkPolicy were the whole perimeter, and the actor recorded against a write came from a header the proxy set, spoofable by diff --git a/src/content/docs/iot/backup.md b/src/content/docs/iot/backup.md index 9233b92..b404917 100644 --- a/src/content/docs/iot/backup.md +++ b/src/content/docs/iot/backup.md @@ -5,10 +5,6 @@ description: "Logical snapshots of the database, restores that refuse a newer sc editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - Added in 4.12.0. `internal/backup` takes logical, table-data-only snapshots of the historian and restores them. Off by default (`HOTLOOP_BACKUP_ENABLED`): a deployment that has not configured a backup volume should not have this diff --git a/src/content/docs/iot/dashboards.md b/src/content/docs/iot/dashboards.md index 8d60c7f..0693e10 100644 --- a/src/content/docs/iot/dashboards.md +++ b/src/content/docs/iot/dashboards.md @@ -5,10 +5,6 @@ description: "Pages of cards built per role or per screen: gauges, trends, entit editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - A dashboard is a page of cards somebody built. Gauges, trends, lists of entities, a button that starts the pump, a floor plan with the temperatures sitting where the zones are. Build as many as the plant needs, give each to the diff --git a/src/content/docs/iot/helpers.md b/src/content/docs/iot/helpers.md index cb86be1..37d8bff 100644 --- a/src/content/docs/iot/helpers.md +++ b/src/content/docs/iot/helpers.md @@ -5,10 +5,6 @@ description: "The values a plant's people own, like a batch target or which shif editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - A helper is a value the plant's people own rather than a PLC. "Line 2 is in changeover." "Today's batch target is 1,250 kg." "B shift is on." "Twelve parts scrapped since the last reset." "The purge has nine minutes left." None of diff --git a/src/content/docs/iot/historian-targets.md b/src/content/docs/iot/historian-targets.md index 783c714..8f19811 100644 --- a/src/content/docs/iot/historian-targets.md +++ b/src/content/docs/iot/historian-targets.md @@ -5,10 +5,6 @@ description: "Write every reading HotLoop keeps out to the plant's own PostgreSQ editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - HotLoop keeps its own history, and it isn't going anywhere. But a plant already has a database. IT backs it up, patches it, points Power BI at it and answers for it in an audit, and they are not adopting ours to get a trend. diff --git a/src/content/docs/iot/install.md b/src/content/docs/iot/install.md index d157e07..e432cbc 100644 --- a/src/content/docs/iot/install.md +++ b/src/content/docs/iot/install.md @@ -1,13 +1,13 @@ --- # generated by scripts/sync-docs.mjs -title: "Install HotLoop IoT 4.18.11" -description: "Install HotLoop IoT 4.18.11 on k3s with Helm, or on a Pi or small PC as systemd services with Podman Quadlet, on PostgreSQL, or a SQLite file." +title: "Install HotLoop IoT 4.19.8" +description: "Install HotLoop IoT 4.19.8 on k3s with Helm, or on a Pi or small PC as systemd services with Podman Quadlet, on PostgreSQL, or a SQLite file." sidebar: label: "Install" editUrl: false --- -HotLoop IoT first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-iot:4.18.11`, built for amd64, arm64, and arm/v7, and the chart is `hotloop-iot` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. +HotLoop IoT first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-iot:4.19.8`, built for amd64 alone this release (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), and the chart is `hotloop-iot` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. Two ways in. A cluster gets Helm. A Pi, a NUC, or a small industrial PC with no cluster gets Podman Quadlet. Either way the database is a choice you make once: PostgreSQL 17 (the default), or a SQLite file with no database container at all. @@ -16,7 +16,7 @@ Two ways in. A cluster gets Helm. A Pi, a NUC, or a small industrial PC with no ```bash helm repo add hotloop https://hotloop.io/hotloop helm repo update -helm install hotloop hotloop/hotloop-iot --version 4.18.11 \ +helm install hotloop hotloop/hotloop-iot --version 4.19.8 \ --namespace hotloop --create-namespace ``` @@ -27,7 +27,7 @@ That deploys IoT plus PostgreSQL 17.11 beside it on a 10 Gi volume, keeping 30 d For a SQLite file and no database pod, ask for it when you install: ```bash -helm install hotloop hotloop/hotloop-iot --version 4.18.11 \ +helm install hotloop hotloop/hotloop-iot --version 4.19.8 \ --namespace hotloop --create-namespace \ --set database.type=sqlite ``` @@ -63,7 +63,7 @@ Put these in a values file and install with `-f`. | `global.timezone` | `America/New_York` | Schedules and shift changes run on it. It has to be a real zone name, because one that won't load stops startup. | | `publicUrl` | empty | The address a phone on the plant Wi-Fi reaches HotLoop at. Without it, notifications have no link back and ntfy gets no Acknowledge button. | | `safety.allowWrites` | `false` | The master switch. Off, nothing writes to a machine, from anywhere. | -| `safety.allowMcpWrites` | `false` | Lets agents write through the same gate. It narrows the master switch and never goes around it. | +| `safety.allowMcpWrites` | `true` | Lets agents write through the same gate, on since 4.19.4. It follows the master switch: with `safety.allowWrites` off, nothing writes, agents included. Set it `false` and agents can still read the whole plant and never write. | | `historian.retentionDays` | `30` | Days of history kept. The Gateway and Edge keep 90, because a Pi's disk is small. | | `logbook.retentionDays` | `90` | Days of entity state changes the [logbook](/iot/logbook/) keeps. | | `database.postgresql.persistence.size` | `10Gi` | Your history lives here. Size it for your tag count and the retention above. | diff --git a/src/content/docs/iot/integrations.md b/src/content/docs/iot/integrations.md index fb0ff5b..2f78be7 100644 --- a/src/content/docs/iot/integrations.md +++ b/src/content/docs/iot/integrations.md @@ -5,10 +5,6 @@ description: "Point HotLoop at a device you know the address of, look at the rea editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - An integration is how you point HotLoop at a kind of device you know the address of, answer a few questions, see what is actually on it, and keep the points you want. It makes the device and its tags. Entities follow from the tags diff --git a/src/content/docs/iot/mcp.md b/src/content/docs/iot/mcp.md index c12c3c0..1a26d71 100644 --- a/src/content/docs/iot/mcp.md +++ b/src/content/docs/iot/mcp.md @@ -5,10 +5,6 @@ description: "Let an agent read the whole plant and touch only what you armed, t editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - The gateway speaks MCP in both directions: agents can read and drive the plant, and an automation rule can call a tool somewhere else. diff --git a/src/content/docs/iot/mqtt-discovery.md b/src/content/docs/iot/mqtt-discovery.md index 059e4dc..359adf4 100644 --- a/src/content/docs/iot/mqtt-discovery.md +++ b/src/content/docs/iot/mqtt-discovery.md @@ -5,10 +5,6 @@ description: "Point ESPHome, Zigbee2MQTT, Tasmota or a Shelly at the broker and editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - Point a device at HotLoop's broker and it shows up. Its sensors read, its switches show their state, its entities have the names, units and classes it announced. Nobody types a tag list. diff --git a/src/content/docs/iot/overview.md b/src/content/docs/iot/overview.md index e5b4ed4..2fbbdfc 100644 --- a/src/content/docs/iot/overview.md +++ b/src/content/docs/iot/overview.md @@ -8,7 +8,7 @@ editUrl: false --- :::note[IoT is out, first released in 4.18.10] -The image is `ghcr.io/hotloop-io/hotloop-iot:4.18.11` for amd64, arm64, and arm/v7, about 31 MB on amd64 and 29 MB on a Pi, and the chart is `hotloop-iot` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `iot/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/iot/install/). The pages in this section say what each feature has been run against, and where a page follows `main` and not the release, it says that too. +The image is `ghcr.io/hotloop-io/hotloop-iot:4.19.8` built for amd64 alone this time (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), about 31 MB on amd64 and 29 MB on a Pi, and the chart is `hotloop-iot` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `iot/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/iot/install/). The pages in this section say what each feature has been run against, and where a page follows `main` and not the release, it says that too. ::: IoT is the base Edge and the Gateway are built on. If the box in front of you is a Pi, a NUC in a closet, a rack of smart plugs and sensors, or one PLC on a bench, this is the one. It is the smallest image of the three full products, and it still carries everything that makes HotLoop useful: the entities, the rules, the alarms, and the one write gate that nothing gets around. @@ -21,8 +21,8 @@ A bigger product never takes anything away from it. [Edge](/edge/overview/) is I - **Automations, blueprints, scripts, and recipes.** Rules compiled when you save, and named sequences, and setpoint sets that go through the write gate. [Automations](/iot/automations/), [blueprints](/iot/blueprints/), [scripts](/iot/scripts/), [recipes](/iot/recipes/). - **Helpers, the logbook, and the sun.** The values your people own, one timeline of everything that happened, and sunrise worked out on the box. [Helpers](/iot/helpers/), [logbook](/iot/logbook/), [the sun](/iot/sun/). - **Dashboards, notifications, and MCP.** Screens built per role, alarms that page a phone, and an agent that can read the plant and touch only what you armed. [Dashboards](/iot/dashboards/), [notifications](/iot/notifications/), [MCP](/iot/mcp/). -- **Every protocol driver, MQTT with discovery, and an embedded broker.** [Protocols](/iot/protocols/), [the broker](/iot/broker/), [MQTT discovery](/iot/mqtt-discovery/), [network discovery](/iot/network-discovery/). -- **UniFi, read-only,** and **history written out to your own database.** [UniFi](/iot/unifi/), [historian targets](/iot/historian-targets/). +- **Every protocol driver (a CODESYS runtime by symbol name, and OPC UA PubSub over UADP included), MQTT with discovery, and an embedded broker.** [Protocols](/iot/protocols/), [the broker](/iot/broker/), [MQTT discovery](/iot/mqtt-discovery/), [network discovery](/iot/network-discovery/). +- **UniFi, read-only,** and **history written out to your own database, or to a Kafka broker.** [UniFi](/iot/unifi/), [historian targets](/iot/historian-targets/). - **Users, backups, and migrations that only add.** [Authentication](/iot/authentication/), [backups](/iot/backup/), [migrations](/iot/migrations/), and [the HTTP API](/iot/api/). [The write gate](/gateway/write-gate/) is the one page to read before a real plant sees any of this. It applies to every product. diff --git a/src/content/docs/iot/protocols.md b/src/content/docs/iot/protocols.md index 7838b96..6f3f47c 100644 --- a/src/content/docs/iot/protocols.md +++ b/src/content/docs/iot/protocols.md @@ -5,10 +5,6 @@ description: "OPC UA, Modbus, MQTT and Sparkplug B, EtherNet/IP, S7comm, MTConne editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - This document says exactly what the gateway speaks, exactly how far each driver has been verified, and what to do about everything else. It is the first thing to read and the one to trust when something disagrees with it. @@ -577,8 +573,10 @@ listens for unicast. A tag's address is `/`, the field's position in the publisher's DataSet counting from 0: `1000/3` is the fourth field writer 1000 publishes. UADP carries no field names (they live in a DataSetMetaData the publisher serves some other way, or not at all), so the -position is what a datagram can say. Browse lists the fields that have been -heard, with their types. +position is what a datagram can say. A field that is an array is read one +element at a time: `1000/3[17]` is the eighteenth value of the array that is +the fourth field. Browse lists the fields that have been heard, with their +types, and an array as its elements (the first 1000 of them). ```json {"publisherId":"2234","writerGroupId":100,"interface":"eth0","timeoutMs":30000, @@ -587,11 +585,25 @@ heard, with their types. - `publisherId` and `writerGroupId` keep a device to one publisher and group when several share an address. Empty and zero take everything. -- `interface` is the network card a multicast group is joined on. A machine with - more than one needs it. +- The group, the port and the card are all the device's: nothing is fixed in + HotLoop. `udp://239.192.0.1:4850` with `"interface":"eth0"` joins that group + on that card, and `udp://127.0.0.1:4842` listens for one publisher on the same + machine. Several devices can listen on different groups and ports at once. +- `interface` is the network card a multicast group is joined on, by name + (`eth0`, `lo`) or by one of its IPv4 addresses (`127.0.0.1`). A machine with + more than one card needs it. A publisher on the same machine that sends to a + group over loopback (`IP_MULTICAST_IF` 127.0.0.1, TTL 0, loopback on, no route) + is heard by a device with `"interface":"lo"` or `"127.0.0.1"`; with no + interface named and no route, the join fails with `no such device` and says so. - `dataSets` is only for RawData, which carries no types on the wire: the publisher's field types, in order, so the bytes can be read. Variant and - DataValue messages carry their own. + DataValue messages carry their own. Each entry is a type name (`"Float"`), and + can also say how a block of values is laid out: `"Float*250"` is 250 fields + one after another with no length in front, which is how a PLC that sends a + block of REALs as separate fields writes it; `"Float[250]"` is one array field + of at most 250 values, written as the OPC Foundation stack writes a RawData + array (an Int32 count, the values, and zero padding up to 250 of them); and + `"String(40)"` is a String padded to 40 bytes, alone or as `"String(40)[4]"`. - Key frames replace a DataSet's fields; delta frames change the ones they name; keep alives prove the publisher is alive. A lost message (a gap in the sequence numbers) marks that DataSetWriter's tags **uncertain** until its next @@ -605,8 +617,12 @@ heard, with their types. - The reading's time is the publisher's, from the field, the DataSetMessage or the NetworkMessage, when it is set and not more than a minute ahead of this clock, and the time it arrived otherwise. +- Arrays are read in Variant and DataValue messages (an Int32 count and the + values, OPC 10000-6 5.2.2.16; a multi-dimensional one comes flat, row by + row) and, with a layout, in RawData. A count the datagram cannot hold is + refused before anything is made for it. - Read-only. Message security (signed or encrypted NetworkMessages), chunked - messages, discovery messages, events, and array or structure fields are not + messages, discovery messages, events, and structure fields are not supported; a datagram with any of them is refused, counted by reason, and never guessed at. @@ -650,7 +666,9 @@ things about open62541 itself, found doing it: its subscriber discards keep alive messages ("Only keyframes are supported") though a keep alive is a valid DataSetMessage, and its RawData publisher sends one message and then stops with `BadInternalError`. HotLoop's publisher is read correctly by its subscriber all -the same, and the RawData read is of that one message. A CODESYS ST publisher and a Go receiver written elsewhere were read for interoperability and share no code with this. +the same, and the RawData read is of that one message. A CODESYS ST publisher and a Go +receiver written elsewhere were read for interoperability and share no code +with this. ### CODESYS A PLC that runs a CODESYS Control runtime (Linux SL, ARM64, Raspberry Pi, Windows, diff --git a/src/content/docs/iot/recipes.md b/src/content/docs/iot/recipes.md index 661113d..f6d92fc 100644 --- a/src/content/docs/iot/recipes.md +++ b/src/content/docs/iot/recipes.md @@ -5,10 +5,6 @@ description: "A product grade's setpoints applied in one go: checked whole, refu editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - A recipe is a set of values that go out to the plant together. Grade A's zone temperatures, line speed and die gap. The changeover to the 500 ml bottle. The night setback. Every plant has these, and every plant has a laminated sheet diff --git a/src/content/docs/iot/scripts.md b/src/content/docs/iot/scripts.md index c8f16fe..b77fc89 100644 --- a/src/content/docs/iot/scripts.md +++ b/src/content/docs/iot/scripts.md @@ -5,10 +5,6 @@ description: "Write a cycle once, name it, and run it from a rule, a screen, MCP editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - A script is a sequence of steps with a name. The CIP cycle. The line purge. The changeover to product B. Every plant has a handful of procedures it runs exactly the same way every time, and the day somebody copies one into a diff --git a/src/content/docs/iot/unifi.md b/src/content/docs/iot/unifi.md index c5bdda0..69253a3 100644 --- a/src/content/docs/iot/unifi.md +++ b/src/content/docs/iot/unifi.md @@ -5,10 +5,6 @@ description: "Switches, ports, PoE, WAN failover and clients read from a UniFi c editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - A plant's network is equipment. When a PLC goes quiet, the first question is "is it the PLC, or the switch port it's plugged into", and until now the answer lived in the UniFi app on somebody's phone. So HotLoop reads the UniFi console diff --git a/src/content/docs/products.md b/src/content/docs/products.md index 1079db2..d403a80 100644 --- a/src/content/docs/products.md +++ b/src/content/docs/products.md @@ -5,10 +5,6 @@ description: "Seven products on four release lines, what each core product has, editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - HotLoop is seven products for the plant floor: four that grow with a site (IoT, Edge, Gateway, and Edge Relay), plus Flow for wiring, Cascade for workflows, and Transmitter for putting a PLC's values on the wire. They ship on four release lines. **HotLoop core** is IoT, Edge, Gateway, and Edge Relay: four products, one codebase, one tag. Every core release ships all four in lockstep, same version, each with its own image, its own Helm chart and its own diff --git a/src/content/docs/releases/hotloop-4-19-0.md b/src/content/docs/releases/hotloop-4-19-0.md deleted file mode 100644 index 79a1101..0000000 --- a/src/content/docs/releases/hotloop-4-19-0.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -# generated by scripts/sync-docs.mjs -title: "HotLoop core 4.19.0 release notes" -description: "What shipped in HotLoop core 4.19.0, what changed on upgrade, and what to read before you touch a running plant." -editUrl: false -sidebar: - label: "4.19.0" - badge: - text: Next release - variant: caution ---- - -:::caution[On main, next release] -This page was written from `main` at 4d5da2f. These notes are for a release that is not out. HotLoop core 4.18.11 is the latest one you can install. -::: - -A value you write to a scaled tag now goes to the device the way you meant it. That is the one thing in 4.19.0 you need to read before you touch a running plant, -because it changes what lands in a register, and a plant that worked around the old behavior has to stop. A Kafka event stream and OPC UA PubSub over UADP come with it, both new, neither changing anything that runs today. - -## MCP ships on, with writes - -- **Agents can write now, where an operator could.** MCP is on in every HotLoop product and reads and writes. `allowMcpWrites` defaults to true in the code and in all three charts. A deployment that has `allowWrites` on and never set `allowMcpWrites` gets agent writes after this upgrade, through the same gate as the screen: the user's permissions, the tag's writable mark and its range all still apply. -- **`allowWrites` did not change.** It stays off by default and still decides whether anything can write, the UI and agents alike. -- **A deployment that saved `allowMcpWrites: false` stays off**, including one installed from the App Store, which stores every answer. Change it if you want agents to write. -- **The MCP endpoint is never open.** A HotLoop started with no `HOTLOOP_MCP_TOKEN` (a Quadlet unit, a bare binary) makes a token at its first start and keeps it in its database. A call without it is a `401`. `hotloop mcp-token` prints it. The charts already set one. - -### Writes to a scaled tag - -- **What was wrong.** A tag with a scale, say 0.1 on a temperature register that holds tenths of a degree, shows you the engineering value: the register says 555 and the screen says 55.5. Writing went the other way without the same care. Type 55.5 for the setpoint and HotLoop put 55 in the register, which then read back as 5.5. It was the same on every screen, through the API, from an automation, a script, a recipe, a loop and over MCP, because they all go through the same write gate. The audit row said 55.5 the whole time, so nothing looked wrong in the log. -- **What changes.** The number you write is in the tag's own units, the same as the number you read. HotLoop takes the scale and the offset back off before it sends: 55.5 on a tag scaled by 0.1 puts 555 in the register, and it reads back as 55.5. A tag with no scale and no offset is not touched at all. Bits, coils, strings and Sparkplug and MQTT writes of a flag are not scaled either. -- **If you worked around it, stop.** If anyone has been typing raw counts into a scaled tag to get the right number, typing 555 to get 55.5, that now sends 5550 and reads back as 555. Type the engineering value. Look at your scaled tags before you upgrade: any tag with a scale other than 1, or an offset, that somebody writes to by hand, from an automation or from a recipe is worth a look. Recipes and automations that wrote counts need their numbers changed to engineering values in the same step as the upgrade. -- **A value the register cannot hold is refused.** After the scale is taken off, a count that does not fit the register, 7000 on a `uint16` scaled by 0.1, is refused with a plain message that names the count and the register, and nothing is sent. It used to wrap quietly: 70000 on a 16-bit register went out as 4464. That goes for Modbus, S7, EtherNet/IP and OPC UA integer types. -- **A recipe with a value that does not fit is refused whole.** Before its first write, every target is checked, so a recipe never half-applies and then refuses. For Modbus, S7 and EtherNet/IP the check knows each tag's register type from its address. For OPC UA, which is the path to a CODESYS runtime, it uses the type the tag's last reading arrived as. A tag that has never been read since HotLoop started has no type to check against yet, so a recipe that targets it is refused at that tag's own write, after the targets before it were written, and the application records which. Reading it once, which the poll does within a second of connecting, closes that. -- **A fraction of a count is cut off.** A register holds whole counts, so 12.34 on a tenth-scaled register holds 12.3, as it always did. -- **What did not change.** The range check on a tag and the audit row use the number you typed. The write gate, the permissions and the refusal rules are the same. - -### A Kafka event stream - -- **What it is.** HotLoop can send its readings and its system log to any broker that speaks the Kafka API: Apache Kafka, Redpanda, and the rest of that family. It is a new kind of historian target, under Settings, Historian targets, so it has the queue, the catch-up from HotLoop's own history and the status those have. Readings go to one topic and the system log (alarms, devices going up and down, changes) to another, as JSON. -- **TLS and SASL.** SCRAM-SHA-256, SCRAM-SHA-512 and PLAIN (PLAIN only over TLS), with your own CA if the broker's certificate isn't from a public one. Client certificates, Kerberos and OAuth logins are not supported yet. -- **It is at least once.** A reading can arrive twice after an outage, because the catch-up overlaps what already went out on purpose. `deviceId`, `tag` and `ts` name a duplicate. Events are the live feed and have no history to catch up from, so one produced while the broker is away is lost, and the target's status counts it. -- **A broker that vanishes mid-write.** The target goes down within 25 seconds and the readings in that write are owed, instead of staying "up" with its queue overflowing. -- **A topic made a moment ago.** The first look at a topic that was just made, by you or by the target's own last try, waits for it to exist and have a leader instead of reporting the target down. -- **What it costs.** About 5 MiB on the binary of every product with a database. The Edge Relay has no historian targets and doesn't carry it. The client is franz-go, credited in `NOTICE`. -- **Upgrading.** Migration 0026 adds an `options` column to the historian targets, empty for every target that exists. Nothing changes for them. HISTORIAN-TARGETS.md has the record formats and what is and isn't proven. - -### OPC UA PubSub over UADP - -- **Subscribing.** A device with the protocol `opcua-pubsub` listens for a publisher's DataSets sent as UADP over UDP, unicast or multicast. A tag's address is `/`. Variant, DataValue and RawData fields, key, delta and keep alive frames. A lost message marks that writer's tags uncertain until its next key frame, and a silent writer reads bad. -- **Publishing.** Edge and Gateway can publish the tags a layout names as UADP, off until `HOTLOOP_UADP_PUBLISH_ENABLED` is set. Key frames when values change and keep alives when they don't. A bad tag is published as its status, never as its last value. -- **Not yet.** Message security (signed or encrypted messages) is refused by name, as are chunked messages, discovery, events and array fields. Delta frames are read but not sent. -- **What it was proven against.** open62541 1.5.10, an independent OPC UA stack, in both directions in the lab. It has not been run against a CODESYS, Siemens or Beckhoff publisher or across a router. PROTOCOLS.md has the setup and the limits. diff --git a/src/content/docs/releases/hotloop-4-19-4.md b/src/content/docs/releases/hotloop-4-19-4.md new file mode 100644 index 0000000..276ce5d --- /dev/null +++ b/src/content/docs/releases/hotloop-4-19-4.md @@ -0,0 +1,113 @@ +--- +# generated by scripts/sync-docs.mjs +title: "HotLoop core 4.19.4 release notes" +description: "What shipped in HotLoop core 4.19.4, what changed on upgrade, and what to read before you touch a running plant." +editUrl: false +sidebar: + label: "4.19.4" +--- + +4.19.0, 4.19.1, 4.19.2 and 4.19.3 were tagged and never published, so nothing named any of them exists and `stable` never moved to one. This is the first release with everything their notes list, which includes one thing that is new since 4.19.1: the charts take `hostNetwork`. + +- 4.19.0's release run died in its Test job, before any image, chart or release was made: the runner's environment file refused the Kafka broker's CA certificate, which was written to it as many lines. The step that writes it now writes such a value in the runner's delimiter form and reads it back the way the runner does, and the release guard checks that. +- 4.19.1 added the fix for that and one more (a discovery listener that said it was up before it could hear, below) and was skipped before it released. +- 4.19.2 added `hostNetwork`. Its release run failed in the Test job on one discovery test, `TestAScoutHearsABroker`, which read a listener's status the instant its devices appeared, before the last of its subscriptions was granted (the status now waits for that, as it should); nothing else changed. +- 4.19.3's release run failed in its Test job on two tests, and both were bugs in the product: a discovery listener could say it was up on a wait that had run out, or on an answer to a connection that had gone, and a process stopped the next run of a script with its own stop coming back to it. Both are fixed below. +- Everything in 4.19.0's notes (MCP, scaled writes, the store sign-in, CODESYS, the Kafka event stream, OPC UA PubSub over UADP and the fixes) is in the notes below. + +What is new since 4.19.1: the charts have a `hostNetwork` setting. + +## hostNetwork + +Every HotLoop chart with a pod (the Gateway, IoT and Edge charts and the Edge Relay's) takes `hostNetwork: true`. It is off by default, and with it off the chart renders exactly what 4.19.1's did. + +- **What it is for.** A pod that has to hear what its node hears: UDP multicast on the node's loopback (a PLC runtime on the same box publishing OPC UA PubSub to a multicast group over loopback), or a second network card. A pod network never carries either. +- **What it does.** The Deployment gets `hostNetwork: true` and `dnsPolicy: ClusterFirstWithHostNet`, so the cluster's DNS names keep resolving. +- **What it means for the pod.** It uses the node's addresses and ports: the page answers on every address of the node at `service.targetPort`, so pick a port nothing on the node uses and keep authentication on. A NetworkPolicy does not apply to a host-network pod (the cluster's network plugin ignores them). Pin the pod to its node with `nodeSelector`. +- **A listener on the loopback.** The OPC UA PubSub subscriber joins a group on a named card by name or by address: `"interface": "127.0.0.1"` in the device's options joins on the loopback card. +- **Nothing else.** The code is what 4.19.1 would have shipped. The release guard now renders every chart with and without the setting and fails if either render is wrong. + +--- + +The rest of this release, from 4.19.0 and 4.19.1: + +Two things in 4.19.4 change what an existing plant does, so read these before you touch a running one. Agents over MCP can write now, and the MCP endpoint always has a token. And a value you write to a scaled tag finally lands in the register the way you meant it, which means anyone who worked around the old behavior has to stop. Everything after those two is new, or a fix. + +The full notes are in the README: +What changed in 4.19.4. + +### MCP is on, it writes, and it always has a token + +- **Agents can write now, where an operator could.** MCP is on in every HotLoop product and it reads and writes. `allowMcpWrites` defaults to true in the code and in all three charts. A deployment that has `allowWrites` on and never set `allowMcpWrites` gets agent writes after this upgrade, through the same gate as the screen: the user's permissions, the tag's writable mark and its range all still apply. +- **`allowWrites` did not change.** It stays off by default and it still decides whether anything can write, the UI and agents alike. If it is off, nothing writes, MCP included. +- **A deployment that saved `allowMcpWrites: false` stays off.** That includes one installed from the App Store, which stores every answer. Flip it if you want agents to write. +- **The endpoint is never open.** The charts always set an MCP token. A HotLoop started without `HOTLOOP_MCP_TOKEN` (a Quadlet unit, a bare binary) used to serve `/mcp` to anybody who could reach the port and only logged a warning. It now makes a 24 byte token at its first start and keeps it in its database, so every replica and every restore carries the same one. A call without the token is a `401`. A token you configured is never replaced. +- **To get the token on a bare or Quadlet install:** `hotloop mcp-token` prints it. If you have an agent pointed at `/mcp` on one of those installs with no token, it stops working at this upgrade until you give it that token. That is on purpose. +- **Who made the call.** A static token says which client called, never which person. With the OIDC settings on, a Vord login or a Fireball Hub assertion on `/mcp` makes the audit name the person (`mcp:pat@example.com`), and an optional write role narrows writes per caller on top of the switches above. It is off unless you set it, and the token keeps working beside it. docs/MCP.md has the setup. + +### Writes to a scaled tag + +- **What was wrong.** A tag with a scale, say 0.1 on a temperature register that holds tenths of a degree, shows you the engineering value: the register says 555 and the screen says 55.5. Writing went the other way without the same care. Type 55.5 for the setpoint and HotLoop put 55 in the register, which then read back as 5.5. It was the same on every screen, through the API, from an automation, a script, a recipe, a loop and over MCP, because they all go through the same write gate. The audit row said 55.5 the whole time, so nothing looked wrong in the log. +- **What changes.** The number you write is in the tag's own units, the same as the number you read. HotLoop takes the scale and the offset back off before it sends: 55.5 on a tag scaled by 0.1 puts 555 in the register, and it reads back as 55.5. A tag with no scale and no offset is not touched at all. Bits, coils, strings and Sparkplug and MQTT writes of a flag are not scaled either. +- **If you worked around it, stop.** If anyone has been typing raw counts into a scaled tag to get the right number, typing 555 to get 55.5, that now sends 5550 and reads back as 555. Type the engineering value. Look at your scaled tags before you upgrade: any tag with a scale other than 1, or an offset, that somebody writes to by hand, from an automation or from a recipe is worth a look. Recipes and automations that wrote counts need their numbers changed to engineering values in the same step as the upgrade. +- **A value the register cannot hold is refused.** After the scale is taken off, a count that does not fit the register, 7000 on a `uint16` scaled by 0.1, is refused with a plain message that names the count and the register, and nothing is sent. It used to wrap quietly: 70000 on a 16 bit register went out as 4464. That goes for Modbus, S7, EtherNet/IP and OPC UA integer types. +- **A recipe with a value that does not fit is refused whole.** Before its first write, every target is checked, so a recipe never half applies and then refuses. For Modbus, S7 and EtherNet/IP the check knows each tag's register type from its address. For OPC UA, which is the path to a CODESYS runtime, it uses the type the tag's last reading arrived as. A tag that has never been read since HotLoop started has no type to check against yet, so a recipe that targets it is refused at that tag's own write, after the targets before it were written, and the application records which. Reading it once, which the poll does within a second of connecting, closes that. +- **A fraction of a count is cut off.** A register holds whole counts, so 12.34 on a tenth scaled register holds 12.3, as it always did. +- **What did not change.** The range check on a tag and the audit row use the number you typed. The write gate, the permissions and the refusal rules are the same. + +### A store install signs you in as yourself + +The chart made a random admin password in a Secret the tenant can't read, so a store install landed on a login with no way through. With the dashboard's public key configured, a request carrying a valid signed assertion for this tenant and this Service signs that person in, makes their user the first time, keeps their role in step with what the dashboard sent, and the audit trail names them. A bad, expired, wrong tenant or wrong app assertion is refused, and local login is untouched. It is off unless a key is set, and the three product charts take the key from the store. docs/AUTH.md has the settings. + +### CODESYS, by the names the programmer gave the variables + +HotLoop reads and writes a CODESYS PLC through the runtime's own OPC UA server, the way CODESYS documents: the application's Symbol Configuration, published under the PLCopen IEC 61131-3 model. Discovery lists what the application exports with the types and the access the programmer gave them, walking into arrays and structures, and a tag is an IEC symbol that is found under whatever the project calls its PLC and application. A restart, a download or a lost link reads bad with the reason and comes back with no hands. I proved it against a real 4.22.0.0 runtime in the lab running my own program, with a write the PLC's own logic answers, and against a second program of over a hundred thousand variables, read in 2.4 seconds. It has not been run on a physical controller. + +This touches a driver plants run, so plainly: the OPC UA driver asked for 200 nodes a request and never read what the server allows. A CODESYS runtime publishes OperationLimits of 100 and refuses more, and the driver answered by declaring every discovered tag a float and by reading none of the tags of a device with more than a hundred. Requests are now cut to the limits the server publishes at connect, and halved when a server that publishes none says too many. A data type that is a subtype of a standard one (a CODESYS STRING, BYTE or TIME) is followed up to the standard type, so it is typed and written as that. Every OPC UA device gets this, not only CODESYS ones. + +### A Kafka event stream + +- **What it is.** HotLoop can send its readings and its system log to any broker that speaks the Kafka API: Apache Kafka, Redpanda, and the rest of that family. It is a new kind of historian target, under Settings, Historian targets, so it has the queue, the catch-up from HotLoop's own history and the status those have. Readings go to one topic and the system log (alarms, devices going up and down, changes) to another, as JSON. +- **TLS and SASL.** SCRAM-SHA-256, SCRAM-SHA-512 and PLAIN (PLAIN only over TLS), with your own CA if the broker's certificate isn't from a public one. Client certificates, Kerberos and OAuth logins are not supported yet. +- **It is at least once.** A reading can arrive twice after an outage, because the catch-up overlaps what already went out on purpose. `deviceId`, `tag` and `ts` name a duplicate. Events are the live feed and have no history to catch up from, so one produced while the broker is away is lost, and the target's status counts it. +- **A broker that vanishes mid write.** The target goes down within 25 seconds and the readings in that write are owed, instead of staying up with its queue overflowing. +- **A topic made a moment ago.** The first look at a topic that was just made, by you or by the target's own last try, waits for it to exist and have a leader instead of reporting the target down. +- **What it costs.** About 5 MiB on the binary of every product with a database. The Edge Relay has no historian targets and doesn't carry it. The client is franz-go, credited in `NOTICE`. +- **Upgrading.** Migration 0026 adds an `options` column to the historian targets, empty for every target that exists. Nothing changes for them. HISTORIAN-TARGETS.md has the record formats and what is and isn't proven. + +### OPC UA PubSub over UADP + +- **Subscribing.** A device with the protocol `opcua-pubsub` listens for a publisher's DataSets sent as UADP over UDP, unicast or multicast. A tag's address is `/`. Variant, DataValue and RawData fields, key, delta and keep alive frames. A lost message marks that writer's tags uncertain until its next key frame, and a silent writer reads bad. +- **Publishing.** Edge and Gateway can publish the tags a layout names as UADP, off until `HOTLOOP_UADP_PUBLISH_ENABLED` is set. Key frames when values change and keep alives when they don't. A bad tag is published as its status, never as its last value. +- **Not yet.** Message security (signed or encrypted messages) is refused by name, as are chunked messages, discovery, events and structure fields. Delta frames are read but not sent. +- **Arrays.** A field that is an array is read in Variant and DataValue messages, and in RawData when the device's DataSet says how it is laid out (`"Float[250]"` for an array with its count, `"Float*250"` for 250 separate fields). A tag names one element, `1000/3[17]`, and Browse lists them. This was checked against datagrams from open62541 1.4.9 (Variant and DataValue arrays of 250 REAL) and a CODESYS runtime publishing RawData arrays. +- **Where it listens.** The group, the port and the network card are the device's own. A multicast group on a named card and a unicast port on this machine both work. +- **What it was proven against.** open62541 1.5.10, an independent OPC UA stack, in both directions in the lab. It has not been run against a CODESYS, Siemens or Beckhoff publisher or across a router. PROTOCOLS.md has the setup and the limits. + +### Images, and a second set of screens + +- **amd64 only, this once.** The 4.19.4 images are built for amd64. The version tag lists amd64 alone, and `stable` and `latest` carry the new amd64 image beside the arm64 and arm/v7 images they already had, which are the previous release's. An arm box that follows `stable` keeps pulling and keeps running what it ran. The next release builds every architecture again and is a version bump with nothing else in it. +- **The new screens are at `/next`.** A rebuilt web UI is in every image behind the old one, which is still what you get at `/`. Try it at `/next` on any install; nothing about the default changes. + +### Fixes + +- **A process no longer stops the next run of a script with its own stop coming back to it.** Stopping a script is announced to every process, the one that made the stop included, and the announcement heard again a moment later cancelled whichever run of that script was going then: under load a waited run was cancelled 8 ms after it began, and another never finished. A process ignores the stop it announced itself, and a stop another process announced still stops it. +- **A discovery listener is up when the broker has answered its subscriptions, and only then.** A wait that ran out counted as an answer, and so did an answer on a connection that had since gone; neither does, and an answer that comes late still counts. +- **A discovery listener says it is up when it can hear.** The MQTT session counted itself connected the moment the broker accepted it, and only then took its subscriptions out and waited for the broker to grant them. A listener that reported up on that sent the next message into the gap, and a device's status or config that is not retained was lost. It reports up when the subscriptions are granted now, on the first connection and on every reconnect. +- **A pin on the board no longer wipes the board.** The page only read the board when the board screen opened, so a reload on the tags screen held an empty list and the first pin replaced the saved board with that one tag. The page reads the board at start now, a pin refuses until it has, and a save carries the board's `rev`, so the server answers `409` to a save over a board that changed since it was read. The `rev` is optional in the API: leave it out and the save is not checked. +- **A stalled OPC UA subscription is noticed.** The library keeps its keep alives to itself, so a stopped publish loop looked like a quiet plant and served its last values as good. After ten publishing intervals of quiet (thirty seconds at least) the driver reads the nodes once, compares them with what it holds, and falls back to reads if any differ. +- **A PLC that goes silent with its socket open is down, not degraded.** Modbus, S7 and EtherNet/IP kept their last values until TCP gave up, because the poll returned before it judged the session. They judge now. No S7 or CIP server is on the wire in the lab yet, and the roadmap says so. +- **A credential typed into a URL or free text is encrypted at rest.** Every screen already hid `mqtt://user:pass@host` and `?token=` from people, but the database and the backups held them as typed. A string that carries one, by the same test redaction hides it by, is sealed whole and comes back exactly as written, so the driver still gets the URL it was given. A key turned on later seals what was stored before, and a restore asks for the key when only an address needs it. +- **Every time the API writes is UTC.** A plant set to New York spelled `-04:00` where one on UTC spelled `Z`, across about thirty fields: `created_at`, `updated_at`, an alarm's `raisedAt`, an event's time, a device's `lastPoll`, a tag's timestamp, a logbook entry, a report's next run. They read the same on both now. If you parse these in a script that expected your local offset, it now gets `Z`. +- **Monthly partitions were skipped on the 31st.** The history and logbook tables made their next partition with a month added to today, and on the 31st that skipped a month, so a month's rows sat in the default partition. They start from the first of the month now. +- **The UniFi event log is ordered by when the event happened,** not by when the frame got here, so a replay after a reconnect stops sitting on top. +- **A switch's faceplate fits a phone.** Eight ports were wider than a 390 pixel screen and the page scrolled sideways by 113 pixels. The ports are tight tiles now, with the PoE watts in the port's detail, and a faceplate that still outgrows its card scrolls inside it. +- **A phone with no session and no connection gets the sign-in screen and the reason** instead of an empty app, and reloads when HotLoop answers. +- **A rollback is one command.** The chart index merges into the live one instead of being rebuilt from the release's own charts, so an older version still resolves. + +### Upgrading from 4.18.11 + +- Read the first two sections above before you do anything on a running plant. +- Migration 0026 is the only one. It is additive and every existing row keeps working. +- A bare binary or Quadlet install gets an MCP token at its first start. `hotloop mcp-token` prints it. +- `helm upgrade` as usual. Quadlet units follow `:stable` and need nothing. `helm rollback` to 4.18.11 is fine for this release: the database is already PostgreSQL 17 and nothing here changes it. diff --git a/src/content/docs/releases/hotloop-4-19-5.md b/src/content/docs/releases/hotloop-4-19-5.md new file mode 100644 index 0000000..f7307d2 --- /dev/null +++ b/src/content/docs/releases/hotloop-4-19-5.md @@ -0,0 +1,17 @@ +--- +# generated by scripts/sync-docs.mjs +title: "HotLoop core 4.19.5 release notes" +description: "What shipped in HotLoop core 4.19.5, what changed on upgrade, and what to read before you touch a running plant." +editUrl: false +sidebar: + label: "4.19.5" +--- + +This is the multi-arch build of [4.19.4](/releases/hotloop-4-19-4/) and nothing else. The code is the same commit's code with the version bumped: no fix, no feature, no change in behavior. + +4.19.4 was built for amd64 alone, so that it could reach production and read stable before the arm images were built. 4.19.5 builds the full set: the Gateway for amd64 and arm64, and Edge, IoT and the Edge Relay for amd64, arm64 and arm/v7. A box on arm moves from the images it was running to 4.19.4's code by pulling `stable`. + +Read [4.19.4's notes](/releases/hotloop-4-19-4/) for everything that changed since the last published release. + +- All four charts (`hotloop`, `hotloop-iot`, `hotloop-edge`, `hotloop-edge-relay`) are at 4.19.5, with every earlier version still in the index. +- `release.yml` builds every platform again (`BUILD_ONLY` is empty). The next release that carries changes sets it back to `linux/amd64`. diff --git a/src/content/docs/releases/hotloop-4-19-8.md b/src/content/docs/releases/hotloop-4-19-8.md new file mode 100644 index 0000000..d9b2d3f --- /dev/null +++ b/src/content/docs/releases/hotloop-4-19-8.md @@ -0,0 +1,38 @@ +--- +# generated by scripts/sync-docs.mjs +title: "HotLoop core 4.19.8 release notes" +description: "What shipped in HotLoop core 4.19.8, what changed on upgrade, and what to read before you touch a running plant." +editUrl: false +sidebar: + label: "4.19.8" +--- + +4.19.7 was tagged and never published: its release run failed in the Test job on one browser test, `TestBrowserPlantAndLogbookWalk/logbook_filters_and_pages`, which clicked the Logbook's Older entries button and saw nothing come (below), so no image, chart or release was made. 4.19.6 had been tagged and never published either, on a different test. 4.19.8 carries everything 4.19.6's and 4.19.7's notes list, plus the fix below. Read [4.19.4's notes](/releases/hotloop-4-19-4/) for what changed before 4.19.5, the multi-arch build of 4.19.4. + +This build is amd64 only, and the next release is one small version bump that builds the arm images, the way the two runs go. `stable` keeps the arm images it holds in the meantime. + +## Also in 4.19.8 + +- **The Logbook's Older entries button no longer loses its page to a live refresh.** The Logbook fetches the newest page again whenever the live feed says it has something new. A log line landing while an older page was on its way sent that refresh in, and the screen threw the older page away for belonging to an earlier look at the list: the button did nothing, and the click was lost. Any plant writing a log line in that moment (the window is the older request's own time) hit it, in the old UI and the new one. The refresh now waits for the older page to land, and runs afterwards only if nothing older came. A browser test holds the older request at the network, lets a log line land and the screen refresh, then lets the page through, and wants it joined to the list; it fails on 4.19.7's screens and passes on these. + +- **Every place that picks a tag searches the whole plant, and none is cut at a number.** On a tester with 2232 tags on three devices (4.19.5), the Tags screen showed 200 of them, all from the first device, the Trends picker had 201 options, a device's Details drawer said "No tags on this device" beside a card that said 1105 tags, and searching for a tag past the first 200 said "No tags match" while `/api/tags?q=` returned it. 4.19.6's tag list changes (limit up to 5000 refused beyond, offset, `X-Total-Count`, the pages reading every page, windows with a count) fix those, and they are in this release. What 4.19.8 adds: the dashboard card editor's tag fields (Gauge, Value, Trend, Map, and the Button's writable tags) were suggestions cut at the first 3000 tags, so on a plant past 3000 a tag was never offered; they are a search the server does as the text is typed (up to 100 matches, `GET /api/tags?q=&limit=100`, and a new `writable=true` filter for the write pickers). A browser test with a device of 3200 tags finds a tag past 200 from the Tags search box, the device drawer, the Pin a tag picker, the Trends picker and the card editor, in the old UI and the new one. + +## Also in 4.19.7 (carried) + +- **Every HotLoop chart ships with hardware access on.** A `hardware:` block, three switches that default on: the app container runs privileged and as root, the node's `/dev` is mounted at `/dev` (USB radios and serial adapters, including ones plugged in later), and the node's `/run/dbus` is mounted (Bluetooth through the node's BlueZ). The pod stays on the pod network. A plain install, from the App Store or from helm, sees what is plugged into the node with no hand edit. HotLoop has no Bluetooth, Z-Wave or USB serial code of its own yet (Zigbee comes through Zigbee2MQTT over MQTT); this is the access that code, or a sidecar, will need. It costs: the pod is root on the node (the NetworkPolicy default stays on). `hardware.bluetooth.hostNetwork` is the opt-in, off by default, for what needs the node's network (raw Bluetooth sockets, discovery on the node's own network): on, the page answers on every address of the node at `service.targetPort`, one HotLoop per node per port, and a NetworkPolicy does not apply to the pod. Turn any part off with the three switches; with all three off the render is, line for line, the pod 4.19.6 rendered. See [Hardware in DEPLOYMENT.md](/install/helm/#hardware-usb-radios-serial-adapters-and-bluetooth). The Edge Relay chart has the same block. +- **The UniFi map and the Entities strip no longer write the word "null".** A script that handed a missing value to `append` or `replaceChildren` wrote it as text: a "null" above the UniFi map, and one in the Entities health strip when no node is selected. Fixed, and a browser test now fails on any text node that is only null, undefined, NaN or [object Object], on every screen. +- **The map test waits for the page to settle, and prints what the page saw when it fails.** Release run 42093843129 lost one click: after three pinches the UniFi map test clicked Fit and read the map still zoomed in. The cause is unknown. It never happened again in 8 contention runs beside the repo's other tests (480 attempts), under CPU throttling up to 16 times, or with each click preceded by a scroll. The test now waits until the page has seen every finger go up and drawn three frames before it clicks, reads the map until it is what the click should have left (for up to five seconds) instead of once, keeps the same expected values, and keeps a log in the page of the pointer and touch events, clicks and view changes, printed only when a step fails, so a miss on a runner carries the order of what the page saw. + +## What changed + +- **The Tags screen and every device's drawer reach every tag of every device.** The page asked the server for 2000 tags and the server turned any limit over 1000 into 200, without saying so. On a plant of three devices and 2232 tags the Tags screen held 200, all from the first device, and the other devices' drawers read "TAGS 0" and "No tags on this device". `GET /api/tags` now takes `limit` (1 to 5000; anything else, 5001 included, is a `400`, never an answer with fewer) and `offset`, answers the total in the `X-Total-Count` header, and takes `device` and `q` together, so a client pages to the end and knows it did. The page reads every page, draws a window of the list (500 on Tags, 200 in a drawer and in the pin picker) and says under it how many there are, with a button for the rest. The same silent ceiling was in the history lists (automation runs, alarm events, write audit, script runs, recipe applications, report runs): a limit over their ceiling was quietly the default, a tenth of what was asked. It is the ceiling now. The MCP `list_tags` tool had it too, and takes the limit it is given. +- **The live feed sends what changed, compressed, per page.** A driver reads every tag on every poll and the feed sent them all, changed or not, four times a second: about 150 KB each time on a tester's 2232 tags. A page on an 85 KB/s link fell minutes behind, because the messages queued ahead of the current ones. The `values` event now carries only the readings that differ from the last one sent in value, text, unit, quality or reason, and sends an unchanged one again after 30 seconds so a steady value's age stays honest. The stream is gzip when the browser accepts it: a tester's first picture is about 326 KB of JSON and about 11 KB on the wire. Each page's readings are held per tag, newest only, until its own writer can send them, and the writer rests as long as its last send took (at most five seconds), so a page on a slow link stays a send or two behind the plant, loses nothing, and takes nothing from other pages. On a link read at 300 KB/s, with every tag changing every 250 ms, a page was 27.8 seconds behind in 4.19.5 and 3.7 seconds behind in 4.19.6. +- **The poll counts on the device cards and the Board's numbers tick.** They were the numbers the page loaded with. They do not ride the live feed, and the refresh behind them (every 30 seconds) did not redraw anything. They are asked for every two seconds while the page is in view, one request at a time, and the screen patches its numbers in place, so a button under your hand is not replaced by it. The device drawer reads its counters from the same list. + +## Reading the feed + +`GET /api/stream` sends `Content-Encoding: gzip` when the request says `Accept-Encoding: gzip`, and a script that does not say it gets plain text as before. The `values` event is still an array of readings in the `GET /api/values` shape; it just no longer repeats the ones that did not change, so a client that rebuilds its own picture from the snapshot and the events needs nothing different. + +## Known + +- **A Kafka or Redpanda history target whose broker refuses every write for lack of disk reports "records have timed out before they were able to be produced", and not why.** The readings are not lost: the target keeps the batch, retries with a growing wait (one second up to thirty), owes the span and catches up from HotLoop's own history when the broker answers, and its status and its system event say it is down. Redpanda refuses writes when the disk under it has less than `storage_min_free_bytes` free (5 GiB by default), and says so only in its own log ("rejecting produce request: no disk space"). The client library does not hand the refusal's code to HotLoop in that case (it rewinds, bumps its producer epoch and retries), so there was no honest way to name the reason in this release; the design is in bead bc49eb. If a Redpanda target is down with that message, look at the broker's log and its disk. diff --git a/src/content/docs/releases/index.md b/src/content/docs/releases/index.md index 1d52f3f..7c243f3 100644 --- a/src/content/docs/releases/index.md +++ b/src/content/docs/releases/index.md @@ -5,13 +5,15 @@ description: "What shipped in every HotLoop core, Flow, Cascade and Transmitter 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.8.1, and the latest Cascade release is 1.0.2. +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.19.8, the latest Flow release is 2.8.1, and the latest Cascade release is 1.0.2. ## HotLoop core IoT, Edge, Gateway and Edge Relay release together from one tag, so they share these notes. -- [HotLoop core 4.19.0 release notes](/releases/hotloop-4-19-0/) (on main, next release) +- [HotLoop core 4.19.8 release notes](/releases/hotloop-4-19-8/) +- [HotLoop core 4.19.5 release notes](/releases/hotloop-4-19-5/) +- [HotLoop core 4.19.4 release notes](/releases/hotloop-4-19-4/) - [HotLoop core 4.18.11 release notes](/releases/hotloop-4-18-11/) - [HotLoop core 4.18.10 release notes](/releases/hotloop-4-18-10/) - [HotLoop core 4.17.0 release notes](/releases/hotloop-4-17-0/) diff --git a/src/content/docs/roadmap.md b/src/content/docs/roadmap.md index 630d4aa..ad0d4af 100644 --- a/src/content/docs/roadmap.md +++ b/src/content/docs/roadmap.md @@ -5,10 +5,6 @@ description: "What HotLoop is, what is left to build and what is broken, in the editUrl: false --- -:::note[Written against main] -This page follows `main` at 4d5da2f, and it differs from the copy in HotLoop core 4.18.11, the latest release. Parts of it may be newer than what you have installed. The [release notes](/releases/) say what moved. -::: - What HotLoop is, what's left to build, and what's broken. If it isn't on this list it isn't getting done, so put it on the list. @@ -432,7 +428,8 @@ back up and patch, and they are not adopting ours to get a trend. - [ ] Against a CODESYS publisher - [ ] Message security (signed and encrypted NetworkMessages) - [ ] Delta frames from the publisher; DataSetMetaData and discovery messages - so a subscriber can name the fields; array and structure fields; events + so a subscriber can name the fields; array and structure fields in what + the publisher sends, and structure fields in what the subscriber reads; events - [ ] Against a Siemens or Beckhoff publisher, and over a router (multicast routing and the TTL) diff --git a/src/content/docs/transmitter/proof.md b/src/content/docs/transmitter/proof.md index a7ba513..68ed6b4 100644 --- a/src/content/docs/transmitter/proof.md +++ b/src/content/docs/transmitter/proof.md @@ -26,7 +26,7 @@ The example application has two publishers, both to a capture listener on a Podm |---|---|---|---| | `test/verify` | a decoder written from OPC 10000-14 and 10000-6 alone, which also reads the same variables from the runtime's OPC UA server and compares them | every field equal to the PLC's, no gap in the NetworkMessage and DataSetMessage sequence numbers over the capture | the same | | open62541 1.4.9 | an independent PubSub implementation, built from its release tarball (sha256 `8735e61391cde0d84e6347a4293d2400b6a72838fb44513caf22d70dfca3a0cf`), run as a subscriber with a DataSet metadata of the 17 fields | all 15 values checked equal, including the string, both arrays and the TIME | the 11 numeric scalars equal. Its RawData reader reads one scalar per field and bounds its buffer by the fields' in-memory sizes (`DataSetReader_processRaw`, `src/pubsub/ua_pubsub_reader.c`), so it cannot read a RawData string longer than a few characters or any RawData array | -| HotLoop core's UADP subscriber | the driver in HotLoop at commit 343b7636, on HotLoop core's main branch, which is not in a core release yet, `internal/driver/uadp.go`, run by `test/core/run.sh` against a live runtime and told the RawData layout in its DataSet types | 21 values checked equal, including the string, the TIME and elements 0, 1, 124 and 249 of the 250 REAL array and all four of the INT array, and both counters advancing between two reads 3 seconds apart | the same 21 values and the same two counters. 62 key frames received, none refused | +| HotLoop core's UADP subscriber | the driver in HotLoop at commit 343b7636, which is in HotLoop core 4.19.4 and every release after it (4.19.4 was published after the check, and the commit is in its history), `internal/driver/uadp.go`, run by `test/core/run.sh` against a live runtime and told the RawData layout in its DataSet types | 21 values checked equal, including the string, the TIME and elements 0, 1, 124 and 249 of the 250 REAL array and all four of the INT array, and both counters advancing between two reads 3 seconds apart | the same 21 values and the same two counters. 62 key frames received, none refused | | A second Go decoder | the UADP decoder in our own receiver code, at commit f640c324 of that repository, run on the 222 captured datagrams with the layout of the 17 fields | all 111 messages decoded, every checked value correct | all 111 messages decoded, every checked value correct, the arrays with their Int32 length and padding | Wireshark 4.4.7's `tshark` (Alpine package) was tried on a capture and dissects UDP port 4840 as plain data: its OPC UA dissector is for the TCP protocol, so it was not usable as a UADP decoder. diff --git a/src/data/sync.json b/src/data/sync.json index 2a2bafb..c1d5c26 100644 --- a/src/data/sync.json +++ b/src/data/sync.json @@ -2,14 +2,14 @@ "repos": { "hotloop": { "slug": "HotLoop-io/hotloop", - "ref": "4d5da2f", - "sha": "4d5da2f216e276bba9587f9aa5e3343bad13258f", + "ref": "origin/main", + "sha": "27882ead1244ee52e1390c10c46fa00f79ebc3bf", "release": { - "tag": "v4.18.11", - "version": "4.18.11", + "tag": "v4.19.8", + "version": "4.19.8", "prerelease": false, - "publishedAt": "2026-10-30T03:26:10Z", - "sha": "b4386b08212ec8246dae254c620eba0c6aee08df" + "publishedAt": "2026-11-08T12:54:20Z", + "sha": "27882ead1244ee52e1390c10c46fa00f79ebc3bf" } }, "flow": { @@ -110,7 +110,7 @@ "group": "The base", "title": "Helpers", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/HELPERS.md", "release": false @@ -165,7 +165,7 @@ "group": "Automation", "title": "Scripts", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/SCRIPTS.md", "release": false @@ -176,7 +176,7 @@ "group": "Automation", "title": "Recipes", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/RECIPES.md", "release": false @@ -187,7 +187,7 @@ "group": "Screens and alerts", "title": "Dashboards", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/DASHBOARDS.md", "release": false @@ -209,7 +209,7 @@ "group": "Screens and alerts", "title": "MCP", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/MCP.md", "release": false @@ -220,7 +220,7 @@ "group": "Devices and data", "title": "Protocols and drivers", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/PROTOCOLS.md", "release": false @@ -242,7 +242,7 @@ "group": "Devices and data", "title": "MQTT discovery", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/DISCOVERY.md", "release": false @@ -264,7 +264,7 @@ "group": "Devices and data", "title": "UniFi Network", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/UNIFI.md", "release": false @@ -275,7 +275,7 @@ "group": "Devices and data", "title": "Integrations", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/INTEGRATIONS.md", "release": false @@ -286,7 +286,7 @@ "group": "Devices and data", "title": "Historian targets", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/HISTORIAN-TARGETS.md", "release": false @@ -297,7 +297,7 @@ "group": "Operate", "title": "Authentication and users", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/AUTH.md", "release": false @@ -308,7 +308,7 @@ "group": "Operate", "title": "Backups and restore", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/BACKUP.md", "release": false @@ -330,7 +330,7 @@ "group": "Operate", "title": "The HTTP API", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/API.md", "release": false @@ -374,7 +374,7 @@ "group": "What the Gateway adds", "title": "Scheduled reports", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/REPORTS.md", "release": false @@ -396,7 +396,7 @@ "group": "Install", "title": "k3s and Helm", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/DEPLOYMENT.md", "release": false @@ -407,7 +407,7 @@ "group": "Install", "title": "Podman Quadlet", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "deploy/quadlet/README.md", "release": false @@ -418,7 +418,7 @@ "group": "Install", "title": "The :stable auto-update", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "deploy/quadlet/README.md", "release": false @@ -429,7 +429,7 @@ "group": "Lineup", "title": "HotLoop products: seven on four release lines", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/PRODUCTS.md", "release": false @@ -440,7 +440,7 @@ "group": "Lineup", "title": "HotLoop roadmap", "label": null, - "status": "changed", + "status": "released", "repo": "hotloop", "source": "docs/ROADMAP.md", "release": false @@ -941,14 +941,36 @@ "release": true }, { - "slug": "releases/hotloop-4-19-0", + "slug": "releases/hotloop-4-19-4", "product": "releases", "group": "HotLoop core", - "title": "HotLoop core 4.19.0 release notes", - "label": "4.19.0", - "status": "next", + "title": "HotLoop core 4.19.4 release notes", + "label": "4.19.4", + "status": "released", + "repo": "hotloop", + "source": "docs/releases/4.19.4.md", + "release": true + }, + { + "slug": "releases/hotloop-4-19-5", + "product": "releases", + "group": "HotLoop core", + "title": "HotLoop core 4.19.5 release notes", + "label": "4.19.5", + "status": "released", + "repo": "hotloop", + "source": "docs/releases/4.19.5.md", + "release": true + }, + { + "slug": "releases/hotloop-4-19-8", + "product": "releases", + "group": "HotLoop core", + "title": "HotLoop core 4.19.8 release notes", + "label": "4.19.8", + "status": "released", "repo": "hotloop", - "source": "docs/releases/4.19.0.md", + "source": "docs/releases/4.19.8.md", "release": true }, { diff --git a/src/pinned/edge-relay/install.md b/src/pinned/edge-relay/install.md index f810949..981d791 100644 --- a/src/pinned/edge-relay/install.md +++ b/src/pinned/edge-relay/install.md @@ -1,5 +1,5 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "Install the Edge Relay with Helm or Quadlet" description: "Run HotLoop Edge Relay {{hotloop}} on a cluster with Helm, or on a Pi or industrial PC as a systemd service with Podman Quadlet. The unit files are here in full." sidebar: diff --git a/src/pinned/edge-relay/overview.md b/src/pinned/edge-relay/overview.md index b5b9b3d..cb6dc91 100644 --- a/src/pinned/edge-relay/overview.md +++ b/src/pinned/edge-relay/overview.md @@ -1,5 +1,5 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "HotLoop Edge Relay overview" description: "A headless Sparkplug B edge node. It polls with the Gateway's drivers, forwards upstream, and buffers to disk when the link drops. No database, no UI." sidebar: @@ -8,7 +8,7 @@ sidebar: The Edge Relay is the headless one. It sits on a Pi or a small industrial PC next to the machines, polls them with the same drivers the Gateway uses, and forwards every reading over Sparkplug B to a Gateway somewhere with a real database. When the uplink dies it queues to disk and replays when the link comes back, so the historian upstream ends up with the outage backfilled, not a hole in it. No database, no web UI, no users, no write path, and every one of those missing pieces is something that can't break at 3 AM. -The current release is {{hotloop}}: `ghcr.io/hotloop-io/hotloop-edge-relay:{{hotloop}}` for amd64, arm64, and arm/v7 (a Pi 3 or a 32-bit Pi OS install is exactly the box a relay ends up on), about 15 MB, pulled with no login, and the `hotloop-edge-relay` chart from `https://hotloop.io/hotloop`. 4.16.0 was the first release that published it, and arm/v7 came with 4.18.10. It shares a version number with the Gateway, and a release refuses to go out unless both charts carry the tag's version and the Quadlet unit follows `:stable`. So the relay gets a new version every release whether its code changed or not. One version number to ask about, and a relay chart can never point at an image nobody pushed. [Install it](/edge-relay/install/). +The current release is {{hotloop}}: `ghcr.io/hotloop-io/hotloop-edge-relay:{{hotloop}}` built for amd64 alone this time (a Pi 3 or a 32-bit Pi OS install is exactly the box a relay ends up on, and its arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, until the next release builds them all again), about 15 MB, pulled with no login, and the `hotloop-edge-relay` chart from `https://hotloop.io/hotloop`. 4.16.0 was the first release that published it, and arm/v7 came with 4.18.10. It shares a version number with the Gateway, and a release refuses to go out unless both charts carry the tag's version and the Quadlet unit follows `:stable`. So the relay gets a new version every release whether its code changed or not. One version number to ask about, and a relay chart can never point at an image nobody pushed. [Install it](/edge-relay/install/). :::note[New in 4.18.10] **A pushed device file applies the moment it lands.** New devices connect, removed ones let go, changed ones reconnect, and the ones nobody touched keep their sessions. The Sparkplug session stays up, with no NDEATH and the same `bdSeq`. A bad file is refused and the old one keeps running, and `/status` says which file is running and which was refused. On 4.17.0 a push only staged the file until something restarted the relay. diff --git a/src/pinned/edge/install.md b/src/pinned/edge/install.md index 1eb0173..e0412c5 100644 --- a/src/pinned/edge/install.md +++ b/src/pinned/edge/install.md @@ -1,12 +1,12 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "Install HotLoop Edge {{hotloop}}" description: "Install HotLoop Edge {{hotloop}} on k3s with Helm or on an industrial PC with Podman Quadlet, then turn on the OPC UA server and Sparkplug publishing." sidebar: label: "Install" --- -HotLoop Edge first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-edge:{{hotloop}}`, built for amd64, arm64, and arm/v7, and the chart is `hotloop-edge` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. +HotLoop Edge first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-edge:{{hotloop}}`, built for amd64 alone this release (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), and the chart is `hotloop-edge` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. A cluster gets Helm. An industrial PC or a Pi with no cluster gets Podman Quadlet. The database is a choice you make once: TimescaleDB (the default), plain PostgreSQL or a SQLite file with no database container at all. diff --git a/src/pinned/edge/overview.md b/src/pinned/edge/overview.md index fc1df0b..b0ade3d 100644 --- a/src/pinned/edge/overview.md +++ b/src/pinned/edge/overview.md @@ -1,5 +1,5 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "HotLoop Edge overview" description: "HotLoop Edge is IoT plus the machine layer: an OPC UA server, Sparkplug B publishing, and store-and-forward that keeps the plant running with the uplink down." sidebar: @@ -7,7 +7,7 @@ sidebar: --- :::note[Edge is out, first released in 4.18.10] -The image is `ghcr.io/hotloop-io/hotloop-edge:{{hotloop}}` for amd64, arm64, and arm/v7, about 37 MB on amd64 and 34 MB on arm, and the chart is `hotloop-edge` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `edge/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/edge/install/). Before 4.18.10 the Gateway could run at the edge and publish Sparkplug upstream, and it has since 4.5.0, but it was the whole Gateway to do it. +The image is `ghcr.io/hotloop-io/hotloop-edge:{{hotloop}}` built for amd64 alone this time (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), about 37 MB on amd64 and 34 MB on arm, and the chart is `hotloop-edge` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `edge/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/edge/install/). Before 4.18.10 the Gateway could run at the edge and publish Sparkplug upstream, and it has since 4.5.0, but it was the whole Gateway to do it. ::: Edge is a box bolted next to a line. It reads the machine, runs the rules on the machine, keeps working when the WAN goes down, and tells the rest of the plant what it saw. Think Ignition Edge, built in Go, with the same write gate as everything else. diff --git a/src/pinned/gateway/install.md b/src/pinned/gateway/install.md index 47576b6..d9f327b 100644 --- a/src/pinned/gateway/install.md +++ b/src/pinned/gateway/install.md @@ -1,12 +1,12 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "Install HotLoop Gateway {{hotloop}} with Helm" description: "Install HotLoop Gateway {{hotloop}} from the published Helm chart on k3s or any Kubernetes, sign in, and set what matters before a real plant sees it." sidebar: label: "Install" --- -HotLoop Gateway {{hotloop}} ships as a container image for amd64 and arm64, about 37 MB on amd64 and 35 MB on arm64, and a Helm chart. Both pull with no login. It keeps the names it shipped under through 4.17.0, image `ghcr.io/hotloop-io/hotloop` and chart `hotloop`, so nobody running it has to change a thing. Already running an older release? Don't install over it. [Upgrading](/gateway/upgrading/) has the path from 4.17.0, which is a plain `helm upgrade`, and the paths from 4.16.0 and 4.3.1, which go in hops. +HotLoop Gateway {{hotloop}} ships as a container image for amd64 (the arm64 image on `:stable` is 4.19.5's, the last build of every architecture, and the next release builds both again), about 37 MB on amd64 and 35 MB on arm64, and a Helm chart. Both pull with no login. It keeps the names it shipped under through 4.17.0, image `ghcr.io/hotloop-io/hotloop` and chart `hotloop`, so nobody running it has to change a thing. Already running an older release? Don't install over it. [Upgrading](/gateway/upgrading/) has the path from 4.17.0, which is a plain `helm upgrade`, and the paths from 4.16.0 and 4.3.1, which go in hops. Two ways to run it for real: the Helm chart on any Kubernetes, k3s included, and Podman Quadlet on a box that isn't in a cluster, below. There's no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. @@ -57,7 +57,7 @@ Put these in a values file and install with `-f`. Don't lean on `--set` for anyt | `global.timezone` | `America/New_York` | Schedules, reports and shift changes run on it. It has to be a real zone name (`America/Chicago`, not `CST`), because a timezone that won't load stops startup instead of quietly running on UTC. | | `publicUrl` | empty | The address a phone on the plant Wi-Fi reaches HotLoop at. Without it, notifications have no link back and ntfy gets no Acknowledge button. | | `safety.allowWrites` | `false` | The master switch. Off, nothing writes to a machine, from anywhere. Turn it on once the deployment has been reviewed. | -| `safety.allowMcpWrites` | `false` | Lets agents write through the same gate. It narrows the master switch and never goes around it. Set it without `safety.allowWrites` and the Gateway refuses to start. | +| `safety.allowMcpWrites` | `true` | Lets agents write through the same gate, on since 4.19.4. It follows the master switch: with `safety.allowWrites` off, nothing writes, agents included. Set it `false` and agents can still read the whole plant and never write. The charts always set an MCP token, so `/mcp` is never open. | | `database.postgresql.persistence.size` | `20Gi` | Your history lives here. Size it for your tag count and `historian.retentionDays` (90). | | `logbook.retentionDays` | `90` | Days of entity state changes the [logbook](/iot/logbook/) keeps. `0` keeps everything until the volume fills, and the Gateway warns you about it at start. | | `secretKey.existingSecret` | empty | A Secret you manage holding the device password key. Leave it empty and the chart makes one. Set it if you render the chart with `helm template`. | diff --git a/src/pinned/gateway/overview.md b/src/pinned/gateway/overview.md index 7083aa1..6c5ab24 100644 --- a/src/pinned/gateway/overview.md +++ b/src/pinned/gateway/overview.md @@ -1,5 +1,5 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "HotLoop Gateway overview" description: "An industrial gateway: ten native protocols, a historian that keeps quality, ISA-18.2 alarms, MCP for agents, and one write gate nothing gets around." sidebar: @@ -7,7 +7,7 @@ sidebar: --- :::note[{{hotloop}} is out] -The release where HotLoop stops being one image. [IoT](/iot/overview/), [Edge](/edge/overview/), the Gateway and the [Edge Relay](/edge-relay/overview/) all ship from this tag, each with its own image and chart. The Gateway keeps `ghcr.io/hotloop-io/hotloop:{{hotloop}}` and the chart `hotloop` from `https://hotloop.io/hotloop`, and both pull with no login. The source repo is still private. [Install it](/gateway/install/). On 4.17.0? It's a plain `helm upgrade`, but not with `--reuse-values`, and a couple of defaults moved. On 4.16.0 or 4.3.1? You go through 4.17.0 first. Either way, [read the upgrade first](/gateway/upgrading/). You want to find this out here, not from a stuck rollout. +Four products from one tag: [IoT](/iot/overview/), [Edge](/edge/overview/), the Gateway and the [Edge Relay](/edge-relay/overview/), each with its own image and chart. This build is amd64 only, and the arm images on `:stable` are 4.19.5's until the next release builds every architecture. The Gateway keeps `ghcr.io/hotloop-io/hotloop:{{hotloop}}` and the chart `hotloop` from `https://hotloop.io/hotloop`, and both pull with no login. The source repo is still private. [Install it](/gateway/install/). On 4.18.11? It's a `helm upgrade`, and agents can write after it, so read the two things that change. On 4.17.0? It's a plain `helm upgrade`, but not with `--reuse-values`, and a couple of defaults moved. On 4.16.0 or 4.3.1? You go through 4.17.0 first. Either way, [read the upgrade first](/gateway/upgrading/). You want to find this out here, not from a stuck rollout. ::: HotLoop Gateway reads your plant. It polls PLCs and servers in their own protocols, keeps every reading and its quality in a historian, raises ISA-18.2 alarms that can page a phone, runs automations against all of it, and hands the whole plant to agents over MCP. A site runs one chart, the Gateway and its database, instead of a historian, an alarm server, a rules engine, and a pile of glue, each one breaking on its own damn schedule. @@ -27,6 +27,8 @@ The ground floor, in {{hotloop}} and in every release back to 4.16.0: - [Automations](/iot/automations/) covers triggers, conditions, and actions, and why a rule is compiled when you save it instead of when it fires at 3 AM. - [Model Context Protocol](/iot/mcp/) is the Gateway as an MCP server and client, and how one Gateway reads every site. +New in 4.19.4, and not in 4.18.11: agents write through the same gate as the screen (MCP is on in every product, and the endpoint always has a token), a value written to a scaled tag lands in the register the way you meant it, a CODESYS runtime is read by the names in its Symbol Configuration, a Kafka event stream joins the historian targets, OPC UA PubSub over UADP subscribes and publishes, and `hostNetwork` is a chart setting. 4.19.5 is its multi-arch build, and 4.19.8 adds a Logbook fix and tag pickers that search the whole plant. [The release notes](/releases/hotloop-4-19-4/) say what each one buys you, and what it costs. + New in 4.18.11, and not in 4.18.10: the database is PostgreSQL 17 with an upgrade that runs itself, SNMP and BACnet/IP through one integrations flow, kiosk sign-in with a lockout, alarm escalation, and a UI that installs on a phone. [The release notes](/releases/hotloop-4-18-11/) say what each one buys you, and what it costs. New in 4.18.10, and not in 4.17.0: the four-product lineup, a choice of database, MQTT, and network discovery, historian targets, and boxes that update themselves. [The release notes](/releases/hotloop-4-18-10/) say what each one buys you. @@ -67,6 +69,9 @@ All four are installable today, all {{hotloop}}. The relay is its own image and Each release has its own notes, and the notes are where the detail lives, so it is in one place and not two. +- [4.19.8](/releases/hotloop-4-19-8/): the Logbook's Older entries button keeps its page, and every tag picker searches the whole plant. It carries 4.19.6's and 4.19.7's changes too, because neither was published. +- [4.19.5](/releases/hotloop-4-19-5/): the multi-arch build of 4.19.4, with nothing else in it. +- [4.19.4](/releases/hotloop-4-19-4/): agent writes and a token on every MCP endpoint, scaled writes that land right, CODESYS by symbol name, a Kafka event stream, OPC UA PubSub over UADP, and `hostNetwork`. There is no 4.19.0 to 4.19.3, and the notes say why. - [4.18.11](/releases/hotloop-4-18-11/): PostgreSQL 17 with an upgrade that runs itself (read this one before you upgrade a running plant), SNMP and BACnet/IP through one integrations flow, kiosk sign-in with a lockout, alarm escalation, and a UI that installs on a phone. - [4.18.10](/releases/hotloop-4-18-10/): the four-product lineup, a choice of database, MQTT and network discovery, historian targets, and boxes that update themselves. There is no 4.18.0 to 4.18.9, and the notes say why. - [4.17.0](/releases/hotloop-4-17-0/): recipes, scripts, blueprints, helpers, the logbook, and UniFi Network, read-only. diff --git a/src/pinned/gateway/upgrading.md b/src/pinned/gateway/upgrading.md index 401ab15..b75d3e8 100644 --- a/src/pinned/gateway/upgrading.md +++ b/src/pinned/gateway/upgrading.md @@ -1,17 +1,35 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "Upgrading HotLoop Gateway to {{hotloop}}" -description: "From 4.18.10 the database moves to PostgreSQL 17 by itself, which needs disk and a backup. From 4.17.0 it is a plain helm upgrade. Older, you go in hops." +description: "From 4.18.11 it is a helm upgrade, but agents can write now and scaled tags change. From 4.18.10 the database moves to PostgreSQL 17. Older, you go in hops." sidebar: label: "Upgrading" --- -Four ways onto {{hotloop}}, and they are nothing alike. From 4.18.10 the database engine changes under you, and you want to know that before it happens. From 4.17.0 it's a plain `helm upgrade`, with a couple of defaults that moved. From 4.16.0 you go through 4.17.0 first, and afterwards a new Secret is as much a part of your plant's data as the database is. From 4.3.1 it's a renamed chart that a plain `helm upgrade` can't touch, and you go through 4.16.0 and 4.17.0 on the way. The release notes cover one hop at a time, and none of them covers jumping over a release, so this page doesn't promise that one works. Find the one that's yours and read all of it before you touch a running plant. +Five ways onto {{hotloop}}, and they are nothing alike. From 4.18.11 it is a plain `helm upgrade`, but two things change what a running plant does, so read them first. From 4.18.10 the database engine changes under you, and you want to know that before it happens. From 4.17.0 it's a plain `helm upgrade`, with a couple of defaults that moved. From 4.16.0 you go through 4.17.0 first, and afterwards a new Secret is as much a part of your plant's data as the database is. From 4.3.1 it's a renamed chart that a plain `helm upgrade` can't touch, and you go through 4.16.0 and 4.17.0 on the way. The release notes cover one hop at a time, and none of them covers jumping over a release, so this page doesn't promise that one works. Find the one that's yours and read all of it before you touch a running plant. :::note[Older charts are fetched by URL] -The chart repository's index lists only the latest chart, {{hotloop}}, so `hotloop/hotloop --version 4.17.0` won't resolve. The older chart tarballs are still served, and the commands below for 4.16.0 and 4.17.0 name the chart by its URL instead, like `https://hotloop.io/hotloop/hotloop-4.17.0.tgz`. +The chart repository's index keeps the Gateway's charts back to 4.16.0, so `hotloop/hotloop --version 4.17.0` resolves, and the tarballs are served by URL too. The commands below for 4.16.0 and 4.17.0 name the chart by its URL, like `https://hotloop.io/hotloop/hotloop-4.17.0.tgz`, and that works either way. IoT and Edge start at 4.18.10. ::: +## Upgrading from 4.18.11 + +Why bother: agents over MCP can write now, a value you write to a scaled tag lands in the register the way you meant it, a CODESYS runtime is read by the names its programmer gave the variables, HotLoop can send readings and the system log to a Kafka broker, OPC UA PubSub over UADP works in both directions, and a store install signs you in as yourself. [The 4.19.4 release notes](/releases/hotloop-4-19-4/) have the whole list, [4.19.5](/releases/hotloop-4-19-5/) is the multi-arch build of it, and [4.19.8](/releases/hotloop-4-19-8/) is the one to install, with a Logbook fix and tag pickers that search the whole plant. 4.19.0 to 4.19.3, 4.19.6 and 4.19.7 were tagged and never published, so none of them exists to upgrade from. + +```bash +helm repo update +helm upgrade hotloop hotloop/hotloop --version {{hotloop}} \ + --namespace hotloop --reset-then-reuse-values +``` + +The same for `hotloop-edge`, `hotloop-iot` and `hotloop-edge-relay`. The database is already PostgreSQL 17 from 4.18.11, and nothing here changes it, so `helm rollback` to 4.18.11 is fine. + +- **Agents can write.** `allowMcpWrites` is on by default in the code and in all three charts. A deployment that has `allowWrites` on and never set `allowMcpWrites` gets agent writes at this upgrade, through the same gate as the screen. A deployment that saved `allowMcpWrites: false` stays off. If you want agents read-only, set it `false` before you upgrade. +- **The MCP endpoint always has a token.** A bare binary or a Quadlet unit that never set one makes its own at first start and keeps it in its database. `hotloop mcp-token` prints it, and an agent pointed at `/mcp` with no token stops working until you give it that one. +- **Scaled tags.** Write 55.5 to a tag scaled by 0.1 and 555 lands in the register, where 55 used to. If anyone typed raw counts to work around it, that now sends ten times too much. Look at every tag with a scale or an offset that somebody writes by hand, from an automation or from a recipe, and change those numbers to engineering values in the same step as the upgrade. +- **Times are `Z`.** Every time the API writes is UTC. A script that parsed your local offset now gets `Z`. +- **One migration,** `0026`, adds an `options` column to the historian targets, empty for every target that exists. + ## Upgrading from 4.18.10 Why bother: the database is PostgreSQL 17 now, and the box moves itself. You also get [integrations](/iot/integrations/) with SNMP and BACnet/IP, kiosk sign-in, alarm escalation, and a UI that installs on a phone. [The 4.18.11 release notes](/releases/hotloop-4-18-11/) have the list. What follows is the part that bites. diff --git a/src/pinned/iot/install.md b/src/pinned/iot/install.md index 4360e21..a9a29be 100644 --- a/src/pinned/iot/install.md +++ b/src/pinned/iot/install.md @@ -1,12 +1,12 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "Install HotLoop IoT {{hotloop}}" description: "Install HotLoop IoT {{hotloop}} on k3s with Helm, or on a Pi or small PC as systemd services with Podman Quadlet, on PostgreSQL, or a SQLite file." sidebar: label: "Install" --- -HotLoop IoT first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-iot:{{hotloop}}`, built for amd64, arm64, and arm/v7, and the chart is `hotloop-iot` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. +HotLoop IoT first shipped in 4.18.10. The image is `ghcr.io/hotloop-io/hotloop-iot:{{hotloop}}`, built for amd64 alone this release (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), and the chart is `hotloop-iot` in the same Helm repository as the rest of the lineup. Both pull with no login. There is no Docker or Compose path, on purpose, and a bare `podman run` from a shell that dies with the session isn't an install. Two ways in. A cluster gets Helm. A Pi, a NUC, or a small industrial PC with no cluster gets Podman Quadlet. Either way the database is a choice you make once: PostgreSQL 17 (the default), or a SQLite file with no database container at all. @@ -62,7 +62,7 @@ Put these in a values file and install with `-f`. | `global.timezone` | `America/New_York` | Schedules and shift changes run on it. It has to be a real zone name, because one that won't load stops startup. | | `publicUrl` | empty | The address a phone on the plant Wi-Fi reaches HotLoop at. Without it, notifications have no link back and ntfy gets no Acknowledge button. | | `safety.allowWrites` | `false` | The master switch. Off, nothing writes to a machine, from anywhere. | -| `safety.allowMcpWrites` | `false` | Lets agents write through the same gate. It narrows the master switch and never goes around it. | +| `safety.allowMcpWrites` | `true` | Lets agents write through the same gate, on since 4.19.4. It follows the master switch: with `safety.allowWrites` off, nothing writes, agents included. Set it `false` and agents can still read the whole plant and never write. | | `historian.retentionDays` | `30` | Days of history kept. The Gateway and Edge keep 90, because a Pi's disk is small. | | `logbook.retentionDays` | `90` | Days of entity state changes the [logbook](/iot/logbook/) keeps. | | `database.postgresql.persistence.size` | `10Gi` | Your history lives here. Size it for your tag count and the retention above. | diff --git a/src/pinned/iot/overview.md b/src/pinned/iot/overview.md index cbeb6f7..7b429ae 100644 --- a/src/pinned/iot/overview.md +++ b/src/pinned/iot/overview.md @@ -1,5 +1,5 @@ --- -reviewed: hotloop=4.18.11 +reviewed: hotloop=4.19.8 title: "HotLoop IoT overview" description: "HotLoop IoT is the automation base: entities, automations, dashboards, alarms, MQTT discovery, and every protocol driver, on SQLite or PostgreSQL, built in Go." sidebar: @@ -7,7 +7,7 @@ sidebar: --- :::note[IoT is out, first released in 4.18.10] -The image is `ghcr.io/hotloop-io/hotloop-iot:{{hotloop}}` for amd64, arm64, and arm/v7, about 31 MB on amd64 and 29 MB on a Pi, and the chart is `hotloop-iot` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `iot/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/iot/install/). The pages in this section say what each feature has been run against, and where a page follows `main` and not the release, it says that too. +The image is `ghcr.io/hotloop-io/hotloop-iot:{{hotloop}}` built for amd64 alone this time (the arm64 and arm/v7 images on `:stable` are 4.19.5's, the last build of every architecture, and the next release builds them all again), about 31 MB on amd64 and 29 MB on a Pi, and the chart is `hotloop-iot` from `https://hotloop.io/hotloop`. Both pull with no login, and the source repo is still private. The Quadlet units are the `iot/` directory of [Podman Quadlet](/install/quadlet/). [Install it](/iot/install/). The pages in this section say what each feature has been run against, and where a page follows `main` and not the release, it says that too. ::: IoT is the base Edge and the Gateway are built on. If the box in front of you is a Pi, a NUC in a closet, a rack of smart plugs and sensors, or one PLC on a bench, this is the one. It is the smallest image of the three full products, and it still carries everything that makes HotLoop useful: the entities, the rules, the alarms, and the one write gate that nothing gets around. @@ -20,8 +20,8 @@ A bigger product never takes anything away from it. [Edge](/edge/overview/) is I - **Automations, blueprints, scripts, and recipes.** Rules compiled when you save, and named sequences, and setpoint sets that go through the write gate. [Automations](/iot/automations/), [blueprints](/iot/blueprints/), [scripts](/iot/scripts/), [recipes](/iot/recipes/). - **Helpers, the logbook, and the sun.** The values your people own, one timeline of everything that happened, and sunrise worked out on the box. [Helpers](/iot/helpers/), [logbook](/iot/logbook/), [the sun](/iot/sun/). - **Dashboards, notifications, and MCP.** Screens built per role, alarms that page a phone, and an agent that can read the plant and touch only what you armed. [Dashboards](/iot/dashboards/), [notifications](/iot/notifications/), [MCP](/iot/mcp/). -- **Every protocol driver, MQTT with discovery, and an embedded broker.** [Protocols](/iot/protocols/), [the broker](/iot/broker/), [MQTT discovery](/iot/mqtt-discovery/), [network discovery](/iot/network-discovery/). -- **UniFi, read-only,** and **history written out to your own database.** [UniFi](/iot/unifi/), [historian targets](/iot/historian-targets/). +- **Every protocol driver (a CODESYS runtime by symbol name, and OPC UA PubSub over UADP included), MQTT with discovery, and an embedded broker.** [Protocols](/iot/protocols/), [the broker](/iot/broker/), [MQTT discovery](/iot/mqtt-discovery/), [network discovery](/iot/network-discovery/). +- **UniFi, read-only,** and **history written out to your own database, or to a Kafka broker.** [UniFi](/iot/unifi/), [historian targets](/iot/historian-targets/). - **Users, backups, and migrations that only add.** [Authentication](/iot/authentication/), [backups](/iot/backup/), [migrations](/iot/migrations/), and [the HTTP API](/iot/api/). [The write gate](/gateway/write-gate/) is the one page to read before a real plant sees any of this. It applies to every product.