STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
ota_service_main.cpp
Go to the documentation of this file.
1/**
2 * OTA Service — Ethernet firmware flash for DiabloAvionics ESP32-S3 boards.
3 *
4 * Listens on TCP port 9997 for commands:
5 * OTA_FLASH:<board_ip>:<firmware_path> — flash existing .bin to board:3232
6 * OTA_BUILD_FLASH|board_ip|abs_project_dir|board_id
7 * — run `pio run` in abs_project_dir (board_id 1–254 sets
8 * PLATFORMIO_BUILD_FLAGS=-DTEMP_HARDCODE_BOARD_ID=N; 0 = no flag), then flash
9 * .pio/build/<env>/firmware.bin
10 *
11 * Replies: "OK\n" or "ERR:<reason>\n"
12 *
13 * Protocol (matches ota-flash.ts):
14 * 1. TCP connect to board_ip:3232
15 * 2. Send 4-byte firmware size (big-endian)
16 * 3. Send .bin in 4 KB chunks
17 * 4. Read "OK" in response before board reboots
18 *
19 * Usage: ./ota_service [--port PORT]
20 */
21
22#include <arpa/inet.h>
23#include <netinet/in.h>
24#include <signal.h>
25#include <sys/socket.h>
26#include <sys/wait.h>
27#include <unistd.h>
28
29#include <atomic>
30#include <cstdlib>
31#include <cstring>
32#include <fstream>
33#include <iostream>
34#include <sstream>
35#include <string>
36#include <thread>
37#include <vector>
38
39#include "config/Config.hpp"
40#include "net/DaqInterface.hpp"
41
42namespace {
43std::atomic<bool> g_running{true};
44
45void signalHandler(int /*sig*/) {
46 std::cout << "\n[OTAService] Shutting down..." << std::endl;
47 g_running = false;
48}
49
50std::string trim(const std::string& s) {
51 size_t a = s.find_first_not_of(" \t\r\n");
52 size_t b = s.find_last_not_of(" \t\r\n");
53 return (a == std::string::npos) ? "" : s.substr(a, b - a + 1);
54}
55
56// ── OTA Flash ────────────────────────────────────────────────────────────────
57
58static constexpr uint16_t OTA_PORT = 3232;
59
60// Local address OTA connections leave from, resolved from config at startup. "0.0.0.0" means the
61// host has no NIC on the board subnet and the kernel picks — see lib/include/net/DaqInterface.hpp.
62std::string g_bind_address = "0.0.0.0";
63static constexpr size_t CHUNK_SIZE = 4096;
64static constexpr int CONNECT_TIMEOUT_S = 5;
65static constexpr int TRANSFER_TIMEOUT_S = 60;
66
67bool flashFirmware(const std::string& ip, const std::string& bin_path, std::string& error) {
68 // Read firmware file
69 std::ifstream f(bin_path, std::ios::binary | std::ios::ate);
70 if (!f.is_open()) {
71 error = "cannot open file: " + bin_path;
72 return false;
73 }
74 std::streamsize file_size = f.tellg();
75 if (file_size <= 0 || file_size > 0x200000) {
76 error = "invalid firmware size: " + std::to_string(file_size) + " bytes";
77 return false;
78 }
79 f.seekg(0);
80 std::vector<uint8_t> firmware(static_cast<size_t>(file_size));
81 if (!f.read(reinterpret_cast<char*>(firmware.data()), file_size)) {
82 error = "failed to read firmware file";
83 return false;
84 }
85
86 // TCP connect to board:3232
87 int sock = socket(AF_INET, SOCK_STREAM, 0);
88 if (sock < 0) {
89 error = "socket() failed";
90 return false;
91 }
92
93 {
94 struct timeval tv{.tv_sec = CONNECT_TIMEOUT_S, .tv_usec = 0};
95 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
96 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
97 }
98
99 // Reach the board over the board NIC. Unlike the UDP senders this one fails loudly if it
100 // takes the wrong route (connect() times out), but "flash timed out" is a much worse
101 // diagnostic than not having taken the wrong route in the first place.
102 if (!fsw::net::bindToDaqInterface(sock, g_bind_address, "OTAService")) {
103 close(sock);
104 error = "cannot bind OTA socket to " + g_bind_address;
105 return false;
106 }
107
108 struct sockaddr_in addr{};
109 addr.sin_family = AF_INET;
110 addr.sin_port = htons(OTA_PORT);
111 if (inet_pton(AF_INET, ip.c_str(), &addr.sin_addr) != 1) {
112 close(sock);
113 error = "invalid IP: " + ip;
114 return false;
115 }
116 if (connect(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
117 close(sock);
118 error = "connect to " + ip + ":3232 failed";
119 return false;
120 }
121
122 // Set longer transfer timeout now that we're connected
123 {
124 struct timeval tv{.tv_sec = TRANSFER_TIMEOUT_S, .tv_usec = 0};
125 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
126 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
127 }
128
129 // Send 4-byte big-endian firmware size
130 uint32_t size_be = htonl(static_cast<uint32_t>(file_size));
131 if (send(sock, &size_be, 4, 0) != 4) {
132 close(sock);
133 error = "failed to send size header";
134 return false;
135 }
136
137 // Send firmware in 4 KB chunks
138 size_t sent = 0;
139 int last_pct = -1;
140 while (sent < firmware.size()) {
141 size_t chunk = std::min(CHUNK_SIZE, firmware.size() - sent);
142 ssize_t n = send(sock, firmware.data() + sent, chunk, 0);
143 if (n <= 0) {
144 close(sock);
145 error = "send failed at offset " + std::to_string(sent);
146 return false;
147 }
148 sent += static_cast<size_t>(n);
149 int pct = static_cast<int>((sent * 100) / firmware.size());
150 if (pct != last_pct) {
151 std::cout << "[OTAService] Flash " << ip << ": " << pct << "% (" << sent << "/"
152 << file_size << " bytes)" << std::endl;
153 last_pct = pct;
154 }
155 }
156
157 // Wait for "OK" response
158 char resp[64]{};
159 ssize_t n = recv(sock, resp, sizeof(resp) - 1, 0);
160 close(sock);
161
162 if (n <= 0) {
163 error = "no response from board (may have rebooted normally)";
164 // Not necessarily a failure — ESP32 sometimes reboots before sending OK
165 return true;
166 }
167 std::string resp_str = trim(std::string(resp, static_cast<size_t>(n)));
168 if (resp_str.find("OK") == std::string::npos) {
169 error = "unexpected response: " + resp_str;
170 return false;
171 }
172 return true;
173}
174
175// ── PlatformIO build (pio run) ───────────────────────────────────────────────
176
177std::string readFirstPioEnv(const std::string& iniPath) {
178 std::ifstream f(iniPath);
179 if (!f.is_open())
180 return "adafruit_feather_esp32s3";
181 std::string line;
182 while (std::getline(f, line)) {
183 if (line.size() > 6 && line.compare(0, 5, "[env:") == 0) {
184 size_t end = line.find(']');
185 if (end != std::string::npos && end > 5)
186 return trim(line.substr(5, end - 5));
187 }
188 }
189 return "adafruit_feather_esp32s3";
190}
191
192void sanitizeOneLine(std::string& s) {
193 for (char& c : s) {
194 if (c == '\n' || c == '\r')
195 c = ' ';
196 }
197 if (s.size() > 4000)
198 s.resize(4000);
199}
200
201/** Run pio in projectDir; capture combined stdout/stderr into buildLog. */
202bool runPioBuild(const std::string& projectDir, int boardId, std::string& buildLog,
203 std::string& error) {
204 const std::string ini = projectDir + "/platformio.ini";
205 std::ifstream check(ini);
206 if (!check.is_open()) {
207 error = "no platformio.ini in " + projectDir;
208 return false;
209 }
210 check.close();
211
212 int pipefd[2];
213 if (pipe(pipefd) < 0) {
214 error = "pipe() failed";
215 return false;
216 }
217
218 pid_t pid = fork();
219 if (pid < 0) {
220 close(pipefd[0]);
221 close(pipefd[1]);
222 error = "fork() failed";
223 return false;
224 }
225
226 if (pid == 0) {
227 close(pipefd[0]);
228 dup2(pipefd[1], STDOUT_FILENO);
229 dup2(pipefd[1], STDERR_FILENO);
230 close(pipefd[1]);
231
232 if (chdir(projectDir.c_str()) != 0) {
233 perror("chdir");
234 _exit(126);
235 }
236 if (boardId > 0 && boardId <= 254) {
237 const std::string fl = "-DTEMP_HARDCODE_BOARD_ID=" + std::to_string(boardId);
238 setenv("PLATFORMIO_BUILD_FLAGS", fl.c_str(), 1);
239 } else {
240 unsetenv("PLATFORMIO_BUILD_FLAGS");
241 }
242
243 execlp("pio", "pio", "run", (char*)nullptr);
244 execlp("platformio", "platformio", "run", (char*)nullptr);
245 _exit(127);
246 }
247
248 close(pipefd[1]);
249 buildLog.clear();
250 char buf[4096];
251 ssize_t n;
252 while ((n = read(pipefd[0], buf, sizeof(buf) - 1)) > 0 && buildLog.size() < 65536) {
253 buf[n] = '\0';
254 buildLog += buf;
255 }
256 close(pipefd[0]);
257
258 int st = 0;
259 if (waitpid(pid, &st, 0) < 0) {
260 error = "waitpid failed";
261 return false;
262 }
263 if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) {
264 int code = WIFEXITED(st) ? WEXITSTATUS(st) : -1;
265 error = "pio run failed (exit " + std::to_string(code) + "): " + buildLog;
266 sanitizeOneLine(error);
267 return false;
268 }
269 return true;
270}
271
272bool buildAndFlash(const std::string& ip, const std::string& projectDir, int boardId,
273 std::string& error, std::string& buildOutput) {
274 std::string blog;
275 if (!runPioBuild(projectDir, boardId, blog, error)) {
276 buildOutput = blog;
277 return false;
278 }
279 buildOutput = blog;
280 // Successful builds previously had no console output; print captured pio log before flash.
281 if (!blog.empty()) {
282 std::cout << "[OTAService] pio run output:\n" << blog;
283 if (blog.back() != '\n')
284 std::cout << '\n';
285 }
286 const std::string envName = readFirstPioEnv(projectDir + "/platformio.ini");
287 const std::string binPath = projectDir + "/.pio/build/" + envName + "/firmware.bin";
288 std::ifstream bf(binPath);
289 if (!bf.is_open()) {
290 error = "firmware.bin missing after build: " + binPath;
291 sanitizeOneLine(error);
292 return false;
293 }
294 bf.close();
295 return flashFirmware(ip, binPath, error);
296}
297
298// ── TCP command handler ───────────────────────────────────────────────────────
299
300void handleClient(int client_fd) {
301 struct timeval tv{.tv_sec = 30, .tv_usec = 0};
302 setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
303
304 auto sendReply = [&](const std::string& msg) {
305 send(client_fd, msg.c_str(), msg.size(), MSG_NOSIGNAL);
306 };
307
308 std::string buf;
309 buf.reserve(512);
310 char c;
311 while (g_running && recv(client_fd, &c, 1, 0) == 1) {
312 if (c == '\n')
313 break;
314 if (buf.size() < 8192)
315 buf += c;
316 }
317 const std::string cmd = trim(buf);
318
319 if (cmd.compare(0, 10, "OTA_FLASH:") == 0) {
320 const std::string rest = cmd.substr(10);
321 const size_t colon = rest.find(':');
322 if (colon == std::string::npos || colon == 0 || colon + 1 >= rest.size()) {
323 sendReply("ERR:bad OTA_FLASH format — use OTA_FLASH:<ip>:<path>\n");
324 } else {
325 const std::string ip = trim(rest.substr(0, colon));
326 const std::string path = trim(rest.substr(colon + 1));
327 std::cout << "[OTAService] Flashing " << ip << " with " << path << std::endl;
328 std::string err;
329 if (flashFirmware(ip, path, err)) {
330 std::cout << "[OTAService] Flash " << ip << " OK" << std::endl;
331 sendReply("OK\n");
332 } else {
333 std::cerr << "[OTAService] Flash " << ip << " failed: " << err << std::endl;
334 sanitizeOneLine(err);
335 sendReply("ERR:" + err + "\n");
336 }
337 }
338 } else if (cmd.compare(0, 16, "OTA_BUILD_FLASH|") == 0) {
339 // OTA_BUILD_FLASH|ip|abs_project_dir|board_id (path must not contain '|')
340 std::istringstream iss(cmd);
341 std::string token;
342 std::vector<std::string> parts;
343 while (std::getline(iss, token, '|')) {
344 parts.push_back(trim(token));
345 }
346 if (parts.size() != 4 || parts[0] != "OTA_BUILD_FLASH") {
347 sendReply(
348 "ERR:bad OTA_BUILD_FLASH — use OTA_BUILD_FLASH|ip|abs_project_dir|board_id\n");
349 } else {
350 const std::string& ip = parts[1];
351 const std::string& proj = parts[2];
352 int bid = std::atoi(parts[3].c_str());
353 if (ip.empty() || proj.empty()) {
354 sendReply("ERR:empty ip or project path\n");
355 } else {
356 std::cout << "[OTAService] Build+flash " << ip << " project " << proj
357 << " board_id " << bid << std::endl;
358 std::string err;
359 std::string buildOut;
360 if (buildAndFlash(ip, proj, bid, err, buildOut)) {
361 std::cout << "[OTAService] Build+flash " << ip << " OK" << std::endl;
362 sendReply("OK\n");
363 } else {
364 std::cerr << "[OTAService] Build+flash failed: " << err << std::endl;
365 sanitizeOneLine(err);
366 sendReply("ERR:" + err + "\n");
367 }
368 }
369 }
370 } else if (!cmd.empty()) {
371 sendReply("ERR:unknown command\n");
372 }
373
374 close(client_fd);
375}
376
377} // namespace
378
379int main(int argc, char* argv[]) {
380 uint16_t listen_port = 9997;
381 std::string config_path = "config/config.toml";
382
383 for (int i = 1; i < argc; ++i) {
384 std::string arg = argv[i];
385 if (arg == "--port" && i + 1 < argc) {
386 listen_port = static_cast<uint16_t>(std::atoi(argv[++i]));
387 } else if (arg == "--config" && i + 1 < argc) {
388 config_path = argv[++i];
389 } else if (arg == "--help" || arg == "-h") {
390 std::cout
391 << "Usage: " << argv[0] << " [--port PORT] [--config PATH]\n"
392 << " OTA_FLASH:<ip>:<firmware.bin>\n"
393 << " OTA_BUILD_FLASH|ip|abs_platformio_project_dir|board_id (0=no ID flag)\n";
394 return 0;
395 }
396 }
397
398 {
399 const auto nic =
400 fsw::net::resolveDaqBindAddress(fsw::config::load(config_path), "OTAService");
401 if (!nic.ok)
402 return 1;
403 g_bind_address = nic.address;
404 }
405
406 signal(SIGINT, signalHandler);
407 signal(SIGTERM, signalHandler);
408 signal(SIGPIPE, SIG_IGN);
409
410 int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
411 if (listen_fd < 0) {
412 std::cerr << "[OTAService] socket() failed" << std::endl;
413 return 1;
414 }
415 {
416 int opt = 1;
417 setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
418 }
419 {
420 struct sockaddr_in addr{};
421 addr.sin_family = AF_INET;
422 addr.sin_addr.s_addr = INADDR_ANY;
423 addr.sin_port = htons(listen_port);
424 if (bind(listen_fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
425 std::cerr << "[OTAService] bind() failed on port " << listen_port << std::endl;
426 close(listen_fd);
427 return 1;
428 }
429 }
430 if (listen(listen_fd, 5) < 0) {
431 std::cerr << "[OTAService] listen() failed" << std::endl;
432 close(listen_fd);
433 return 1;
434 }
435
436 std::cout << "[OTAService] Listening on port " << listen_port << std::endl;
437 std::cout << "[OTAService] OTA_FLASH:… and OTA_BUILD_FLASH|ip|dir|board_id" << std::endl;
438
439 while (g_running) {
440 fd_set rd;
441 FD_ZERO(&rd);
442 FD_SET(listen_fd, &rd);
443 struct timeval tv{.tv_sec = 1, .tv_usec = 0};
444 if (select(listen_fd + 1, &rd, nullptr, nullptr, &tv) <= 0)
445 continue;
446
447 int client_fd = accept(listen_fd, nullptr, nullptr);
448 if (client_fd < 0)
449 continue;
450
451 std::thread([client_fd]() {
452 handleClient(client_fd);
453 }).detach();
454 }
455
456 close(listen_fd);
457 std::cout << "[OTAService] Stopped." << std::endl;
458 return 0;
459}
static uint8_t chunk[64]
Definition board_uart9.c:12
static void signalHandler(int)
static std::atomic< bool > g_running
static void send(void *ctx, const uint8_t *bytes, uint16_t len)
Definition gnss_task.c:27
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
Definition json.hpp:13878
*brief create JSON pointer *sa reference_tokens const string_t & a
Definition json.hpp:13877
*brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer *sa string_t token
Definition json.hpp:13937
*brief create JSON pointer *sa reference_tokens end()
std::string trim(const std::string &s)
bool buildAndFlash(const std::string &ip, const std::string &projectDir, int boardId, std::string &error, std::string &buildOutput)
bool runPioBuild(const std::string &projectDir, int boardId, std::string &buildLog, std::string &error)
Run pio in projectDir; capture combined stdout/stderr into buildLog.
bool flashFirmware(const std::string &ip, const std::string &bin_path, std::string &error)
std::string readFirstPioEnv(const std::string &iniPath)
Config load(const std::string &path)
Parse config.toml.
Definition Config.cpp:442
bool bindToDaqInterface(int sock, const std::string &address, const char *who)
bind() an already-created socket to address on an ephemeral port.
DaqBindResult resolveDaqBindAddress(const ::fsw::config::Config &cfg, const char *who)
Decide which local address board traffic should leave from, and log the decision.
*brief user defined string literal for JSON values *sa std::size_t n
Definition json.hpp:24474
void _exit(int status)
Definition syscalls.c:61
static void check(bool ok, const std::string &what)
int main()
static double read(int32_t code)