hotloop
GitHub
HotLoop Gateway4.18.11 is out

Every reading is real. Every write is earned.

Gateway talks to your actual equipment over the protocols it already speaks, keeps every value with how much you should trust it, and alarms and automates on top of that. Agents can read the whole plant over MCP. When a sensor goes dark, you see a dark sensor, not a frozen number pretending everything's fine. And nothing moves a machine until someone with the authority says it can. You can believe the screen, and you can sleep.

4.18.11 is out: four products from one tag, MQTT discovery, historian targets, a database you pick, and an Edge that survives a dead WAN.

The image is ghcr.io/hotloop-io/hotloop:4.18.11, the chart is hotloop from https://hotloop.io/hotloop, and both pull with no login. The source repo is still private. On 4.17.0 it's a helm upgrade with --reset-then-reuse-values, not --reuse-values, and a couple of defaults moved, so the upgrade notes are worth reading first. Still on 4.16.0 or 4.3.1? The notes cover both.

Where it sits

The top of the lineup. Everything, for the whole site.

HotLoop is one codebase shipped as four products, all from the same tag. IoT is the automation base, Edge adds the machine layer, and the Gateway is both of those plus what a whole site needs: fleet management, multi-site roll-ups and scheduled reports. The Edge Relay is the one with no screen that polls and forwards.

All four are out in 4.18.11, each with its own image and chart. The Gateway kept the image and chart names it shipped under, so nobody running it changes a thing. If you need one box running a whole site, this is the box. If the box sits next to one machine, Edge is smaller.

What only the Gateway has
  • Fleet

    In 4.18.11

    Restart a relay, ask it to rebirth (re-announce itself), or send it a new device file without anybody driving out to the box.

    More
    Fleet knows every Edge Relay you register from its Sparkplug birth and death messages. Off until you switch it on, and API only, no screen yet. A pushed file lands from 4.17.0 on; 4.16.0 relays dropped it.
  • Multi-site

    In 4.18.11

    One Gateway asks the others the questions it would answer for itself. A site that doesn't answer shows as unreachable, never as zero alarms.

  • Scheduled reports

    In 4.18.11

    Historian numbers on a schedule, as CSV or HTML, stored and emailed. Only good readings count, and every row says how many it threw out.

The write gate

Every write goes through one function, and nothing routes around it.

The UI, the API, a rule, a script, an agent: same function, same checks, same order. Policy lives in one place because the day it lives in five, somebody forgets the fifth, and that somebody's write goes to a live press.

  1. 01

    The caller may write.

    A logged-in user needs values.write, and a viewer does not have it. An agent needs the MCP token, or it gets a 401.

    More
    A write straight to a tag that fails this is a 403 before the gate hears a word. (A call through an entity hands this check to the gate instead, so that refusal is audited.)

  2. 02

    Writes are on.

    Writes ship off.

    More
    The switch is safety.allowWrites, and it has shipped off since 4.4.0, because a fresh install that can move machinery before anybody decided it should is a lawsuit with a web UI.

  3. 03

    Agent writes are on.

    Agents get their own switch, safety.allowMcpWrites, checked only for an agent. It narrows the master switch. It can never open a door the master switch closed.

  4. 04

    The tag is armed.

    Every tag is read-only until somebody marks it writable, one at a time, on purpose. A browse import of 4,000 tags arms none of them.

  5. 05

    The value is in range.

    Out of range is refused, never clamped. Somebody who typed 500 on a 0 to 250 setpoint made a mistake, and quietly sending 250 means they never find out.

  6. 06

    The entity agrees.

    A call through an entity adds the entity's own rules: a number's step, a text's pattern, an entity somebody disabled.

    More
    They're checked here, inside the gate, so that refusal is audited like any other. A write straight to a tag has none.

  7. 07

    The device is running.

    No running device, no write, and the refusal is audited. One that's up but mid-reconnect gets past this check.

    More
    The audit row opens, and the write fails there with a 503, on the record as a failed attempt. The dry run checks the live session itself, so a recipe or a script finds out before anything moves.

  8. 08

    The audit row is written, then the value goes out.

    In that order. A refusal is recorded as a refusal, and a write that hangs or crashes the process still left evidence that it was attempted.

