Developer Docs

Connect your ESP32, Arduino, or Raspberry Pi to live Android sensor streams via the SensorCast WebSocket API.

Quick Start — ESP32 Arduino

The sketch below connects to a SensorCast stream as a subscriber. It uses the Links2004/arduinoWebSockets library. Install it from the Arduino Library Manager before uploading.

Connection facts
  • Server: wss://api.sensorcast.app
  • Socket.IO path: /socket.io/ (default)
  • Namespace: /stream/:username
  • After connect: emit role with "subscriber" within 5 seconds
  • Server replies with a connected event
  • Send a heartbeat event every <30 s or the server drops you as a zombie
  • When the publisher leaves the server emits publisher_disconnected
#include <Arduino.h>
#include <WiFi.h>
#include <SocketIOclient.h>   // Links2004/arduinoWebSockets

const char* SSID       = "your-wifi";
const char* PASS       = "your-password";
const char* SC_HOST    = "api.sensorcast.app";
const uint16_t SC_PORT = 443;
const char* USERNAME   = "alice";   // SensorCast username of the publisher

SocketIOclient socketIO;

unsigned long lastHeartbeat = 0;

void onEvent(socketIOmessageType_t type, uint8_t* payload, size_t length) {
  if (type == sIOtype_EVENT) {
    String msg = String((char*)payload, length);

    // Parse event name from Socket.IO payload ["name","data"]
    if (msg.startsWith("["connected"")) {
      Serial.println("[SC] Connected as subscriber");

    } else if (msg.startsWith("["frame"")) {
      // Extract the frame string between the outer quotes
      int start = msg.indexOf(","") + 2;
      int end   = msg.lastIndexOf(""]");
      if (start > 1 && end > start) {
        String frame = msg.substring(start, end);
        Serial.print("[FRAME] ");
        Serial.println(frame);
      }

    } else if (msg.startsWith("["publisher_disconnected"")) {
      Serial.println("[SC] Publisher left — waiting for reconnect");
    }
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.println("WiFi connected");

  // Connect to namespace /stream/<username>
  String ns = "/stream/";
  ns += USERNAME;
  socketIO.beginSSL(SC_HOST, SC_PORT, "/socket.io/?EIO=4", ns.c_str());
  socketIO.onEvent(onEvent);

  // Announce subscriber role immediately after connection
  socketIO.sendEVENT("["role","subscriber"]");
}

void loop() {
  socketIO.loop();

  // Send heartbeat every 20 seconds to stay alive
  if (millis() - lastHeartbeat > 20000) {
    socketIO.sendEVENT("["heartbeat",{}]");
    lastHeartbeat = millis();
  }
}

Python / Raspberry Pi

Install the async-free client: pip install "python-socketio[client]"

"""
SensorCast subscriber — Python / Raspberry Pi
pip install "python-socketio[client]" requests
"""
import time
import socketio

SENSORCAST_HOST = "https://api.sensorcast.app"
USERNAME        = "alice"      # SensorCast username of the publisher

sio = socketio.Client()

@sio.event
def connect():
    print("[SC] Socket connected, announcing subscriber role…")
    sio.emit("role", "subscriber")

@sio.on("connected")
def on_connected(data):
    print(f"[SC] Server accepted role: {data}")

@sio.on("frame")
def on_frame(data):
    print(f"[FRAME] {data}")

@sio.on("publisher_disconnected")
def on_pub_gone():
    print("[SC] Publisher disconnected — waiting…")

@sio.event
def disconnect():
    print("[SC] Disconnected from server")

def heartbeat_loop():
    """Keep-alive: send heartbeat every 20 s or get kicked after 30 s."""
    while True:
        time.sleep(20)
        if sio.connected:
            sio.emit("heartbeat")

import threading
threading.Thread(target=heartbeat_loop, daemon=True).start()

namespace = f"/stream/{USERNAME}"
sio.connect(SENSORCAST_HOST, namespaces=[namespace], socketio_path="/socket.io/")
sio.wait()

Private Streams

When a publisher marks their stream as private, subscribers must pass the stream key in the Socket.IO auth object at connection time. You can find the stream key in the SensorCast dashboard under Stream Settings.

# For private streams, pass streamKey in the auth dict:
sio.connect(
    SENSORCAST_HOST,
    namespaces=[namespace],
    socketio_path="/socket.io/",
    auth={"streamKey": "YOUR_STREAM_KEY"},
)

Frame Formats

The Android app lets the publisher choose one of four formats. Your subscriber code should handle whichever format the publisher selects — or agree on one in advance.

COMPACT
Smallest payload. Fields and values separated by colon; fields comma-separated.
Accelerometer;x,y,z:0.1230,-0.4560,9.8100
TIMESTAMP
Unix ms timestamp prepended before sensor name.
1707494400123;Accelerometer;x,y,z:0.1230,-0.4560,9.8100
CSV
Fully comma-separated. Easy to split with strtok on Arduino.
1707494400123,Accelerometer,x,y,z,0.1230,-0.4560,9.8100
JSON
Structured. Use ArduinoJson on ESP32 or json.loads() in Python.
{"sensor":"Accelerometer","type":1,"timestamp":1707494400123,"accuracy":3,"values":{"x":0.12,"y":-0.45,"z":9.81}}

Available Sensors

These sensors are available on most Android devices. Exact field names depend on the mapping configured in the Android app; defaults are shown below.

SensorFieldsUnit
Accelerometerx, y, zm/s²
Gyroscopex, y, zrad/s
Magnetometerx, y, zµT
Linear Accelerationx, y, zm/s²
Gravityx, y, zm/s²
GPSlatitude, longitude, altitude, speed, accuracy°, m, m/s
Lightluxlx
PressurepressurehPa
Temperaturetemperature°C
Humidityhumidity%
Proximitydistancecm
Step Counterstepscount
Heart Ratebpmbpm

Orientation Angles

Pitch, Roll, Yaw via Rotation Vector

Coming Soon

Euler angles (pitch / roll / yaw) derived from TYPE_ROTATION_VECTOR will be exposed as a dedicated sensor field in an upcoming release. Until then, use the Rotation Vector sensor and compute Euler angles client-side:

# Python — convert quaternion [x, y, z, w] to Euler angles
import math

def quat_to_euler(x, y, z, w):
    pitch = math.atan2(2*(w*x + y*z), 1 - 2*(x*x + y*y))
    roll  = math.asin(2*(w*y - z*x))
    yaw   = math.atan2(2*(w*z + x*y), 1 - 2*(y*y + z*z))
    return math.degrees(pitch), math.degrees(roll), math.degrees(yaw)

Connection Troubleshooting

Disconnected immediately after connecting

Why: You did not emit the role event within 5 seconds. The server enforces a strict 5 s timeout.

Fix: Emit ["role","subscriber"] (or sendEVENT as shown in the sketch) as the very first action inside your connect handler — before any delays.

Server emits error: "Stream not found"

Why: The username in the namespace does not match any registered SensorCast account.

Fix: Double-check the username spelling. The namespace is case-sensitive: /stream/Alice is different from /stream/alice.

Disconnected after 30 seconds of no data

Why: The zombie-connection detector removes subscribers that have not sent a heartbeat in 30 s.

Fix: Send a heartbeat event at least once every 20–25 seconds (the example code uses 20 s).

Server emits "publisher_disconnected" then closes socket

Why: The Android publisher left. The server closes all subscribers immediately.

Fix: Implement reconnect logic — listen for publisher_disconnected, wait a few seconds, then reconnect to the namespace.

error: "Invalid stream key for private stream"

Why: The stream is private and the key you passed is wrong or missing.

Fix: Copy the stream key from the SensorCast dashboard and pass it in the auth object on connect.

Frames arrive but the format is unexpected

Why: The publisher changed the frame format in the Android app while you were connected.

Fix: Detect the format at runtime: JSON starts with {, CSV is all commas, COMPACT uses ;.