ESPectre API

September 12, 2026 ยท View on GitHub

This document owns the public ESPectre application contract carried by Direct HTTP, Server-Sent Events (SSE), and MQTT. Discovery is specified in DISCOVERY.md.

Contract principles

  • The application version is 1.0, and the Direct base path is /espectre/v1.
  • HTTP and MQTT share resource payloads, operation names, validation, and result codes. Transport framing and delivery policy stay outside resource payloads.
  • protocol_version appears only in capabilities and discovery metadata.
  • In application JSON, device_id appears only in device and multi-device discovery results. The established binary CSI records also carry it.
  • This contract replaces the former RPC and explicit CSI-session APIs. There are no legacy routes, commands, topics, or aliases.

Direct HTTP

Direct HTTP listens on TCP port 62587. A client starts with GET /espectre/v1/capabilities and uses the returned resources and operations catalogs instead of inferring support from the frontend name.

Support matrix

MethodPathNativeESPHomeMatterMicro
GET/health, /device, /capabilities, /sensing, /wifi, /diagnostics, /eventsyesyesyesyes
GET/wifi/access-points, /devices, /csiyesyesyesno
GET/mqtt, /otayesnonono
PATCH/device, /sensingyesyesyesno
PATCH/mqttyesnonono
POST/sensing/calibrationsyesyesyesyes
POST/wifi/scansyesyesyesno
POST/ota/checks, /ota/updatesyesnonono
PUT/wifi/bssidyesyesyesno
DELETE/wifi/bssidyesyesyesno
DELETE/wifi/credentials, /mqttyesnonono

Unsupported resources and method combinations return HTTP 404. Capability negotiation remains authoritative when a custom build disables an otherwise supported feature. Micro-ESPectre's request limits, SSE behavior, and transport policies are documented in its README.md.

Request and response framing

Successful GET requests return the resource object directly. A request body, when present, must be a JSON object. An empty body is equivalent to {}. C++ frontends accept at most 2,048 request bytes. Send Content-Type: application/json whenever the body is non-empty.

Mutations return a result object:

{"accepted":true,"code":"ok","message":"operation accepted","data":{}}

data is optional. Synchronous mutations use HTTP 200. Asynchronous and disruptive routes use HTTP 202 after the request has passed application dispatch. Validation, capability, and conflict errors instead use the status codes in Errors.

Resources

health

{"status":"ok","online":true,"uptime_s":42,"timestamp_ms":42000}

status is ok while the device is online and offline in an MQTT Last Will. uptime_s and timestamp_ms use the device's monotonic clock. Calibration and intentional CSI collection do not make health degraded.

device

{
  "device_id": "3cf79180d3a0aca4",
  "name": "Kitchen",
  "label": "Kitchen",
  "frontend": "native",
  "firmware": "3.0.0-rc1",
  "chip": "esp32c6",
  "csi_profile": "ht20"
}

device_id is the stable 16-character lowercase hexadecimal identity. label is the configured label and may be empty. When it is empty, name contains the generated stable display name. csi_profile, when available, is the runtime-selected read-only profile: lltf20, ht20, or vht20.

capabilities

capabilities contains:

FieldTypeMeaning
protocol_versionstringApplication contract version, currently 1.0
resourcesarray of stringsResource names accepted by GET, without the base path
operationsarray of objectsSupported operations as {name, method, path}
eventsarray of stringsEvent names published by this frontend
features.csibooleanWhether GET /csi is available
csiobject, optionalCSI endpoint, framing versions, queue parameters, and external traffic settings

The CSI feature is named csi. Clients must tolerate additive resources, operations, events, feature fields, and CSI parameters.

sensing

FieldTypeMeaning
enabledbooleanDesired sensing state
readybooleanDetector output may be consumed: calibration is complete, the detector window is valid, and admitted input is newer than one detector window
calibratingbooleanCalibration is active
modestringsensing or csi_collection
derived_events_pausedbooleanMotion and other derived events are suspended
detectorstringlightweight or high_accuracy
thresholdnumberCurrent detector threshold in [0.0, 1.0]
motion_on_hits, motion_off_hitsintegerConsecutive evaluations required for each state transition
csi_traffic_modestringinternal or external
traffic_generator_modestringping, dns, dns_tcp, or wifi_raw (experimental; see CSI.md)
csi_target_ppsintegerConfigured CSI traffic target in packets per second
csi_traffic_udp_portinteger, optionalExternal CSI traffic UDP port
csi_traffic_multicast_groupstring, optionalExternal CSI traffic multicast group

