# Send your own data

# Send your own data

Send readings from your own script, rig or service, and they become live
channels on this device.

## The endpoint

```text
http://127.0.0.1:8078/ingest/<codec>
```

`<codec>` is the body format you send — `batch`, `jsonl`, `mcap` and the
others [below](#body-formats).

| 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](https://phloem.ai/docs/inputs/)).
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


### Python

```python
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

```bash
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.


### batch
An array with one entry per channel. The only codec that can mark a channel
as a picture or a mask.

```json
[{"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:

```json
[{"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](https://phloem.ai/docs/inputs/channels/)). 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:

```json
[{"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.

### jsonl
One JSON object per line.

```jsonl
{"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](https://phloem.ai/docs/inputs/files/). A bad line is skipped;
the request still succeeds.

### candump
candump text, one frame per line: `(<seconds>) <interface> <ID>#<HEXDATA>`.

```text
(1785500000.000000) can0 0C0#1F40007D52030000
(1785500000.010000) can0 0C0#1F60007E52030000
```

Decoded as on [SocketCAN](https://phloem.ai/docs/inputs/streams/).

### canframe
The same candump text, not decoded: one raw sample per frame on
`/can/<id>`. Decode it in a pipeline instead.

### mcap
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.

### rosbag2
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.

### mjpeg
`/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:

```json
{"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**.

```python
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.
