Link copied!

Open Telemetry API

Bob streams live telemetry to an InfluxDB Cloud database every 60 seconds — GPS position, battery voltage, solar power, heading, IMU orientation, and more. The database is open for anyone to query with a free read-only token.

Bob's Current Position

Read-Only API Token

PBQmE7mOKmd5U_6krcYOSjBad9_pveomsc8srZME_BfgQpHjaucNQ-4_VYehA7quC7qNLWczAwjFn6OGUMcs3A==

Python

pip install influxdb-client
#!/usr/bin/env python3
"""ProjectBob.xyz — Get Latest GPS Position from InfluxDB"""

from influxdb_client import InfluxDBClient

INFLUXDB_URL   = "https://us-east-1-1.aws.cloud2.influxdata.com"
INFLUXDB_TOKEN = "PBQmE7mOKmd5U_6krcYOSjBad9_pveomsc8srZME_BfgQpHjaucNQ-4_VYehA7quC7qNLWczAwjFn6OGUMcs3A=="
INFLUXDB_ORG   = "littlebob"

QUERY = '''
from(bucket: "littlebob")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "littlebob")
  |> filter(fn: (r) => r._field == "lat" or r._field == "lon")
  |> last()
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
'''

def get_latest_position():
    with InfluxDBClient(url=INFLUXDB_URL, token=INFLUXDB_TOKEN, org=INFLUXDB_ORG) as client:
        tables = client.query_api().query(QUERY)
        for table in tables:
            for record in table.records:
                lat = record.values.get("lat")
                lon = record.values.get("lon")
                if lat is not None and lon is not None:
                    return lat, lon, record.get_time()
    return None

if __name__ == "__main__":
    result = get_latest_position()
    if result:
        lat, lon, ts = result
        print(f"Latest position: {lat:.6f}, {lon:.6f}")
        print(f"Timestamp:       {ts}")
        print(f"Google Maps:     https://www.google.com/maps?q={lat},{lon}")
    else:
        print("No position data found in the last 24 hours.")
Download get_position.py

JavaScript — Try It Live

Runs right in your browser via the InfluxDB HTTP API. Hit Run to fetch Bob's position, or paste it into JSFiddle.

const INFLUXDB_URL = "https://us-east-1-1.aws.cloud2.influxdata.com";
const TOKEN = "PBQmE7mOKmd5U_6krcYOSjBad9_pveomsc8srZME_BfgQpHjaucNQ-4_VYehA7quC7qNLWczAwjFn6OGUMcs3A==";
const ORG   = "littlebob";

const query = `from(bucket: "littlebob")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "littlebob")
  |> filter(fn: (r) => r._field == "lat" or r._field == "lon")
  |> last()`;

async function getPosition() {
  const resp = await fetch(
    `${INFLUXDB_URL}/api/v2/query?org=${ORG}`,
    {
      method: "POST",
      headers: {
        "Authorization": `Token ${TOKEN}`,
        "Content-Type": "application/vnd.flux",
        "Accept": "application/csv",
      },
      body: query,
    }
  );
  const csv = await resp.text();
  const rows = csv.trim().split("\n").filter(r => !r.startsWith("#") && r);
  const header = rows[0]?.split(",");
  const fieldIdx = header?.indexOf("_field");
  const valueIdx = header?.indexOf("_value");

  const data = {};
  for (const row of rows.slice(1)) {
    const cols = row.split(",");
    if (cols[fieldIdx]) data[cols[fieldIdx]] = parseFloat(cols[valueIdx]);
  }
  return data;
}

getPosition().then(d => {
  console.log(`Position: ${d.lat}, ${d.lon}`);
});

cURL

One-liner for the terminal. Returns CSV.

curl -s -XPOST \
  "https://us-east-1-1.aws.cloud2.influxdata.com/api/v2/query?org=littlebob" \
  -H "Authorization: Token PBQmE7mOKmd5U_6krcYOSjBad9_pveomsc8srZME_BfgQpHjaucNQ-4_VYehA7quC7qNLWczAwjFn6OGUMcs3A==" \
  -H "Content-Type: application/vnd.flux" \
  -H "Accept: application/csv" \
  -d 'from(bucket:"littlebob") |> range(start:-1h) |> filter(fn:(r)=>r._measurement=="littlebob") |> last() |> pivot(rowKey:["_time"],columnKey:["_field"],valueColumn:"_value")'

Query Playground

Edit the Flux query below and hit Run to see live results.

Connection Details

Use these values to connect from any InfluxDB client library (Python, JS, Go, Rust, etc.) or the InfluxDB Cloud UI.

URLhttps://us-east-1-1.aws.cloud2.influxdata.com
Organization2a6cdd2d0e5960bf
Bucketlittlebob
Measurementlittlebob
Write interval~60 seconds

Data Schema

Every telemetry row is tagged with string (which controller: A or B), device (hardware ID), and master (true/false — whether this controller currently owns the helm).

Position & Navigation

FieldTypeUnitDescription
latfloatdegreesGPS latitude (WGS84)
lonfloatdegreesGPS longitude (WGS84)
headingfloatdegreesMagnetometer compass heading (tilt-compensated)
gps_headingfloatdegreesGPS course over ground — only valid when moving
speedfloatknotsSpeed over ground from GPS
rudderfloatdegreesCurrent rudder angle (-90 to +90)
throttlefloat%Propeller throttle setting (0–100)

Power

FieldTypeUnitDescription
vbusfloatVBattery bus voltage — healthy range 12.0–14.4 V
wattsfloatWSolar panel power input

Sensors & Comms

FieldTypeUnitDescription
rollfloatdegreesIMU roll (heel angle of the boat)
pitchfloatdegreesIMU pitch (fore/aft tilt)
wifi_rssiintdBmWiFi signal — typically -30 (strong) to -90 (weak)
iridium_signalintbarsIridium satellite signal (0–5)

System

FieldTypeUnitDescription
uptime_secintsecondsTime since last reboot
heap_kbintKBFree heap memory on the ESP32
relay_statesintbitmask4-bit relay state: bit 0 = USB1, bit 1 = USB2, bit 2 = Starlink, bit 3 = Relay4

More Flux Queries

Use these with any client or the Flux editor in InfluxDB Cloud.

Battery Voltage — Last 24 Hours

10-minute averages — overnight drain and daytime solar charge curves.

from(bucket: "littlebob")
  |> range(start: -24h)
  |> filter(fn: (r) => r._measurement == "littlebob")
  |> filter(fn: (r) => r._field == "vbus")
  |> aggregateWindow(every: 10m, fn: mean, createEmpty: false)

Speed Over the Last Week

Hourly average speed — daily sailing patterns and overnight drift.

from(bucket: "littlebob")
  |> range(start: -7d)
  |> filter(fn: (r) => r._measurement == "littlebob")
  |> filter(fn: (r) => r._field == "speed")
  |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)

All Fields — Latest Snapshot

Every field in one wide row — good for a status dashboard.

from(bucket: "littlebob")
  |> range(start: -1h)
  |> filter(fn: (r) => r._measurement == "littlebob")
  |> last()
  |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")

Fair Use

This is a public read-only token. To keep it available for everyone:

  • Use aggregateWindow for large time ranges — don't pull millions of raw points
  • Don't poll more than once per minute — Bob only writes every ~60 seconds anyway
  • The token is read-only — you can't write, delete, or modify anything