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.
- Open the menu and choose Import, paste the flow below, and click Import.
- Double-click the flow's tab, open Environment variables, and add
HEARTBEAT_TOKENwith 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. - Open the Ping What Started node and change the address to yours.
- 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.