An agent gets no side door. write_tag and call_service hit the same gate an operator does, and neither one runs without a stated reason. The audit row records who and what, and the reason lands in the system log right next to the write it explains. A write nobody can explain six months later is just a timestamp with a guilty conscience.

Bad is not zero

A failed read is not a value.

Every reading carries good, uncertain or bad. A driver that can't read a point still reports, marked bad, because silence looks exactly like a value that hasn't changed. Alarms hold their state on a bad reading instead of evaluating it, so a dead instrument shows up as a dead instrument and not as a process upset.

Pull the wire on TT-101.

Same sensor, same moment, two systems. Low alarm at 40 degC. Pull the wire and watch which one wakes somebody up for nothing.

Calls a failed read zero
LO 40
0.0quality: good

LOW TEMPERATURE. Paging on-call.

HotLoop Gateway
LO 40
61.3quality: bad

Normal, held. The reading is bad, so the alarm isn't evaluated on it.

The left one just paged somebody at 3am for a process event that never happened. The right one told the truth: nobody knows the temperature right now, and the problem is the instrument, not the tank.

A simulation in your browser. The quality rule and the held alarm are the Gateway's.

Built for agents

MCP in both directions.

27 tools and 4 resources, over streamable HTTP and stdio, up from 16 in 4.16.0. An agent reads the plant in entities, the infeed pump rather than register 40001, and it can only act through the gate. So you can ask it why line 3 is slow and let it read everything, knowing it can only touch what you armed, and every write it makes comes with a reason in the log.

It goes the other way too: a rule can call a tool on another MCP server.

Many sites

One question, every plant.

Point one Gateway at the others and the Sites screen asks each of them the question it would answer itself. Each site keeps its own database, so a dead WAN link takes out a roll-up, never a plant.

A site that doesn't answer shows as unreachable, with the reason, and stays out of the totals. Counting a plant you can't reach as zero alarms is how a head office decides everything is fine on the worst night of the year.

What it speaks

Ten native drivers, and a table that admits how far each has been proven.

S7comm and EtherNet/IP have never talked to a physical PLC.

Their decoding is tested hard, and tested decoding is still not a value that came off a real controller. If you're the first to point either one at real hardware, treat it as commissioning: check a value you already know before you trust a single screen.

SNMP and BACnet/IP have never been read off physical gear.

SNMP was read from net-snmp's agent at every v3 security level, and BACnet/IP from bacpypes3, an independent BACnet stack, and frames recorded off it. Neither is any vendor's firmware. Vendors differ about their MIBs, their object lists, and how they answer a request that is too big, so treat the first read of a real switch, UPS, or controller as commissioning: compare the probe's values with the device's own display.

What's inside

The rest of the plant, in one binary.

No sidecar, no second service to keep alive, no runtime to babysit. One static Go binary running as a non-root user with every capability dropped.

Historian

TimescaleDB (a time-series database) when the extension's there, native partitioning when it isn't. Per-tag deadband, so a sensor twitching in its last bit doesn't write a row every scan until the disk fills.

Alarms

ISA-18.2 (the alarm-management standard), rtn-unack (cleared but not yet acknowledged) included. Acknowledged and cleared are separate, so the trip at 03:00 that fixed itself is still on the list when the morning shift walks in.

Automations

Eleven trigger types, entity state and the sun included, and conditions compiled on save. A broken rule gets caught in the editor, not at 3am halfway through a batch.

The Board and dashboards

The Board is the screen that lives on a wall, with gauges that show the alarm limits where they fall. Build other dashboards for a role or a physical screen.

Entities

The infeed pump, not register 40001. Every tag gets an entity, and a motor, valve, or PID loop is one entity built from the tags that make it up.

Plant tree

One ISA-95 tree (the standard plant hierarchy), enterprise down to equipment, plus labels. Filter a whole area in one click, and the site, area, and line your devices already carry stay in step with it.

