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}