During CSI collection, mode is csi_collection, ready is false, and derived_events_paused is true.

wifi

The Direct resource contains configured, connected, ssid, bssid, band, channel, and rssi_dbm. band is 2g, 5g, or an empty string when unknown. Unavailable RSSI is null. Native also returns ip, apply_state, and apply_message; these fields are optional for other frontends. Station MAC addresses and stored credentials are never returned.

The retained MQTT wifi payload is redacted. It contains configured, connected, band, channel, rssi_dbm, apply_state, and apply_message, but omits SSID, BSSID, and IP address.

wifi/access-points

{
  "scanning": false,
  "message": "scan complete",
  "access_points": [
    {"bssid":"aa:bb:cc:dd:ee:ff","rssi_dbm":-51,"channel":10}
  ]
}

access_points contains the latest completed scan. Starting a later scan sets scanning and updates message; clients poll this resource for completion.

mqtt

Native returns configured, scheme, host, port, username_configured, and topic_prefix. configured is true only when the scheme, host, and port form a valid endpoint. The password is write-only, and the username is represented only by username_configured.

ota

OTA is a frontend-owned extension of the canonical message model. Native registers its resource, operations, event, and parameter validation through the same extension catalog for Direct HTTP and MQTT. The sensing SDK does not implement firmware updates.

FieldTypeMeaning
statestringidle, checking, update_available, up_to_date, downloading, applying, reboot_scheduled, error, or unknown
timestamp_msintegerMonotonic snapshot timestamp
busybooleanAn OTA check or update is running
update_availablebooleanThe latest completed check found a newer image
current_version, target_versionstringRunning and candidate firmware versions
manifest_url, image_urlstringResolved release artifacts; empty before resolution
default_channel, channelstringBuild default and channel used by the current or latest attempt
messagestringCurrent status or failure detail

diagnostics

GET /diagnostics without fields, or with fields: [], returns a catalog: {"fields":[{"name":"traffic_tx_pps","type":"number","unit":"pps"}, ...]}. Names and metadata come from the canonical registry in src/cpp/runtime/diagnostic_fields.h; the catalog is limited to the frontend's diagnostic surface and does not read measurement values. A supported measurement can still be unavailable, in which case selecting it returns null.

Pass {"fields":["traffic_tx_pps","csi_hw_error_total","direct_http.send_failures"]} to request values. Responses contain only the selected fields, plus timestamp_ms and uptime. Dotted paths select leaves while preserving nested response objects, and a group name such as raw_csi selects all its leaves. Duplicate and overlapping selections are emitted once. {"fields":["*"]} requests the complete snapshot; the wildcard must be the only selection. Unknown names, wrong types, and mixed wildcard selections are invalid parameters. Names outside the frontend catalog are also invalid parameters.

HTTP accepts the JSON parameter object in the GET body. Browsers use GET /espectre/v1/diagnostics?fields=%5B%22traffic_tx_pps%22%5D, where fields is a URL-encoded JSON array; a query cannot be combined with a body or additional query parameters. Body limits still apply to decoded query parameters, and the HTTP server also enforces its URI limit. Use a body for long selections. MQTT carries the same fields array at the top level of read_diagnostics and returns the same object inside commands/result.data. Diagnostics are not published through SSE or a retained MQTT topic.

Current clients request their fields directly, without fetching the catalog first. The Monitor requests its eight indicators, Device settings requests the Wi-Fi channel, the firmware benchmark requests its measurement and Direct transport fields, and CSI collection requests raw_csi. The generic CLI defaults to ["*"]; explicitly pass fields: [] to inspect the catalog. Continuous counters and performance windows retain their existing sampling behavior; selection controls value retrieval and serialization, not detector instrumentation.

