23#include <netinet/in.h>
25#include <sys/socket.h>
46 std::cout <<
"\n[OTAService] Shutting down..." << std::endl;
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);
67bool flashFirmware(
const std::string& ip,
const std::string& bin_path, std::string& error) {
69 std::ifstream f(bin_path, std::ios::binary | std::ios::ate);
71 error =
"cannot open file: " + bin_path;
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";
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";
87 int sock = socket(AF_INET, SOCK_STREAM, 0);
89 error =
"socket() failed";
95 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv,
sizeof(tv));
96 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv,
sizeof(tv));
108 struct sockaddr_in addr{};
109 addr.sin_family = AF_INET;
111 if (inet_pton(AF_INET, ip.c_str(), &addr.sin_addr) != 1) {
113 error =
"invalid IP: " + ip;
116 if (connect(sock,
reinterpret_cast<struct sockaddr*
>(&addr),
sizeof(addr)) < 0) {
118 error =
"connect to " + ip +
":3232 failed";
125 setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv,
sizeof(tv));
126 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv,
sizeof(tv));
130 uint32_t size_be = htonl(
static_cast<uint32_t
>(file_size));
131 if (
send(sock, &size_be, 4, 0) != 4) {
133 error =
"failed to send size header";
140 while (sent < firmware.size()) {
142 ssize_t
n =
send(sock, firmware.data() + sent,
chunk, 0);
145 error =
"send failed at offset " + std::to_string(sent);
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;
159 ssize_t
n = recv(sock, resp,
sizeof(resp) - 1, 0);
163 error =
"no response from board (may have rebooted normally)";
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;
178 std::ifstream f(iniPath);
180 return "adafruit_feather_esp32s3";
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));
189 return "adafruit_feather_esp32s3";
194 if (c ==
'\n' || c ==
'\r')
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;
213 if (pipe(pipefd) < 0) {
214 error =
"pipe() failed";
222 error =
"fork() failed";
228 dup2(pipefd[1], STDOUT_FILENO);
229 dup2(pipefd[1], STDERR_FILENO);
232 if (chdir(projectDir.c_str()) != 0) {
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);
240 unsetenv(
"PLATFORMIO_BUILD_FLAGS");
243 execlp(
"pio",
"pio",
"run", (
char*)
nullptr);
244 execlp(
"platformio",
"platformio",
"run", (
char*)
nullptr);
252 while ((
n =
read(pipefd[0], buf,
sizeof(buf) - 1)) > 0 && buildLog.size() < 65536) {
259 if (waitpid(pid, &st, 0) < 0) {
260 error =
"waitpid failed";
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;
272bool buildAndFlash(
const std::string& ip,
const std::string& projectDir,
int boardId,
273 std::string& error, std::string& buildOutput) {
275 if (!
runPioBuild(projectDir, boardId, blog, error)) {
282 std::cout <<
"[OTAService] pio run output:\n" << blog;
283 if (blog.back() !=
'\n')
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);
290 error =
"firmware.bin missing after build: " + binPath;
301 struct timeval tv{.tv_sec = 30, .tv_usec = 0};
302 setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv,
sizeof(tv));
304 auto sendReply = [&](
const std::string& msg) {
305 send(client_fd, msg.c_str(), msg.size(), MSG_NOSIGNAL);
311 while (
g_running && recv(client_fd, &c, 1, 0) == 1) {
314 if (buf.size() < 8192)
317 const std::string cmd =
trim(buf);
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");
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;
330 std::cout <<
"[OTAService] Flash " << ip <<
" OK" << std::endl;
333 std::cerr <<
"[OTAService] Flash " << ip <<
" failed: " << err << std::endl;
335 sendReply(
"ERR:" + err +
"\n");
338 }
else if (cmd.compare(0, 16,
"OTA_BUILD_FLASH|") == 0) {
340 std::istringstream iss(cmd);
342 std::vector<std::string> parts;
343 while (std::getline(iss,
token,
'|')) {
346 if (parts.size() != 4 || parts[0] !=
"OTA_BUILD_FLASH") {
348 "ERR:bad OTA_BUILD_FLASH — use OTA_BUILD_FLASH|ip|abs_project_dir|board_id\n");
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");
356 std::cout <<
"[OTAService] Build+flash " << ip <<
" project " << proj
357 <<
" board_id " << bid << std::endl;
359 std::string buildOut;
361 std::cout <<
"[OTAService] Build+flash " << ip <<
" OK" << std::endl;
364 std::cerr <<
"[OTAService] Build+flash failed: " << err << std::endl;
366 sendReply(
"ERR:" + err +
"\n");
370 }
else if (!cmd.empty()) {
371 sendReply(
"ERR:unknown command\n");
379int main(
int argc,
char* argv[]) {
380 uint16_t listen_port = 9997;
381 std::string config_path =
"config/config.toml";
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") {
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";
408 signal(SIGPIPE, SIG_IGN);
410 int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
412 std::cerr <<
"[OTAService] socket() failed" << std::endl;
417 setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt,
sizeof(opt));
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;
430 if (listen(listen_fd, 5) < 0) {
431 std::cerr <<
"[OTAService] listen() failed" << std::endl;
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;
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)
447 int client_fd = accept(listen_fd,
nullptr,
nullptr);
451 std::thread([client_fd]() {
457 std::cout <<
"[OTAService] Stopped." << std::endl;
static void signalHandler(int)
static std::atomic< bool > g_running
static void send(void *ctx, const uint8_t *bytes, uint16_t len)
*brief create JSON pointer *sa reference_tokens const string_t const string_t & b
*brief create JSON pointer *sa reference_tokens const string_t & a
*brief create a new JSON pointer by appending the unescaped token at the end of the JSON pointer *sa string_t token
*brief create JSON pointer *sa reference_tokens end()
std::string g_bind_address
void handleClient(int client_fd)
std::string trim(const std::string &s)
static constexpr int CONNECT_TIMEOUT_S
void sanitizeOneLine(std::string &s)
static constexpr size_t CHUNK_SIZE
std::atomic< bool > g_running
static constexpr uint16_t OTA_PORT
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.
static constexpr int TRANSFER_TIMEOUT_S
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.
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
static void check(bool ok, const std::string &what)
static double read(int32_t code)