Paging

ntfy, Gotify, Discord, webhook, and email. Acknowledge from an ntfy push or the Discord bot and the Gateway records it. Discord has only met a stand-in.

Reports

Historian numbers on a schedule, as CSV or HTML, stored and emailed. Only good readings count, every row says how many it threw out, and an empty period says no data, never zero.

OPC UA server

Read-only, secured, and Ignition 8.3.9 reads it end to end. The SCADA you already paid for can see every tag, and there is no write handler in there to worry about.

Edge Relay

Its own product, image and chart: about 15 MB against the Gateway's 35 on arm64, no database, no UI. More on the Edge Relay.

Embedded broker

An optional MQTT broker in the same process, so a single box can be its own Sparkplug hub. At scale, run a real broker.

Backups

A snapshot in pure Go over the connection it already has. Restore loads an older backup by column name and flat refuses one from a newer version, so a restore can't half-work.

The full detail behind these cards

The Board and dashboards

The Board is the screen that lives on a wall: gauges with the alarm limits drawn where they fall, and a layout stored with the plant, so the panel PC and your laptop always show the same thing. It is one dashboard among the ones you can build, for a role or for a physical screen.

Entities

The infeed pump, not register 40001. Every tag gets an entity for free, and a motor, valve, or PID loop is one entity built from the tags that make it up. Rules, screens and agents read like the plant, not like a register map.

Paging

ntfy, Gotify, Discord, webhook, and email. Press Acknowledge on an ntfy push or in the Discord bot and the alarm is acknowledged in the Gateway, signed and single use. Discord has only met a stand-in, not the real thing.

Edge Relay

Its own product with its own image and chart: about 15 MB against the Gateway's 35 on arm64, no database, no UI. The Gateway's fleet API tracks and commands every relay you register (no screen for it yet). More on the Edge Relay.

Embedded broker

An optional MQTT broker in the same process, so a single box can be its own Sparkplug hub. Logins made in HotLoop carry per-topic publish and subscribe rules, and logins from HOTLOOP_BROKER_USERS can do anything. At scale, run a real broker.

Install

Helm, from the published chart.

Any Kubernetes, k3s included. The chart brings its own TimescaleDB, generates the database password, the MCP token and an admin account, and keeps all three across upgrades, so an upgrade never logs you out of your own plant.

helm repo add hotloop https://hotloop.io/hotloop
helm repo update
helm install hotloop hotloop/hotloop --version 4.18.11 \
  --namespace hotloop --create-namespace

The user is admin. The chart generates the password, keeps it in the release Secret, and the install notes print the command that reads it back:

kubectl -n hotloop get secret hotloop \
  -o jsonpath='{.data.admin-password}' | base64 -d
kubectl -n hotloop port-forward svc/hotloop 8080:8080

Open http://localhost:8080, sign in, add a device, discover its tags. Then notice that you can't write to anything. That's on purpose. Every tag starts read-only and gets armed one at a time, and safety.allowWrites stays off until you decide this pod gets to move machinery. Nobody's first day with HotLoop should end with a press doing something they didn't ask for.

The image is ghcr.io/hotloop-io/hotloop:4.18.11, amd64 and arm64, about 35 MB on arm64 and 37 MB on amd64, and it pulls anonymously. The database is a setting, not an image: TimescaleDB by default, with plain PostgreSQL and SQLite as database.type choices in the same image.

Each product picks its own. IoT takes SQLite or PostgreSQL. Edge and the Gateway take SQLite, PostgreSQL or TimescaleDB. The Edge Relay has no database at all, only a disk-backed forward queue.

Off a cluster, the Gateway has Podman Quadlet units under deploy/quadlet/gateway/ in the hotloop repository, which is private for now, so the Quadlet install guide is where to read how they work. Those units were run rootless under systemd, including an update that can't start and gets rolled back. The install docs cover the values worth setting before a real plant sees it.

Coming from 4.17.0

A helm upgrade, and a few defaults moved.

Use --reset-then-reuse-values, not plain --reuse-values, so the new defaults come along.

