Posting sensor data to Bayou

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.

On this page
  1. The end-to-end pipeline
  2. What the microcontroller does
  3. Bayou feeds: what the keys are for
  4. The allowed measurement fields
  5. A Python script that mimics the sensor
  6. End-to-end walkthrough
  7. Gotchas & things Bayou does not do

1. The end-to-end pipeline

In the reference kit that this repo ships (examples/v3-ultrasonic), a physical distance reading takes four hops to reach a browser chart:

MaxBotixultrasonic sensor
→
Rook sendernRF52840 + LoRa
→
Heltec receiverESP32-S3 + WiFi
→
BayouExpress + Postgres feed

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.

2. What the microcontroller does

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:

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.

3. Bayou feeds: what the keys are for

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.

KeyWhere it appearsWhat 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.

Destructive GET warning. The endpoint that wipes a feed is 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.

4. The allowed measurement fields

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.

FieldGroupTypical use
temperature_cenv Ambient temperature in °C. Default plot parameter for many feeds.
humidity_rhenv Relative humidity, %.
pressure_mbarenv Atmospheric pressure, millibars.
co2_ppmenv CO₂ concentration, ppm. Drives the CO₂ air-changes-per-hour tool.
light_luxenv Ambient light level, lux.
distance_metersdistance 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_voltspower 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_idradio 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.
logtext 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).

5. A Python script that mimics the sensor

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.

6. End-to-end walkthrough

  1. Create a feed. Open https://bayou.pvos.org/ and click to create a feed. Copy the public key and (especially) the private key from the page it hands back.
  2. Point the poster at your feed. Open example_temp_poster.py in a text editor and paste the two keys into PUBLIC_KEY and PRIVATE_KEY.
  3. Run the poster.
    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.
  4. Watch the chart. Open, in a browser:
    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.
  5. Explore the read side. Try the JSON, CSV, and latest endpoints:
    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.
  6. Swap in the real hardware (optional). Flash the ultrasonic kit from this repo, point the receiver at your feed with 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.

7. Gotchas & things Bayou does not do