How a small ESP32 firmware — or a five-line Python script — pushes a measurement to a Bayou feed, and what Bayou will do with each field.
In the reference kit that this repo ships (examples/v3-ultrasonic),
a physical distance reading takes four hops to reach a browser chart:
This page is about the last hop: the plain HTTP POST from the Heltec receiver to Bayou. It's simple enough that anything that can make an HTTPS request — a shell script, a Python script, a webhook, another microcontroller — can play the role of "sensor" for a Bayou feed. The Python script in section 5 is a working example of exactly that.
When the receiver gets a LoRa DM from the sender, it parses the distance and
battery voltage out of the payload, then builds a small JSON object and
POSTs it to Bayou. The relevant firmware is in
examples/v3-ultrasonic/companion_sensor_receiver/main.cpp,
function postToBayou. The request looks like this:
POST https://bayou.pvos.org/data/<feed_public_key>
Content-Type: application/json
{
"private_key": "<feed_private_key>",
"node_id": 2,
"distance_meters": 1.723,
"battery_volts": 3.874,
"aux_1": 0,
"aux_2": 1,
"log": "path=5a.91 route=DIRECT",
"source": "Sensor"
}
Two things about that body worth pausing on:
distance_meters, battery_volts, node_id,
aux_1, aux_2, and log are stored;
source is dropped on the floor because Bayou has no column
for it. The receiver sends it anyway because it costs nothing and reads
nicely in a packet capture.
On a successful post Bayou answers 200 Measurement recorded.
On a private-key mismatch it answers 400 Private key mismatch.
A network error or unreachable server produces an HTTP client error, which
the receiver logs and retries after 15 seconds.
A Bayou "feed" is just two 12-character IDs (nanoid, alphabet
23456789abcdefghijkmnpqrstuvwxyz — ambiguous characters
removed) plus a Postgres row backing it. There are no accounts, no logins,
and no email addresses; the two keys are the only access control.
| Key | Where it appears | What it grants |
|---|---|---|
public_key |
In every URL of the feed
(/data/<public_key>/…) and on the feed page itself.
You can share it freely. |
Read. Anyone with the public key can view the chart, pull the JSON, download the CSV, and see all fields you've posted. |
private_key |
Only in the body of write requests
({"private_key": "…"}). Never in a URL, never on the
feed page. |
Write. Anyone with the private key can post new
measurements to the feed — or wipe the feed via
/manage/clear/…. |
To create a feed, either open the Bayou home page in a browser and click through the create-feed form, or from a script:
curl -X POST https://bayou.pvos.org/manage/
Either way, Bayou returns an HTML page showing the new feed's public key, private key, feed URL, and a QR code. Save the private key immediately — it's shown once and there is no recovery flow if you lose it.
GET /manage/clear/<public_key>/<private_key>. Because
it's a GET, any crawler, link-preview bot, or curious viewer that follows a
URL containing both keys will erase the data. Never share a URL that includes
the private key.
A POST to /data/<public_key> may include any subset of
the fields below (plus the required private_key). Unknown
fields are ignored; omitted fields are stored as SQL NULL.
Numeric fields are numeric-typed in Postgres, so pass numbers not strings.
Timestamps are assigned by Bayou when the row is inserted — clients cannot
supply their own timestamp, which also means offline data cannot be
backfilled.
| Field | Group | Typical use |
|---|---|---|
temperature_c | env | Ambient temperature in °C. Default plot parameter for many feeds. |
humidity_rh | env | Relative humidity, %. |
pressure_mbar | env | Atmospheric pressure, millibars. |
co2_ppm | env | CO₂ concentration, ppm. Drives the CO₂ air-changes-per-hour tool. |
light_lux | env | Ambient light level, lux. |
distance_meters | distance | Primary distance reading. Used by the river-level view and by this repo's ultrasonic kit for water level. |
distance_meters_1,distance_meters_2,distance_meters_3 |
distance | Extra distance channels when one node reads several distance sensors. |
temperature_c_1,temperature_c_2,temperature_c_3 |
env | Extra temperature channels — e.g. probes at different depths, or enclosure vs. ambient. |
battery_volts | power | Sensor node's battery voltage, useful for health monitoring. |
voltage_1,voltage_2,voltage_3 |
power | Extra voltage channels — solar panel, load bus, secondary battery. |
gps_lat,gps_lon,gps_alt |
gps | Location. Used by the /data/<pubkey>/map/ view
to plot fixes on Leaflet. |
aux_1,aux_2,aux_3 |
generic | Generic numeric channels for anything without a dedicated column.
The receiver in this repo uses aux_1 / aux_2
to record measured and claimed LoRa hop counts. |
node_id | radio | Distinguishes multiple sensors sharing one feed. The feed page
draws one line per node, and most read endpoints have
/…/<node_id> variants for per-node views. |
rssi,next_hop,next_rssi |
radio | Mesh-radio diagnostics. Used by the gridsquare / signal-map view. |
log | text | Short free-form text (VARCHAR-sized). Useful for structured but
column-less metadata — e.g. path=5a.91 route=DIRECT. |
If none of the recognized fields fits, use aux_1–aux_3
or log. Adding a genuinely new column is possible but touches
three files in the Bayou codebase (the SQL schema, the INSERT statement, and
restructureJSON in controllers/dataControllers.js).
The script example_temp_poster.py
next to this page does exactly what the receiver does — build a small JSON
object with the private key plus a measurement field, and POST it to Bayou
every few seconds. The only difference is that instead of pulling a real
distance from a LoRa DM, it generates a random temperature.
The heart of the script:
URL = f"https://bayou.pvos.org/data/{PUBLIC_KEY}"
while True:
payload = {
"private_key": PRIVATE_KEY,
"node_id": 1,
"temperature_c": round(random.uniform(15.0, 25.0), 2),
}
r = requests.post(URL, json=payload, timeout=10)
print(payload["temperature_c"], "->", r.status_code, r.text.strip())
time.sleep(10)
You can swap temperature_c for any recognized field in
section 4, or include several at once — everything Bayou recognizes
in one POST lands on the same measurement row.
example_temp_poster.py in a text editor and paste the two
keys into PUBLIC_KEY and PRIVATE_KEY.
pip install requests # first time only
python3 example_temp_poster.py
You should see one line per POST: temperature_c=21.4 -> 200 Measurement recorded.
https://bayou.pvos.org/data/<YOUR_PUBLIC_KEY>/?plot_param=temperature_c
The chart refreshes every few seconds. Each POST from the script should
add another point.
curl "https://bayou.pvos.org/data/<PUBKEY>/latest/"
curl "https://bayou.pvos.org/data/<PUBKEY>/json/?limit=20"
curl -O "https://bayou.pvos.org/data/<PUBKEY>/csv/"
The JSON response always includes every recognized field,
with null for fields you never posted — that's how the
feed page can plot any parameter without knowing in advance which
ones you actually populate.
set bayou_public_key <your pubkey> /
set bayou_private_key <your privkey>, and pair it
with the Rook sender. From Bayou's point of view the microcontroller
and the Python script are indistinguishable: same URL, same JSON body
shape, different measurement field.
NOW() when the row is inserted. There is
no way to backfill offline data — a queue-and-flush strategy will end up
with all queued readings sharing the flush time.
limit
(newest N rows), not by time. The CSV endpoint always returns every row
for the feed. Filtering by time has to happen on the client.
tempeature_c won't error — it just posts a row with every
real field NULL. If a value seems missing, print the payload
you sent and confirm the field name matches the table in section 4.
/manage/clear/<pub>/<priv> anywhere a crawler
might follow it.