The command and the catches
helm repo update hotloop
helm upgrade <release> hotloop/hotloop --version 4.18.11 --reset-then-reuse-values

The 4.17.0 and 4.18.10 charts render the same eight objects with the same selectors and the same volume claims. Plain --reuse-values renders too this time, but it replays 4.17.0's values and drops every new default, so use the reset form or your values file.

Migrations 0016 to 0024 run on start. Migrations only add, so going back to 4.17.0 works, but take a pg_dump first, like always. edgePublish.queueMaxBytes is 64 MiB by default now, not unbounded. Past it the oldest readings are dropped and counted, because an unbounded queue filled the volume in a long outage. Size it from your own reading rate, about 180 bytes a reading, or set it to 0 to keep the old behavior. MQTT logins made in Settings carry topic rules from a profile, and logins from HOTLOOP_BROKER_USERS are untouched. There's a new fleet.update permission, admin only.

Coming from 4.16.0

A plain helm upgrade, with two catches.

Use the reset form, and pg_dump first, because device passwords get encrypted on the first start.

The command and the catches
helm repo update hotloop
helm upgrade <release> hotloop/hotloop --version 4.18.11 --reset-then-reuse-values

Not --reuse-values. That replays 4.16.0's values, drops every default this chart added, and the render dies on the first one it needs. Nothing changes on the cluster when it does, so just run it again the right way.

Your device passwords get encrypted on the first start, with a key the chart makes in <release>-secret-key. From then on that key is part of your data: a backup taken after the upgrade only restores where the key is mounted, and going back to 4.16.0 hands every device the sealed text as its password. So pg_dump before you upgrade, and keep that Secret somewhere other than next to your backups, because a key sitting beside the thing it protects protects nothing. Migrations 0009 to 0015 run on start, all additive.

Coming from 4.3.1

It is not a plain helm upgrade.

The name changed, and Kubernetes refuses to change a workload selector in place, so the old workloads go first.

The steps and the surprises

4.16.0 was the first published release after 4.3.1, so from 4.3.1 you're taking every version from 4.4.0 on in one jump, under a new name. The image was ghcr.io/embernet-ai/industrial-iot, the chart was industrial-iot, and every IIOT_ variable is HOTLOOP_ now. The chart name is baked into the workload selectors, and Kubernetes flat out refuses to change a selector in place. Keep the release name and the namespace, and:

  1. pg_dump the database. Belt and braces, and you'll be glad you did.
  2. Delete the old Deployment (<release>) and StatefulSet (<release>-postgresql). The database volume is its own claim and stays put.
  3. helm upgrade <release> hotloop/hotloop -f your-4.3.1-values.yaml, with any IIOT_ in extraEnv renamed and any image.repository override dropped. Not --reuse-values, which skips every default the chart added since.

The new database pod mounts the old volume and the Gateway migrates it forward on start. We proved that by rendering both charts side by side, not by running it on a live cluster. That gap is exactly why step one exists.

Three more things will surprise a script. There's a login now. Writes default to off. And the API changed in a handful of places, like empty lists coming back as [] and never null. The license is HotLoop's own now, the Business Source License 1.1. The full list and the procedure are in the upgrade docs.

What changed lately

The detail is in the release notes, and it is one click away.

4.18.11 moved the database to PostgreSQL 17 with an upgrade that runs itself, so read that one before you touch a running plant. 4.18.10 was the four-product split, MQTT and network discovery, historian targets and boxes that update themselves, and 4.17.0 was recipes, scripts, blueprints, helpers, the logbook and UniFi, read-only. Each has its own page with what it buys you and what it costs.

Read the core release notes

Licensing

Free. For you, and for your business through EmberNet.

The Gateway is source-available under the HotLoop Business Source License 1.1, which is not an OSI open source license. There are no license keys and nothing unlocks at runtime. Home and lab use is free. Any business use is free through EmberNet, where signing up is free and running HotLoop is free. Support, SLAs and warranties come from our Official Systems Integrator, Fireball Industries, who'll also design, install, and support the rollout if you'd rather not.

Read the license