API - Real time
API - Real time
This page details how to consume Phyling real-time streams: opening the Socket.IO connection, subscribing to the available rooms, selecting telemetry signals and running RPC commands.
Required REST snapshot
Before opening the socket, build a complete snapshot of each device through the settings route. This reference tells you which modules are available, the RPC commands, the real-time keys, and lets you apply the patches received in the stream correctly.
Retrieve device settings
GET /devices/rt/{client_id}/{device_number}/settings
Authorization: Bearer <access_token> || ApiKey <key>The response serves as the reference snapshot for the device. It contains:
- Global metadata (
battery,state,version,timezone,sport, etc.). epoch: the device's current Unix timestamp, in seconds (float, millisecond precision) — same time base as theepochcolumn of the measurement and indicator streams.stream_outside_record: boolean indicating that the device broadcasts a real-time stream outside of a recording (real-time keys are then exposed even when no record is in progress).mqtt_refresh_delay: interval (in seconds) between two MQTT wake-ups of the device.0for always-connected devices (Maxi); positive for power-saving Phyling-LTE devices. An RPC command can take up to this delay to reach the device (see §4).disconnect_timeout: delay (in seconds) without news from the device after which it is considered disconnected.available_connectivity: list of connectivity types supported by the device (wifi,lte).network_infos: device network information;has_roamingindicates whether multi-access-point WiFi is active (a prerequisite forv1.board.wifi.connect, see §4.2).- The attachments (
client,group,user) with their identifiers. - A summary of the config modules with some information about them.
- The
featuresdictionary listing all available RPC commands (see §4). - The real-time keys (when available) via
realtime_data_keysandrealtime_indicator_keys.
Simplified excerpt:
{
"number": 10300378,
"name": "Maxi 378",
"state": "record",
"is_connected": true,
"battery": 39,
"timezone": "Europe/Paris",
"epoch": 1762441207.142,
"stream_outside_record": false,
"mqtt_refresh_delay": 0,
"disconnect_timeout": 30,
"available_connectivity": ["wifi", "lte"],
"network_infos": { "has_roaming": false },
"recTime": 23.142,
"selectionStartTime": 0,
"startRecTimeSinceEpoch": 0,
"deviceRecId": 42,
"features": {
"record": { "cmd_start": "v1.record.rec.start", "cmd_stop": "v1.record.rec.stop" },
"restart": { "cmd": "v1.board.restart" },
"...": "..."
},
"modules": {
"gps": { "id": 1, "is_connected": false, "realtime": false, "type": "gps" },
"imu": { "id": 2, "is_connected": true, "realtime": true, "type": "imu" }
},
"realtime_data_keys": [
{ "name": "gps.gpstimeUs", "label": "gps.gpstimeUs", "description": "", "unit": "us", "precision": 3, "yRange": [] },
{ "name": "gps.gpsTimeAccuracyNs", "label": "gps.gpsTimeAccuracyNs", "description": "", "unit": "ns", "precision": 3, "yRange": [] },
{ "...": "..." }
],
"realtime_indicator_keys": [
{ "unit": { "fr": "cpm", "en": "spm" }, "description": { "fr": "Cadence", "en": "Cadence" }, "precision": 1, "enabled": true, "name": "cadence", "type": "number" },
{ "...": "..." }
],
"client": { "id": 2, "name": "phyling" },
"group": { "id": 1, "name": "phyling" },
"user": { "active": true, "firstname": "<firstname>", "id": 42, "lastname": "<lastname>", "mail": "<mail>" },
"sport": { "display_name": "Aviron", "name": "aviron" },
"version": "v7.0.1",
"...": "..."
}Merge settings and status
The Socket.IO events app/device/{device}/board/status emit partial patches that reuse the same structure as settings. After each patch, perform a deep merge with your local snapshot to keep the state up to date (battery, connected modules, new real-time keys, etc.). The realtime.html and single-device.html examples provided in this repository contain a reusable deepMerge function if needed.
1. Socket.IO connection
- Derive the Socket.IO URL from the REST API URL: same host/port,
ws://when the API is on HTTP,wss://otherwise.
const apiBase = 'https://api.app.phyling.fr';
const socketBase = 'wss://api.app.phyling.fr';
const socket = io(socketBase, { transports: ['websocket'] });- Wait for the
connectevent to make sure the socket is ready. - Emit
subscribefor each room to listen to, including the access token.
socket.emit('subscribe', {
authorization: `Bearer ${accessToken}`,
room: `app/client/${clientId}/device/list_connected`
});- On
disconnect, restart the connection then re-emit the active subscriptions. - To leave a room, send
socket.emit('unsubscribe', { room }).
🔐 Each
subscribemust include{ token: <access_token>, room: <room> }. Without a valid token, the socket rejects the subscription.
2. Available rooms
| Room | Description | Received payload |
|---|---|---|
app/client/{client_id}/device/list_connectedReplies on app/client/device/list_connected | Complete list of a client's devices with their connection state. | Array of { number, name, is_connected, state, ... } objects to detect (dis)connections and initialize your stores. |
app/device/{device_number}/ind/json/allReplies on app/device/ind/json/all | Snapshot of the computed indicators for a device. | Object { number, recTime, epoch, indicators: { ... } } usable for monitoring (see §2.1). |
app/device/{device_number}/board/statusReplies on app/device/board/status | State patches to merge with settings. | Updates to battery, state, modules, real-time keys, epoch, etc. |
app/device/{device_number}/data/json/allReplies on app/device/data/json/all | Real-time stream of the selected measurements. | Object { number, recTime, data: { module: { T:[], epoch:[], <signal>:[] } } } (see §2.1). |
2.1. Measurement and indicator payload format
Indicators — app/device/ind/json/all
The payload carries a global epoch (at the same level as number and recTime) and the indicators dictionary (last known value of each indicator).
recTime: time elapsed since the start of the record, in seconds.epoch: corresponding absolute Unix timestamp, in seconds (float, millisecond precision).indicators:{ <name>: { "x": <time>, "y": <value> } }. Each indicator provides itsx(time) /y(value) point.
{
"number": 10300378,
"recTime": 12.345,
"epoch": 1762441219.345,
"indicators": {
"speed": { "x": 12.345, "y": 28.9 },
"power": { "x": 12.345, "y": 410.0 }
}
}Measurements — app/device/data/json/all
The payload groups measurements by module. In addition to its signals, each module provides two time columns aligned sample by sample with the signals:
T: time since the start of the record, in seconds (axis local to the record).epoch: absolute Unix timestamp of each sample, in seconds (lets you realign the stream on an absolute clock, useful for synchronizing several devices or an outside-record stream).
All columns of a given module (T, epoch and the signals) have the same length: index i of each array corresponds to the same sample.
{
"number": 10300378,
"recTime": 12.345,
"data": {
"imu": {
"T": [12.301, 12.311, 12.321],
"epoch": [1762441219.301, 1762441219.311, 1762441219.321],
"acc_x": [0.10, 0.12, 0.09],
"acc_y": [-0.01, 0.00, 0.02],
"acc_z": [9.79, 9.80, 9.81]
}
},
"selections": [
{ "num": 1, "start": 5.0, "stop": 9.5 },
{ "num": 2, "start": 11.0, "stop": -1 }
]
}selections lists the selections (SelectionRT) of the current record or those stopped less than 30 s ago. Each entry carries its number (num) and its bounds in seconds since the start of the record (start, stop); a negative stop indicates a selection still in progress.
The root-level
recTimereflects the progress of the record at the time of sending; to position each point precisely, use theT(orepoch) column of the relevant module.
3. Select the broadcast signals
Before receiving app/device/{device}/data/json/all, send the desired selection through the dedicated REST route.
POST /devices/rt/{client_id}/{device_number}/realtime
Authorization: Bearer <access_token>
Content-Type: application/json
{
"realtime_data_keys": [
"imu.acc_x",
"imu.acc_y",
"imu.acc_z"
]
}You can also enable entire modules:
POST /devices/rt/{client_id}/{device_number}/realtime
Authorization: Bearer <access_token>
Content-Type: application/json
{
"modules": ["imu"]
}- Refer to the names listed in
realtime_data_keys(settings route or latest status). - Sending an empty list immediately stops the real-time stream for this device.
- Repeat the selection periodically (~30 s) to keep the broadcast active.
- Activation is done at module level: requesting
imu.acc_xactivates the wholeimumodule. If another client activatesgps, everyone receives both imu and gps.
4. Run commands (RPC)
Device commands are asynchronous and go through two routes: the command is sent via /rpc/request (which replies immediately), then the result is retrieved by polling /rpc/response.
1. Send the command:
POST /devices/rt/{client_id}/{device_number}/rpc/request
Authorization: Bearer <access_token>
{
"method": "v1.record.rec.start",
"params": {}
}methodmust match a value listed insettings.features. Example:settings.features.record.cmd_startcontains the value"v1.record.rec.start"to use inmethod.feature/cmd_typeare an alternative syntax:{ "feature": "record", "cmd_type": "cmd_start" }is equivalent to{ "method": "v1.record.rec.start" }.
The route always replies immediately:
201: the command is being processed. The response contains the requestid, to be used to retrieve the result.{ "message": "Command is being processed", "id": "3f2a1c9e-1b2c-4d5e-8f90-abcdef123456" }200: the command has an immediate result (notification or cached value) — no polling needed.
2. Retrieve the result:
POST /devices/rt/{client_id}/{device_number}/rpc/response
Authorization: Bearer <access_token>
{
"id": "3f2a1c9e-1b2c-4d5e-8f90-abcdef123456"
}202: the command is still being processed — try again later.200(or a mapped error code): the result is available. Polling is idempotent: the result remains available until it expires (a few minutes after the command timeout).404: unknown or expiredid.
In practice, poll /rpc/response once per second after the 201, until you get a code other than 202. Also watch app/device/{device}/board/status to see the state merged with your local settings.
⏱️ Execution delay (Phyling-LTE). On a power-saving device, the result can take up to
mqtt_refresh_delay + 4seconds to become available (the device only wakes up atmqtt_refresh_delayintervals, see §1). A command such asv1.record.rec.start/stopmay therefore return202for several seconds; beyond that,/rpc/responsereturns the error-32003(timeout). For always-connected devices (mqtt_refresh_delay = 0), the delay remains 4 s.
4.1 RPC error codes
Each RPC response can carry an error object instead of a result. The codes follow the JSON-RPC 2.0 specification and are extended by negative Phyling codes in the range -32000 to -32004.
| Code | Message | Meaning |
|---|---|---|
| Standard JSON-RPC errors | ||
-32700 | Parse error | Invalid JSON received by the server. |
-32600 | Invalid Request | The object sent is not a valid JSON-RPC request. |
-32601 | Method not found | The method does not exist or is not available. |
-32602 | Invalid params | Invalid parameter(s). |
-32603 | Internal error | Internal JSON-RPC error. |
| Phyling errors | ||
-32000 | Service unavailable | MQTT disconnected, device offline, shutting down, etc. |
-32001 | Record in progress | The RPC method cannot be executed during a recording, or (auto calibration) while the device is not in the idle state. |
-32002 | Not recording | The RPC method cannot be executed outside a recording. |
-32003 | Request timeout | The device's response never arrived within the allotted time. |
-32004 | Max size reached | The content exceeds the maximum accepted size. |
The error object may carry an optional data field, when the device has a usable detail to add to its error code — typically a reason string in snake_case, stable and intended for the calling code, not for display. An error without data remains the normal form: do not make it a condition.
4.2 Common RPCs
Shutdown / Restart — v1.board.shutdown · v1.board.restart
{ "method": "v1.board.shutdown" }
{ "method": "v1.board.restart" }Result:
{ "actions": ["shutdown"] }
{ "actions": ["restart"] }Possible errors: -32000 (service unavailable), -32001 (record in progress), -32003 (timeout).
A recording in progress blocks shutdown/restart. Stop the recording first via
v1.record.rec.stop.
Read a file — v1.board.<elem>.get
<elem> can be: config, calib, wifi, device_info.
{ "method": "v1.board.config.get" }Result:
{ "content": { /* JSON content of the file */ } }Possible errors: -32000, -32003.
Write a file — v1.board.<elem>.set
<elem> can be: config, calib, wifi, device_info.
{
"method": "v1.board.config.set",
"params": { "content": { /* complete JSON content */ } }
}Variants by element:
| Element | Params variant | Effect |
|---|---|---|
calib | { "merge_calib": { … } } | Partially merges with the existing calibration (module addition, factor update…) |
Result: { "actions": ["restart"] } (except for calib, which does not restart).
Possible errors: -32000, -32001 (record in progress), -32602 (invalid params), -32003.
Switch WiFi access point — v1.board.wifi.connect
📡 Phyling-LTE in multi-access-point WiFi mode only. Check
network_infos.has_roaming(see §1) before calling it; other firmwares do not declare this method and reply-32601.
Asks the sensor to leave its current access point immediately, without waiting for the usual conditions (gain margin, RSSI thresholds, delay between two switches) to be met. Useful to free a sensor stuck on a saturated access point, and to replay a roaming test without depending on radio conditions.
Three forms, depending on the parameters provided:
| Parameter | Effect |
|---|---|
| (none) | The sensor runs a full scan, then connects to the access point with the best RSSI. |
ssid | SSID of an enabled network in the device configuration. The sensor connects to the best other access point of this SSID present in its neighbor table. |
bssid | MAC address of an access point, in the format xx:xx:xx:xx:xx:xx. The sensor connects to it if it is present in its neighbor table. |
Providing both ssid and bssid is a parameter error.
In all three forms, already being on the requested access point is a success, not a refusal: the sensor does not move, no interruption occurs, and the response says so with changed: false. The question "am I on the right access point?" is therefore answered by the return code, and changed only says whether a move was needed to get there.
{ "method": "v1.board.wifi.connect" }
{ "method": "v1.board.wifi.connect", "params": { "ssid": "Phyling-Stade" } }
{ "method": "v1.board.wifi.connect", "params": { "bssid": "cf:b8:ed:3c:01:02" } }With ssid or bssid, the sensor does not run a scan to find the target: it only uses what its scans have already taught it. An access point that has never been seen is therefore refused, not attempted blindly. Without parameters, on the contrary, the scan is the very purpose of the call — the response then takes one to two seconds longer to arrive. In all cases the switch itself follows the firmware's normal path: it is the same behavior as the one triggered spontaneously by signal degradation.
Result:
{
"from_bssid": "cf:b8:ed:3e:01:02",
"from_rssi": -74,
"bssid": "cf:b8:ed:3c:01:02",
"rssi": -61,
"channel": 6,
"changed": true
}| Field | Description |
|---|---|
from_bssid / from_rssi | Access point left and its last measured RSSI (dBm). |
bssid / rssi | Target access point and its last measured RSSI (dBm). |
channel | Channel of the target access point; 0 if the sensor lets the supplicant search for it. |
changed | false = the requested access point was already the current one, no switch took place and the from_* fields repeat the access point that was kept. |
The response arrives as soon as the association request has been sent, not at the end of the switch: the link is then cut for a few seconds. Follow the recovery on the real-time telemetry fields exposed by board/status (ap_bssid, net_state, cut_ms, roam_count).
Possible errors:
Each refusal from the device carries an error.data.reason (see §4.1) that specifies which of the code's causes applies:
{
"code": -32000,
"message": "Service unavailable",
"data": { "reason": "target_not_seen", "current_bssid": "cf:b8:ed:3e:01:02" }
}| Code | reason | Cause |
|---|---|---|
-32601 | (none) | Firmware without roaming (method not declared). |
-32602 | ambiguous_target | ssid and bssid provided together. |
-32602 | malformed_bssid | bssid that is not in the format xx:xx:xx:xx:xx:xx. |
-32602 | malformed_target | ssid or bssid provided is not a string, or ssid exceeds the maximum accepted length. |
-32602 | ssid_not_configured | ssid that matches no enabled network in the configuration of the device. |
-32000 | target_not_seen | The target access point is not in the neighbor table (no scan has seen it). |
-32000 | not_associated | The sensor is not associated with any access point. |
-32000 | radio_busy | A switch or a scan is already in progress. |
-32000 | scan_timeout | The requested scan (call without parameters) did not complete. |
-32603 | switch_failed | The supplicant refused the association request; data.errno carries its code. |
-32003 | (none) | Server-side timeout (no response from the device). |
On target_not_seen and not_associated, data additionally carries current_bssid, the access point the sensor is on — absent if the link is cut and there is none.
🔎 Telling the two most common refusals apart.
ssid_not_configuredmeans that this network is not (or no longer) enabled in the device'swifi.json— it is a configuration problem.target_not_seenmeans that the target is properly configured but the sensor does not see it — it is a radio coverage problem.
List automatic calibrations — v1.board.calib.auto.get
📡 Phyling-LTE only. Other devices do not declare this command: check for the presence of the
calib_autokey infeatures(see §1) before calling it, otherwise the request replies-32601.
Returns the list of calibrations that the sensor can run by itself, without server intervention.
{ "method": "v1.board.calib.auto.get" }Result:
{
"calibrations": [
{
"module": "imu",
"name": "gyro",
"duration": 3,
"title": { "fr": "Calibration gyro", "en": "Gyro calibration" },
"description": { "fr": "Posez le capteur…", "en": "Place the sensor…" }
}
]
}| Field | Description |
|---|---|
module | Name of the module instance in the device configuration (not its type). To be reused as is in v1.board.calib.auto.start. |
name | Calibration identifier, to be reused as is. |
duration | Measurement duration in seconds, specific to each calibration. |
title | Short localized label (fr / en), to display in your interface. |
description | Localized instruction to follow before starting the calibration (sensor position, stillness…). |
Possible errors: -32000, -32003, -32603 (internal failure on the device side while listing calibrations).
Start an automatic calibration — v1.board.calib.auto.start
📡 Phyling-LTE only. Same remark as above about the
calib_autokey infeatures.
{
"method": "v1.board.calib.auto.start",
"params": {
"module": "imu",
"name": "gyro"
}
}module and name must come from v1.board.calib.auto.get.
Result:
{
"set_status": {
"state": "calibrating"
}
}The response arrives immediately, without waiting for the end of the measurement. The device then switches to the calibrating state (cyan LED) for duration seconds, then saves the result and returns to idle. Follow the progress on the state field of the device status.
Full sequence:
- The device must be in the
idlestate: a calibration cannot start during a recording. - Display the calibration
descriptionand wait until the user has placed the sensor. - Send the command. The sensor must not move until it has returned to
idle. - At the end, the new calibration is applied and published automatically;
v1.board.calib.getreturns the up-to-date version.
⚠️ If the sensor moved during the measurement, or if the result falls outside the expected ranges, the calibration is rejected: nothing is saved, the old calibration stays in place and the device returns to
idle. Try again on a stable surface.
Possible errors: -32602 (unknown module or calibration), -32000 (module disconnected), -32001 (device not in the idle state, or calibration already in progress), -32003.
Start a recording — v1.record.rec.start
{ "method": "v1.record.rec.start" }Result:
{
"set_status": {
"state": "record",
"maxiRecId": 42
}
}Possible errors: -32000, -32001 (recording already in progress), -32003.
Stop a recording — v1.record.rec.stop
{
"method": "v1.record.rec.stop",
"params": { "force_stop": false }
}force_stop: false(default): the device enters recovery mode (recovery_record) before stopping completely. Use this in the nominal case.force_stop: true: the device skips recovery mode and stops immediately.
Result:
{
"set_status": {
"state": "idle"
}
}or "state": "recovery_record" if the device is in recovery.
Possible errors: -32000, -32002 (no recording in progress), -32003.
5. Phyling-LTE specifics
Some Phyling-LTE devices (sensors connected over LTE-M) have particular characteristics when network connectivity resumes. These behaviors are built in by design and transparent to the user.
Old out-of-order data when the network resumes
During a prolonged network interruption (underground, tunnel, etc.), a Phyling-LTE device keeps recording measurements locally. On reconnection, the device sends this "historical" data back mixed with new data, and potentially out of chronological order because of MQTT buffering delays.
Example: a device offline for 5 minutes sends back data from t=100s to t=305s. On reconnection, the payloads received may arrive in an order that does not guarantee t=100 before t=300.
Implication for the API: clients consuming app/device/{device}/data/json/all may receive messages with decreasing epoch or T. Apply a freshness filter in your consuming code (e.g. ignore a sample if its timestamp is older than the last one received).
realtimeRate and realtimeSlowRate as nominal ceilings
The realtimeRate metadata (real-time broadcast rate, Hz) indicated in settings is a nominal ceiling, not a guarantee. In case of network congestion or data backlog, the Phyling-LTE device may dynamically reduce its sending rate to avoid saturating the link. This reduction is transparent and temporary: the rate returns to the ceiling as soon as the backlog is cleared.
Phyling-LTE devices also expose realtimeSlowRate: a reduced rate (Hz) used automatically when the link is degraded or the backlog is large. The device switches between realtimeRate and realtimeSlowRate without external intervention.
Implication: do not assume a fixed 1/realtimeRate rate between two messages. Always use the timestamps of the T or epoch columns to position data on your time axis; do not rely on a constant rate.
6. Additional resources
Ready-to-use examples are available in the Phyling open-source repository to quickly test the API without starting from scratch: RAW API examples.
- Phyling library and examples: github.com/phyling-sport/phyling — high-level Python client + complete examples (authentication, real time, RPC).
- RAW API examples: RAW API examples — direct calls to the REST API and Socket.IO with no third-party dependency (
minimal_oauth.py,minimal_apikey.html,realtime.html,single-device.html). - The Swagger documentation provided with the API lists all REST routes (login, refresh, devices, real time, RPC).