Field groupMeaning
timestamp_ms, uptimeMonotonic timestamp in milliseconds and uptime in whole seconds
free_memory_kb, minimum_free_memory_kb, largest_free_memory_kbCurrent heap, cumulative low-water heap, and current largest free block
cpu_frequency_mhz, loop_time_msCPU frequency and the most recent frontend loop-body cost
performance_window_ready, performance_window_ms, runtime_load_percentAvailability, duration, and runtime-loop load for the latest complete performance window
loop_samples, loop_avg_us, loop_max_usRuntime loop timing for that window
detection_timing_supported, detection_samples, detection_sum_us, detection_avg_us, detection_min_us, detection_max_usDetector timing support and aggregates
traffic_packets_total, csi_callbacks_total, csi_provenance_rejected_total, csi_accepted_total, csi_admitted_total, csi_filtered_totalCumulative traffic and CSI pipeline counters
csi_rx_error_total, csi_rx_end_error_total, csi_invalid_estimate_total, csi_invalid_first_word_totalCumulative capture-quality rejections; one reason per rejected callback
csi_hw_error_totalCumulative hardware-quality rejections, aggregated on the device for the web Monitor
csi_hw_error_ppsHardware-quality rejection rate over the same elapsed interval as the other CSI rates
csi_sanitized_first_word_totalFrames whose hardware-invalid source pairs were zeroed; classic DC/+1 or centered guards, with the default turbulence band preserved
csi_pending_frame_drops_total, csi_missing_slots_total, csi_excess_total, csi_stale_total, csi_out_of_order_totalCumulative queue and temporal-admission drop counters
csi_occupancy_slots, csi_window_slots, csi_pending_frames, csi_pending_frame_capacityDetector-window and callback-queue occupancy
CSI and traffic fields ending in _pps, plus csi_occupancyCached packet rates and detector-window occupancy ratio
wifi_channel, wifi_rssi_dbmCurrent channel and RSSI; unavailable RSSI is null
runtime_motion_event_drops_totalRuntime-to-frontend motion publications overwritten in the bounded mailbox
task_stack_high_water_bytesNative frontend task stack headroom; null where unavailable
direct_httpSSE client and queue budgets plus connection, request, delivery, and dropped_motion_events counters
raw_csiCSI session state, drops, send backpressure, delivered records, and stream sequence
mqttNative MQTT connection, queue, outbox, drop, failure, and reconnect counters

Memory values use KiB. Timings use microseconds unless the field ends in _ms, and rates use packets per second. csi_occupancy is the valid fraction of the active detector window. Fields that depend on a complete performance window are null until that window is ready. A frontend may omit measurements and transport objects that it cannot provide; clients must not replace a missing value with zero.

The periodic sensing log labels the traffic, callback, accepted-packet, and hardware-rejection rates tx, cb, accepted, and hwerr. Rates use packets per second over the actual diagnostic interval. hwerr sums RX errors, RX end errors, invalid hardware estimates, and unsafe invalid first words; it includes background traffic and excludes other filtering reasons. occ is valid detector-window occupancy. Channel and RSSI use the same association diagnostics as the API. Missing values appear as --. The diagnostics resource also includes admitted rates, temporal-drop reasons, cumulative error counts, and performance details.

Operations

Operations reject unknown fields. Routes described as taking no parameters accept an empty body or {} only.

Device update

PATCH /device accepts:

{"label":"Kitchen"}

label is required, must be a single-line string, and may contain at most 32 encoded bytes. An empty string clears the label. Success returns HTTP 200 and publishes the updated device resource.

Sensing update and calibration

PATCH /sensing accepts any non-empty supported subset of these fields:

FieldType and constraint
enabledboolean
detectorlightweight or high_accuracy
thresholdfinite number in [0.0, 1.0]
motion_on_hits, motion_off_hitsintegers from 1 through 20; both must be present together
csi_traffic_modeinternal or external
traffic_generator_modeping, dns, dns_tcp, or wifi_raw (experimental; see CSI.md)

The request is parsed and all field constraints and capabilities are checked before changes are applied. Success returns HTTP 200 and publishes sensing. POST /sensing/calibrations takes no parameters, returns HTTP 202 when queued, and returns 409 with code busy when calibration is already active.

Wi-Fi scan and BSSID selection

POST /wifi/scans takes no parameters and returns HTTP 202 when the scan starts. Read progress and results from GET /wifi/access-points.

PUT /wifi/bssid accepts:

{"bssid":"aa:bb:cc:dd:ee:ff","force":false}

bssid is required and must contain six hexadecimal octets separated by colons. force is an optional boolean and defaults to false. If the requested BSSID is already active, the normal request persists the selection without forcing a link transition; force: true exercises the disconnect and reconnect lifecycle.

DELETE /wifi/bssid clears the pin. DELETE /wifi/credentials, available only on Native, removes provisioned Wi-Fi credentials and returns the device to provisioning. Both DELETE routes take no parameters.

The three disruptive Wi-Fi mutations return HTTP 202 before any disconnect. Their result data contains the active BSSID observed before apply:

{"accepted":true,"code":"ok","message":"Wi-Fi BSSID update accepted","data":{"current_bssid":"11:22:33:44:55:66"}}

MQTT configuration

PATCH /mqtt accepts:

{
  "scheme": "mqtts",
  "host": "broker.example.net",
  "port": 8883,
  "username": "sensor",
  "password": "secret",
  "topic_prefix": "espectre/v1/devices"
}

scheme, host, and port are required on every request. scheme accepts lowercase mqtt or mqtts. host is a DNS hostname, IPv4 address, or unbracketed IPv6 address without a scheme, user information, port, path, query, fragment, whitespace, or control characters. port is an integer from 1 through 65535.

username, password, and topic_prefix are optional single-line strings with maximum lengths of 128, 256, and 128 bytes. An omitted optional field keeps its stored value. An empty topic_prefix restores espectre/v1/devices. Success returns HTTP 200. DELETE /mqtt takes no parameters, clears the broker endpoint and credentials, and returns HTTP 200.

mqtt selects explicit plaintext TCP for a trusted local broker. mqtts uses the public certificate bundle and verifies the broker hostname. WebSocket transports are not supported.

OTA actions

POST /ota/checks and POST /ota/updates accept an optional channel:

{"channel":"release"}

channel is release, preview, or develop. Omitting it uses the firmware build's default channel. Client-supplied manifest_url, image_url, and version values are rejected, as are all other unknown fields. A started check or update returns HTTP 202; an already active OTA operation returns 409 with code busy. OTA state changes are published as ota events and retained on the MQTT ota topic.

Events

GET /espectre/v1/events is the only JSON SSE connection. The C++ frontends publish health, device, sensing, wifi, ota, motion, and fault according to their capability catalog. Resource events carry complete snapshots. MQTT configuration, diagnostics, discovery results, and CSI never appear in this stream.

A motion event is produced for each detector evaluation:

{"timestamp_ms":42000,"state":"idle","score":0.0123}

state is idle or motion, and score is the detector output. Threshold and detector metadata remain in sensing. Under backpressure, replaceable motion events may be coalesced or dropped; C++ diagnostics expose the count as direct_http.dropped_motion_events. Future derived events, such as presence and gesture, use this event plane.

A fault event reports a runtime error without changing a resource payload:

{"timestamp_ms":42000,"message":"runtime fault"}

Each SSE connection receives a : heartbeat comment every 10 seconds. There is no replay. C++ frontends support at most two event clients. Persistent send failures close the affected stream.

CSI collection

GET /espectre/v1/csi opens the single exclusive binary CSI collection session. No setup request, bearer token, session deletion, or bind timeout exists. Closing the TCP response ends collection.

The C++ runtime requires sensing services to be armed before collection starts. If sensing is disabled or services are suspended for reconfiguration or maintenance, opening collection fails without re-enabling CSI capture or traffic generation. An accepted collection pauses derived sensing while retaining the active capture and traffic services.

Capture rejects hardware-invalid estimates before sensing and collection. CSI.md defines validation and normalization; the diagnostics above expose rejection counts and rates.

The public CSI record format is unchanged. Filtered streams may have fewer usable packets, including no usable packets if the hardware reports invalid estimates for the selected source. This is not a quiet measurement, and the runtime does not silently select another generator. Detailed hardware metadata is not added to collected datasets.

Each CSI V8 record retains the established 60-byte little-endian HTTP prefix. The client adopts the 16-byte session identifier from the first frame and rejects a change within the same connection. The producer preserves order; fixed-ring drops remain observable in the transport counters.

While CSI is active, sensing reports csi_collection, readiness is false, and motion plus all present or future derived events are paused on every transport. Control and resource events remain available. A second /csi request and sensing, Wi-Fi, or OTA mutations return 409. On close, the runtime restores its prior state, recalibrates when required, and resumes derived events only after readiness returns. The raw worker also detects a disconnected client when no CSI records are available; it closes the session without synthesizing records. When external traffic is configured, the host traffic generator must start before opening /csi.

External CSI traffic

In external mode, ESPHome, Native, and Matter accept UDP markers and unicast ICMP Echo Requests addressed to the device. UDP can use the device IP or csi_traffic_multicast_group, which defaults to 239.255.0.1. An empty multicast setting disables the group join while preserving unicast reception.

