Send your own data
Send readings from your own script, rig or service, and they become live channels on this device.
The endpoint
http://127.0.0.1:8078/ingest/<codec>
<codec> is the body format you send — batch, jsonl, mcap and the
others below.
| Detail | Value |
|---|---|
| Port | 8078 on a default install |
| Reach | this device, while the Phloem app runs |
| Auth | none — only this device can reach the address |
| Limit | 64 MiB per request |
To send from another machine, forward the port over SSH or your VPN.
Before you send, add a Push endpoint input on Inputs (how).
Set its Codec to batch for the examples below, or * to accept every
codec. Without one, a push gets 403.
If you send first, the input appears on Inputs. Enable accepts it.
Send
import json, time, urllib.request
URL = "http://127.0.0.1:8078/ingest/batch"
for i in range(20):
body = [{"path": "/demo/temp_c",
"samples": [{"t_us": time.time_ns() // 1000,
"value": {"f64": 21.5 + i}}]}]
req = urllib.request.Request(
URL, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
print(r.read().decode())
time.sleep(0.5)
curl -X POST http://127.0.0.1:8078/ingest/batch \
-H 'Content-Type: application/json' \
-d '[{"path":"/demo/temp_c","samples":[{"t_us":1785500000000000,"value":{"f64":21.5}}]}]'
Each POST replies with what it took:
{"codec":"batch","batches":1,"samples":1}. A refused list names any path
another input already owns.
On Live, a new channel gets its own tile while there are only a few channels. After that, find it in the list on the left and drag it in. On Inputs, the row shows a rate and a last-seen time while you send.
Body formats
Each Codec takes one body shape.
An array with one entry per channel. The only codec that can mark a channel as a picture or a mask.
[{"path": "/usv/heading_deg",
"samples": [{"t_us": 1785500000000000, "value": {"f64": 91.4}}]},
{"path": "/usv/estop_ok",
"samples": [{"t_us": 1785500000000000, "value": {"bool": true}}]}]
t_us is required: microseconds since 1970. value holds exactly one of
{"f64": 21.5} · {"i64": 3} · {"bool": true} · {"text": "drive"} ·
{"bytes": [255, 216]} · {"null": null}. Use null for a reading that
happened but had no valid value.
For bytes, add archetype to say what they are. Phloem cannot tell a picture
from its bytes:
[{"path": "/cam/front", "archetype": "image",
"samples": [{"t_us": 1785500000000000,
"value": {"bytes": [255, 216, 255, 224]}}]}]
Use image for a camera frame, or segmentation_image for a mask over one.
Both get a camera tile. You cannot send waveforms or 3D this way
(Name channels and units). Bytes without archetype still record
and replay, but nothing draws them.
If your program falls behind and skips samples, say so in the next body. Add a
gap item beside the batches:
[{"gap": {"paths": ["/demo/temp_c"], "frontier_us": 1785500000000000,
"detail": "loop stalled 1.2 s and skipped forward"}},
{"path": "/demo/temp_c",
"samples": [{"t_us": 1785500001200000, "value": {"f64": 21.5}}]}]
frontier_us is the time of the last sample you sent. Add
"at_least_samples": n if you know how many you skipped; otherwise Phloem
estimates it from the channel's rate. Replay shows a stated gap as missing
samples with your reason, and an unstated one as unexplained.
One JSON object per line.
{"t_us":1785500000000000,"/can/rpm":3000.0,"flag":true,"mode":"drive"}
{"t_us":1785500000500000,"/can/rpm":3100.0,"flag":false,"mode":"drive"}
Same shape as the jsonl file reader. A bad line is skipped;
the request still succeeds.
candump text, one frame per line: (<seconds>) <interface> <ID>#<HEXDATA>.
(1785500000.000000) can0 0C0#1F40007D52030000
(1785500000.010000) can0 0C0#1F60007E52030000
Decoded as on SocketCAN.
The same candump text, not decoded: one raw sample per frame on
/can/<id>. Decode it in a pipeline instead.
The body is a whole MCAP file, sent as raw bytes (--data-binary @run.mcap).
You get one channel per topic, and one per field of a JSON message.
The body is a ROS 2 bag, sent as raw bytes. The only codec that carries 3D: point clouds, images and detections arrive ready to draw. Everything else becomes numbers and text.
/ingest/mjpeg?path=/cam/front — the body is one JPEG frame, or several
back to back. No multipart. ?path= names the channel (default /cam/front),
so one address serves many cameras. Use it for a camera that cannot run its own
server.
Responses
| Status | Means | Do |
|---|---|---|
200 | accepted | nothing |
400 | unknown codec, or a body it can't read | read error |
403 | no input open for this codec | open one, above |
409 | every channel is owned by another input | send on different paths |
503 | not stored | retry the same body |
| no reply | nothing is listening | is the app running? see Reach |
A 403 explains itself:
{"error": "no enabled input for push codec 'batch' — enable it in Inputs (it appears there as a discovered candidate)",
"candidate": true}
Plain TCP. Port 8079 takes the same text codecs: the codec name on the
first line, then one record per line.
- It replies with nothing, and an unknown codec closes the connection. Get it working over HTTP first.
- When you stop, send a last line that is exactly
end. - If the connection closes without
end, Phloem assumes your program died or lost its link. Every channel it sent gets a gap (producer_cut) until it reconnects.
From a program: the SDK
The Phloem SDK (Python, C, Go, Rust) sends to this device over a local
connection instead of HTTP (a socket on macOS and Linux, a named pipe on
Windows). It finds it with no setup, says which clock each
frame's times come from, and reports gaps. First add an input for codec phlm
on Inputs.
import phloem_sdk
with phloem_sdk.Client(producer="bench-pc", clock_id="usb:0x0683-SN-01A2B3C4") as c:
c.uniform(t0_ns, "source", {"/rig/pressure": readings}, step_hz=1000)
Set clock_id when your times come from the device itself ("source").
Use something that survives a restart, like a serial number or bus address.
Phloem learns how that clock drifts from this computer's and remembers it under
that name.
Without clock_id, each connection is a new clock, and learning its drift takes
minutes. Until then, if the device clock is more than 5 s ahead of this
computer, its readings are moved back to the computer's time. Leave clock_id
out only when your times are this computer's clock ("host"). An empty
clock_id is refused.
In other languages: phloem_connect_with_clock in C, phloem.WithClock(id)
in Go, connect_with_clock in Rust.
Updated