Protocol implementation

These source listings are refreshed on every documentation build. The USB protocol and telemetry guide explain the wire formats and client behavior; the code guide links the other modules.

USB dispatcher

  1#include "serial_setup.h"
  2#include "device_settings.h"
  3#include "network.h"
  4#include "driver/uart.h"
  5#include "esp_system.h"
  6#include "esp_netif.h"
  7#include "esp_mac.h"
  8#include "esp_timer.h"
  9#include "freertos/FreeRTOS.h"
 10#include "freertos/task.h"
 11#include <stdio.h>
 12#include <string.h>
 13#include <stdlib.h>
 14#if CONFIG_IDF_TARGET_ESP32S3
 15#include "system_control.h"
 16#define BOARD "smartdb_s3"
 17#else
 18#define BOARD "esp32cam"
 19#endif
 20#define LINE_SIZE 4096
 21#define PREFIX "SMARTDB "
 22
 23static void reply(cJSON *j) {
 24    char *s = cJSON_PrintUnformatted(j);
 25
 26    if (s) {
 27        printf("\n" PREFIX "%s\n", s);
 28        fflush(stdout);
 29        free(s);
 30    }
 31
 32    cJSON_Delete(j);
 33}
 34
 35static void process(const char *line) {
 36    cJSON *request = cJSON_ParseWithOpts(line, NULL, true);
 37    const cJSON *id = cJSON_GetObjectItemCaseSensitive(request, "id");
 38    const char *op = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(request, "op"));
 39    cJSON *response = cJSON_CreateObject();
 40
 41    if (!response) {
 42        cJSON_Delete(request);
 43        return;
 44    }
 45
 46    if (cJSON_IsString(id)) {
 47        cJSON_AddStringToObject(response, "id", id->valuestring);
 48    }
 49
 50    esp_err_t e = ESP_ERR_INVALID_ARG;
 51    bool restart = false;
 52
 53    if (op && cJSON_IsString(id) && strlen(id->valuestring) <= 32) {
 54        const cJSON *data = cJSON_GetObjectItemCaseSensitive(request, "data");
 55
 56        if (!strcmp(op, "info")) {
 57            e = ESP_OK;
 58            cJSON_AddNumberToObject(response, "protocol", 1);
 59            cJSON_AddStringToObject(response, "board", BOARD);
 60            cJSON_AddNumberToObject(response, "uptime_ms", esp_timer_get_time() / 1000);
 61            uint8_t mac[6];
 62            char identity[18];
 63            esp_read_mac(mac, ESP_MAC_WIFI_STA);
 64            snprintf(
 65                identity,
 66                sizeof(identity),
 67                "%02x:%02x:%02x:%02x:%02x:%02x",
 68                mac[0],
 69                mac[1],
 70                mac[2],
 71                mac[3],
 72                mac[4],
 73                mac[5]
 74            );
 75            cJSON_AddStringToObject(response, "device_id", identity);
 76            cJSON_AddItemToObject(response, "settings", device_settings_public());
 77            esp_ip4_addr_t ip = {.addr = network_ipv4()};
 78            char address[16] = "";
 79
 80            if (ip.addr) {
 81                snprintf(address, sizeof(address), IPSTR, IP2STR(&ip));
 82            }
 83
 84            cJSON_AddStringToObject(response, "ip", address);
 85#if CONFIG_IDF_TARGET_ESP32S3
 86            system_add_status(response);
 87#endif
 88        } else if (!strcmp(op, "save_settings")) {
 89#if CONFIG_IDF_TARGET_ESP32S3
 90            system_request_open();
 91#endif
 92            e = device_settings_save(data);
 93
 94            if (e == ESP_OK) {
 95                cJSON_AddBoolToObject(response, "restart_required", true);
 96            }
 97        } else if (!strcmp(op, "restart")) {
 98#if CONFIG_IDF_TARGET_ESP32S3
 99            system_request_open();
100#endif
101            e = ESP_OK;
102            restart = true;
103#if CONFIG_IDF_TARGET_ESP32S3
104        } else if (!strcmp(op, "read_control")) {
105            e = ESP_OK;
106            cJSON_AddItemToObject(response, "config", system_config_json());
107        } else if (!strcmp(op, "write_control")) {
108            e = system_config_apply(data);
109        } else if (!strcmp(op, "open_relay")) {
110            system_request_open();
111            e = ESP_OK;
112#endif
113        }
114    }
115
116    cJSON_AddBoolToObject(response, "ok", e == ESP_OK);
117
118    if (e != ESP_OK) {
119        cJSON_AddStringToObject(response, "error", esp_err_to_name(e));
120    }
121
122    cJSON_Delete(request);
123    reply(response);
124
125    if (restart) {
126        vTaskDelay(pdMS_TO_TICKS(200));
127        esp_restart();
128    }
129}
130
131static void task(void *unused) {
132    (void)unused;
133    char *line = malloc(LINE_SIZE);
134
135    if (!line) {
136        vTaskDelete(NULL);
137        return;
138    }
139
140    size_t length = 0;
141    bool overflow = false;
142
143    for (;;) {
144        char c;
145
146        if (uart_read_bytes(CONFIG_ESP_CONSOLE_UART_NUM, &c, 1, pdMS_TO_TICKS(100)) != 1) {
147            continue;
148        }
149
150        if (c == '\n') {
151            line[length] = 0;
152
153            if (!overflow && !strncmp(line, PREFIX, strlen(PREFIX))) {
154                process(line + strlen(PREFIX));
155            }
156
157            memset(line, 0, length);
158            length = 0;
159            overflow = false;
160        } else if (c != '\r') {
161            if (length < LINE_SIZE - 1) {
162                line[length++] = c;
163            } else {
164                overflow = true;
165            }
166        }
167    }
168}
169
170esp_err_t serial_setup_start(void) {
171    esp_err_t e = uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, 8192, 0, 0, NULL, 0);
172
173    if (e != ESP_OK) {
174        return e;
175    }
176
177    return xTaskCreate(task, "usb_setup", 12288, NULL, 2, NULL) == pdPASS ? ESP_OK : ESP_ERR_NO_MEM;
178}

