Connect machines

Ping from curl, Python or Node-RED

When a machine's PLC cannot make web requests itself, a small computer next to it usually can: an industrial PC, a Raspberry Pi or a server running Node-RED. This guide has working examples for each. They all send the same ping described in Send pings from a machine.

In every example, replace https://heartbeat.example.com with your What Started address and hbp_… with the machine's ping token. Keep the token out of shared files and screenshots: anyone who has it can send pings for that machine. If one leaks, revoke it on the machine page and make a new one.

curl

curl is built into Windows 10 and 11, macOS and most Linux systems. Use this to check a token works before setting anything else up:

curl -X POST https://heartbeat.example.com/api/v1/ping \
  -H "Authorization: Bearer hbp_…" \
  -H "Content-Type: application/json" \
  -d '{"status":"run","cycleCount":1200}'

A good ping answers with status 202 and a body like this:

{ "ok": true, "statusChanged": false, "metricsRecorded": true, "requestId": "…" }

On Windows, in Command Prompt, use curl.exe, put the command on one line and escape the quotes in the body:

curl.exe -X POST https://heartbeat.example.com/api/v1/ping -H "Authorization: Bearer hbp_…" -H "Content-Type: application/json" -d "{\"status\":\"run\",\"cycleCount\":1200}"

Every minute, with no code

On Linux, a cron line sends a plain heartbeat once a minute. Run crontab -e and add:

* * * * * curl -fsS -m 10 -X POST https://heartbeat.example.com/api/v1/ping -H "Authorization: Bearer hbp_…" > /dev/null

On Windows, Task Scheduler can do the same: create a task that runs curl.exe with the arguments above, triggered daily and repeated every minute.

A plain heartbeat tells What Started the computer is up and can reach us. It says nothing about the machine itself. To send its status or counter, read them from the machine with one of the examples below.

Python

This script uses only Python's standard library, so there is nothing to install. It reads the token from the HEARTBEAT_TOKEN environment variable, sends a ping every 60 seconds and keeps going if the network drops.

Replace read_machine() with code that reads your machine, for example over OPC UA or Modbus, or from a file another program writes.

import json
import os
import time
import urllib.error
import urllib.request

URL = "https://heartbeat.example.com/api/v1/ping"
TOKEN = os.environ["HEARTBEAT_TOKEN"]
EVERY_SECONDS = 60


def read_machine():
    # Replace with real values from your machine. Leave out what you don't have.
    return {"status": "run", "cycleCount": 1200, "partNumber": "A-100"}


def send_ping(values):
    request = urllib.request.Request(
        URL,
        data=json.dumps(values).encode(),
        headers={"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(request, timeout=10) as response:
            return response.status, json.load(response)
    except urllib.error.HTTPError as error:
        return error.code, json.load(error)


while True:
    try:
        status, body = send_ping(read_machine())
        if status != 202:
            print("Ping refused:", body["error"]["message"], body["error"]["requestId"])
    except Exception as error:  # network down, timeout: try again next round
        print("Ping not sent:", error)
    time.sleep(EVERY_SECONDS)

Run it with:

HEARTBEAT_TOKEN=hbp_… python3 -u heartbeat_ping.py

On Windows, in PowerShell:

$env:HEARTBEAT_TOKEN = "hbp_…"
python -u heartbeat_ping.py

The -u makes messages show up straight away in logs. To keep it running after a restart, run it as a service: a systemd unit on Linux, or a Task Scheduler task that starts at boot on Windows.

Node-RED

Node-RED often already runs on gateways and Raspberry Pis next to machines, and has nodes for OPC UA, Modbus, S7 and MQTT. The flow below sends a ping every 60 seconds. It uses only built-in nodes.

  1. Open the menu and choose Import, paste the flow below, and click Import.
  2. Double-click the flow's tab, open Environment variables, and add HEARTBEAT_TOKEN with the ping token as its value. The token then lives in one place instead of inside a node. Exporting the whole flow includes it, so export only the nodes when you share a flow.
  3. Open the Ping What Started node and change the address to yours.
  4. Deploy. The debug panel shows What Started's answer to each ping.
[
  {
    "id": "hb_every_minute",
    "x": 130,
    "y": 80,
    "type": "inject",
    "name": "Every 60 s",
    "props": [],
    "repeat": "60",
    "once": true,
    "onceDelay": "5",
    "wires": [["hb_build"]]
  },
  {
    "id": "hb_build",
    "x": 310,
    "y": 80,
    "type": "function",
    "name": "Build ping",
    "func": "// Replace with values from your machine nodes. Leave out what you don't have.\nmsg.payload = { status: \"run\", cycleCount: 1200, partNumber: \"A-100\" };\nmsg.headers = {\n  Authorization: \"Bearer \" + env.get(\"HEARTBEAT_TOKEN\"),\n  \"Content-Type\": \"application/json\",\n};\nreturn msg;",
    "outputs": 1,
    "wires": [["hb_send"]]
  },
  {
    "id": "hb_send",
    "x": 490,
    "y": 80,
    "type": "http request",
    "name": "Ping What Started",
    "method": "POST",
    "ret": "obj",
    "url": "https://heartbeat.example.com/api/v1/ping",
    "wires": [["hb_answer"]]
  },
  {
    "id": "hb_answer",
    "x": 690,
    "y": 80,
    "type": "debug",
    "name": "What Started answer",
    "active": true,
    "complete": "payload",
    "wires": []
  }
]

To send real values, replace the Every 60 s node with the node that reads your machine and add a delay or rate limit node set to one message per minute. In Build ping, take the values from the incoming message instead of the fixed ones, for example msg.payload.counter.

Check it worked

Open the machine in What Started. Its tile turns from No signal to its current status within a few seconds of the first good ping.

If a ping is refused, the answer says why:

Status What it means
401 The token is missing, mistyped or revoked
400 A field is wrong. The message names it, for example status
413 The body is over 4 KB, which a normal ping never reaches
415 A body was sent without Content-Type: application/json
500 Our side failed. Try again on the next ping, and tell us if it lasts

Every answer includes a requestId. Quote it if you contact us.