The UDP listener uses port 5555 and accepts only the exact four-byte UTF-8 marker "๐Ÿ‘ป".encode("utf-8") (F0 9F 91 BB). Unicast Echo Requests become CSI candidates, and the normal IP stack sends the replies. The external host owns pacing for either protocol. CLI.md covers host generation; CSI.md describes delivery limits.

Multicast sensing traffic may arrive with the group's multicast MAC or the device's unicast MAC when the access point converts multicast to unicast. Both delivery forms require the configured multicast destination IP, UDP port, and exact marker; frames addressed to another device are rejected.

MQTT

The base topic is espectre/v1/devices/{device_id} unless Native is configured with another topic_prefix.

SuffixRetainedPurpose
healthyesHealth, availability, and Last Will
deviceyesDevice identity and build
capabilitiesyesNegotiation and supported surface
sensingyesSensing state and tuning
wifiyesRedacted radio state
otayesOTA state
motionnoPer-evaluation motion event
faultnoRuntime fault
commands/resultnoCorrelated command result

There is no application heartbeat in addition to MQTT keepalive. The retained health topic is also the Home Assistant availability topic. The broker publishes the retained offline health Last Will after an ungraceful disconnect.

Publish commands to commands/request. Every request has a top-level command_id and command; parameters are also top-level. Neither protocol_version nor device_id is present.

{"command_id":"cmd-42","command":"update_sensing","threshold":0.5}

command_id is 1 to 64 characters and accepts ASCII letters, digits, _, -, ., and :. The complete command payload is limited to 2,048 bytes. Results echo command_id and command, then carry accepted, code, message, and optional data.

CommandParametersHTTP-equivalent validation
update_devicelabelPATCH /device
update_sensingAny non-empty supported sensing subsetPATCH /sensing
recalibratenonePOST /sensing/calibrations
read_diagnosticsoptional fields arrayGET /diagnostics; snapshot returned in data
check_otaoptional channelPOST /ota/checks
start_otaoptional channelPOST /ota/updates

There are no MQTT topics for MQTT configuration, diagnostics, discovery, or CSI. Home Assistant Discovery remains a separate adapter profile and uses health for availability.

Errors

Application results use application/json and the result object shown above. The C++ Direct dispatcher maps these stable codes as follows:

HTTP statusResult codesMeaning
400invalid_paramsThe JSON object, field set, type, or value is invalid
404unsupportedThe resource or operation is absent from the frontend capability set
409busy, conflict, busy_raw_collectionThe request conflicts with active calibration, discovery, CSI, or OTA work
200 or 202unavailable, internal_errorDispatch reached the operation, but its backend could not complete it; the route's synchronous or asynchronous status is retained

MQTT has no HTTP status; subscribers use accepted and code in commands/result. A command outside the published MQTT command set returns code forbidden.

The HTTP service can reject a request before application dispatch. These failures are not canonical result objects. C++ frontends return text/plain; charset=utf-8. Clients must inspect the HTTP status and Content-Type before parsing JSON.

HTTP statusPre-dispatch failure
400Malformed JSON, invalid route framing, or incomplete body
403Origin or transport policy rejected the request
404No route exists for the method and path
409The HTTP service detected a CSI collection conflict
413The body exceeds the frontend request limit
415A non-empty body does not declare JSON content
429The request or mutation rate limit was reached
503The service is stopping, its queue is full, or request handling cannot start

rate_limited is not an application result code. Rate limiting is a transport failure reported with HTTP 429 and counted in diagnostics.

Security and versioning

Direct HTTP is a trusted-LAN surface. Firmware enforces exact browser Origin allowlists, Private Network Access preflight, bounded bodies, queues, clients, and request rates. It binds to the station interface and does not expose stored Wi-Fi or MQTT passwords. /mqtt may gain independent protection only through an additive security extension.

During the 3.0.0 release-candidate phase, the application contract remains 1.0: Direct uses /espectre/v1, the default MQTT prefix is espectre/v1/devices, and DNS-SD advertises protovers=1.0. Diagnostic field selection changes within this pre-release contract: an unselected request returns a catalog, while fields: ["*"] returns all values. Use matching firmware and clients; there is no automatic fallback to the earlier diagnostics response. An explicitly configured MQTT prefix remains a user setting.

Clients negotiate once through capabilities.protocol_version. Compatible additions may add resources, fields, operations, or events within v1; consumers must ignore unknown additive fields. After the contract is released as stable, an incompatible change requires a new base-path major version and discovery protocol version.