HTTPS telemetry worker

  1#include "device_settings.h"
  2#include "telemetry.h"
  3#include "cloud_control.h"
  4#include "project_config.h"
  5#include "esp_http_client.h"
  6#include "esp_crt_bundle.h"
  7#include "esp_netif_sntp.h"
  8#include "esp_log.h"
  9#include "esp_timer.h"
 10#include "freertos/FreeRTOS.h"
 11#include "freertos/task.h"
 12#include "freertos/queue.h"
 13#include <stdlib.h>
 14#include <string.h>
 15#include <stdio.h>
 16static telemetry_snapshot_fn snapshot;
 17static telemetry_frame_fn frame_snapshot;
 18static QueueHandle_t pending;
 19static TaskHandle_t collector;
 20
 21typedef struct {
 22    char body[2048];
 23    size_t length;
 24    bool overflow;
 25} response_t;
 26
 27static esp_err_t response_event(esp_http_client_event_t *event) {
 28    response_t *r = event->user_data;
 29
 30    if (r && event->event_id == HTTP_EVENT_ON_DATA && event->data_len > 0) {
 31        if ((size_t)event->data_len >= sizeof(r->body) - r->length) {
 32            r->overflow = true;
 33        } else if (!r->overflow) {
 34            memcpy(r->body + r->length, event->data, event->data_len);
 35            r->length += event->data_len;
 36            r->body[r->length] = 0;
 37        }
 38    }
 39
 40    return ESP_OK;
 41}
 42
 43static void handle_command(const response_t *response) {
 44    if (response->overflow || !response->length) {
 45        return;
 46    }
 47
 48    cJSON *root = cJSON_ParseWithOpts(response->body, NULL, true);
 49    const cJSON *command = cJSON_GetObjectItemCaseSensitive(root, "command");
 50
 51    if (!cJSON_IsObject(command)) {
 52        cJSON_Delete(root);
 53        return;
 54    }
 55
 56    const char *url = device_settings()->telemetry_url;
 57    size_t length = strlen(url);
 58    const char suffix[] = "/telemetry";
 59
 60    if (length < sizeof(suffix) - 1 || strcmp(url + length - (sizeof(suffix) - 1), suffix)) {
 61        cJSON_Delete(root);
 62        return;
 63    }
 64
 65    uint64_t id = 0;
 66    bool fresh = false;
 67    esp_err_t result = cloud_control_apply(command, &id, &fresh);
 68    cJSON_Delete(root);
 69
 70    if (!id) {
 71        return;
 72    }
 73
 74    if (fresh && collector) {
 75        xTaskNotifyGive(collector);
 76    }
 77
 78    char endpoint[320], body[192], token[768];
 79    snprintf(
 80        endpoint,
 81        sizeof(endpoint),
 82        "%.*s/commands/%llu/ack",
 83        (int)(length - (sizeof(suffix) - 1)),
 84        url,
 85        (unsigned long long)id
 86    );
 87    snprintf(
 88        body,
 89        sizeof(body),
 90        "{\"status\":\"%s\",\"detail\":\"%s\"}",
 91        result == ESP_OK ? "success" : "failed",
 92        esp_err_to_name(result)
 93    );
 94    snprintf(token, sizeof(token), "Bearer %s", device_settings()->telemetry_token);
 95    esp_http_client_config_t cfg = {
 96        .url = endpoint,
 97        .method = HTTP_METHOD_POST,
 98        .timeout_ms = 3000,
 99        .crt_bundle_attach = esp_crt_bundle_attach,
100        .disable_auto_redirect = true
101    };
102    esp_http_client_handle_t client = esp_http_client_init(&cfg);
103
104    if (!client) {
105        return;
106    }
107
108    esp_http_client_set_header(client, "Authorization", token);
109    esp_http_client_set_header(client, "Content-Type", "application/json");
110    esp_http_client_set_post_field(client, body, strlen(body));
111    esp_err_t e = esp_http_client_perform(client);
112    int status = esp_http_client_get_status_code(client);
113    esp_http_client_cleanup(client);
114
115    if (e != ESP_OK || status != 204) {
116        ESP_LOGW("telemetry", "Command acknowledgement not accepted; server may redeliver");
117    }
118}
119
120static void collect(void *arg) {
121    (void)arg;
122
123    for (;;) {
124        char *json = snapshot();
125
126        if (json && xQueueSend(pending, &json, 0) != pdTRUE) {
127            char *old = NULL;
128
129            if (xQueueReceive(pending, &old, 0) == pdTRUE) {
130                free(old);
131            }
132
133            if (xQueueSend(pending, &json, 0) != pdTRUE) {
134                free(json);
135            }
136        }
137
138        ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(cloud_control_interval() * 1000));
139    }
140}
141
142static void upload_frame(void) {
143    static int64_t last_attempt;
144    int64_t now = esp_timer_get_time();
145
146    if (!frame_snapshot || now - last_attempt < 30000000) {
147        return;
148    }
149
150    last_attempt = now;
151    const char *url = device_settings()->telemetry_url;
152    size_t length = strlen(url);
153    const char suffix[] = "/telemetry";
154
155    if (length < sizeof(suffix) - 1 || strcmp(url + length - (sizeof(suffix) - 1), suffix)) {
156        return;
157    }
158
159    telemetry_frame_t frame = frame_snapshot();
160
161    if (!frame.bytes) {
162        return;
163    }
164
165    char endpoint[320], token[768], sequence[24], age[24];
166    snprintf(
167        endpoint, sizeof(endpoint), "%.*s/fusion/frame", (int)(length - (sizeof(suffix) - 1)), url
168    );
169    snprintf(token, sizeof(token), "Bearer %s", device_settings()->telemetry_token);
170    snprintf(sequence, sizeof(sequence), "%lu", (unsigned long)frame.sequence);
171    snprintf(age, sizeof(age), "%lu", (unsigned long)frame.age_ms);
172    esp_http_client_config_t cfg = {
173        .url = endpoint,
174        .method = HTTP_METHOD_POST,
175        .timeout_ms = 3000,
176        .crt_bundle_attach = esp_crt_bundle_attach,
177        .disable_auto_redirect = true
178    };
179    esp_http_client_handle_t client = esp_http_client_init(&cfg);
180
181    if (client) {
182        esp_http_client_set_header(client, "Authorization", token);
183        esp_http_client_set_header(client, "Content-Type", "image/bmp");
184        esp_http_client_set_header(client, "X-Fusion-Sequence", sequence);
185        esp_http_client_set_header(client, "X-Capture-Age-Ms", age);
186        esp_http_client_set_header(client, "X-Synthetic", frame.synthetic ? "true" : "false");
187        esp_http_client_set_post_field(client, (const char *)frame.bytes, frame.size);
188        esp_err_t e = esp_http_client_perform(client);
189        int status = esp_http_client_get_status_code(client);
190        esp_http_client_cleanup(client);
191
192        if (e != ESP_OK || status != 202) {
193            ESP_LOGW("telemetry", "Preview upload unavailable (HTTP %d)", status);
194        }
195    }
196
197    free(frame.bytes); /* Preview is best-effort; never retain stale images for retry. */
198}
199
200static void upload(void *arg) {
201    (void)arg;
202    char *json = NULL;
203    unsigned backoff = 5;
204
205    for (;;) {
206        if (!json) {
207            xQueueReceive(pending, &json, portMAX_DELAY);
208        }
209
210        response_t response = {0};
211        esp_http_client_config_t cfg = {
212            .url = device_settings()->telemetry_url,
213            .method = HTTP_METHOD_POST,
214            .timeout_ms = 3000,
215            .crt_bundle_attach = esp_crt_bundle_attach,
216            .disable_auto_redirect = true,
217            .buffer_size = 1024,
218            .event_handler = response_event,
219            .user_data = &response
220        };
221        esp_http_client_handle_t c = esp_http_client_init(&cfg);
222        int status = 0;
223        esp_err_t e = ESP_FAIL;
224
225        if (c) {
226            char token[768];
227            snprintf(token, sizeof(token), "Bearer %s", device_settings()->telemetry_token);
228            esp_http_client_set_header(c, "Authorization", token);
229            esp_http_client_set_header(c, "Content-Type", "application/json");
230            esp_http_client_set_post_field(c, json, strlen(json));
231            e = esp_http_client_perform(c);
232            status = esp_http_client_get_status_code(c);
233            esp_http_client_cleanup(c);
234        }
235
236        if (e == ESP_OK && status == 202) {
237            free(json);
238            json = NULL;
239            backoff = 5;
240            handle_command(&response);
241            upload_frame();
242        } else {
243            ESP_LOGW(
244                "telemetry", "Upload not accepted (HTTP %d); retry in %u seconds", status, backoff
245            );
246            vTaskDelay(pdMS_TO_TICKS(backoff * 1000));
247
248            if (backoff < 60) {
249                backoff = backoff * 2 > 60 ? 60 : backoff * 2;
250            }
251        }
252    }
253}
254
255void telemetry_start(telemetry_snapshot_fn fn, telemetry_frame_fn frame) {
256    snapshot = fn;
257    frame_snapshot = frame;
258
259    if (cloud_control_init() != ESP_OK) {
260        ESP_LOGW("telemetry", "Cloud command storage unavailable; uploads remain available");
261    }
262
263    esp_sntp_config_t ntp = ESP_NETIF_SNTP_DEFAULT_CONFIG(SMARTDB_SNTP_SERVER);
264
265    if (esp_netif_sntp_init(&ntp) != ESP_OK) {
266        ESP_LOGW("telemetry", "SNTP unavailable; timestamp stays unknown");
267    }
268
269    if (!device_settings()->telemetry_url[0]) {
270        ESP_LOGI("telemetry", "Uploads disabled; configure the Postman HTTPS telemetry URL");
271        return;
272    }
273
274    size_t token_len = strlen(device_settings()->telemetry_token);
275
276    if (strncmp(device_settings()->telemetry_url, "https://", 8) || token_len < 1 ||
277        token_len > 700) {
278        ESP_LOGE("telemetry", "HTTPS endpoint and bearer token required");
279        return;
280    }
281
282    for (size_t i = 0; i < token_len; i++) {
283        if (device_settings()->telemetry_token[i] < 33 ||
284            device_settings()->telemetry_token[i] > 126) {
285            ESP_LOGE("telemetry", "Invalid token characters");
286            return;
287        }
288    }
289
290    pending = xQueueCreate(12, sizeof(char *));
291
292    if (!pending) {
293        return;
294    }
295
296    if (xTaskCreate(collect, "telemetry_collect", 8192, NULL, 2, &collector) != pdPASS) {
297        return;
298    }
299
300    if (xTaskCreate(upload, "telemetry_upload", 12288, NULL, 2, NULL) != pdPASS) {
301        ESP_LOGE("telemetry", "Uploader allocation failed");
302    }
303}