Files
DigiRadio/Software/components/net/src/SigmaStudioTcpServer.cpp
T
micheleandClaude Sonnet 5 75cb147f09 Fix SigmaStudio TCP bridge accept() spinning on EBADF forever
Every boot path constructs SigmaStudioTcpServer as a named local,
start()s it, then moves it into NetBootstrap. The moved-from local's
own destructor still runs stop() right after, which used to do an
unconditional activeListenFd().store(-1) -- clobbering the singleton
the moved-to (real, running) instance had just inherited. From then on
acceptLoopTask() called accept(-1, ...) == EBADF forever, on every
single boot, breaking every SigmaStudio Remote Connection attempt.

stop() now only clears the singleton via compare-exchange against its
own listenFd_, so a moved-from husk with no fd of its own leaves the
real instance's registration alone. Keeps a recreateListenSocket()
self-heal in acceptLoopTask() as a safety net for EBADF from any other
future cause, though it's no longer expected to fire in normal
operation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 18:34:28 +02:00

648 lines
21 KiB
C++

/**
* @file SigmaStudioTcpServer.cpp
* @brief SigmaStudioTcpServer implementation.
*
* DigiRadio firmware — https://github.com/manvalan/DigiRadio
*
* Copyright 2026 Michele Bigi
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* @author Michele Bigi
* @date 2026-08-07
*/
#include "net/SigmaStudioTcpServer.hpp"
#include "adau1701/FlashDspProgramSource.hpp"
#include "core/DspProgram.hpp"
#include "core/DspProgramBlob.hpp"
#include "core/RegisterWrite.hpp"
#include "SigmaStudioFW.h"
#include "esp_log.h"
#include "lwip/sockets.h"
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <span>
#include <vector>
namespace net {
namespace {
constexpr char kTag[] = "SigmaTcp";
constexpr std::uint16_t kPort = 8086U;
constexpr std::size_t kRecvBufSize = 16U * 1024U;
constexpr std::uint32_t kTaskStackBytes = 8192U;
constexpr UBaseType_t kTaskPriority = 5U;
constexpr std::uint8_t kCtrlWrite = 0x09U;
constexpr std::uint8_t kCtrlReadReq = 0x0AU;
constexpr std::uint8_t kCtrlReadResp = 0x0BU;
constexpr std::uint8_t kChipAddrDsp = 0x01U;
// board::pins::Adau1701Addr (main/board_pins.hpp), duplicated as a literal
// to avoid a net -> main include dependency; keep in sync if the board's
// I2C address ever changes. The reference project accepts both the IC
// index (0x01) and the raw address as chipAddr since it's unclear which
// convention real SigmaStudio uses for a single-IC project — mirrored here.
constexpr std::uint8_t kDspI2cAddr7 = 0x34U;
[[nodiscard]] bool isDspChipAddr(std::uint8_t chipAddr) noexcept
{
return chipAddr == kChipAddrDsp || chipAddr == kDspI2cAddr7;
}
/**
* @brief activeListenFd — process-lifetime storage for the listen fd.
*
* @dname activeListenFd
* @return Reference to the singleton listen-fd slot.
* @pubstate Written by SigmaStudioTcpServer::start()/stop(); read by
* acceptLoopTask(). SigmaStudioTcpServer is constructed as a
* local and move-relocated into NetBootstrap (see
* NetBootstrap.cpp), so a `this` pointer captured at start()
* time would go stale once that local's stack frame returns —
* same problem SetupWebServer's routeContextStorage() solves.
*
* @author Michele Bigi
* @date 2026-08-07
*/
[[nodiscard]] std::atomic<int>& activeListenFd() noexcept
{
static std::atomic<int> fd{-1};
return fd;
}
constexpr std::uint16_t kProgRamStart = 0x0400U;
constexpr std::uint16_t kProgRamEnd = 0x07FFU;
constexpr std::uint16_t kCtrlRegStart = 0x0800U;
constexpr std::uint16_t kCoreControlReg = 0x081CU;
constexpr std::uint8_t kDspRunBit = 0x04U;
constexpr unsigned kWordBytesParam = 4U;
constexpr unsigned kWordsPerSafeload = 5U;
constexpr std::size_t kWriteHeaderSize = 10U;
constexpr std::size_t kReadReqHeaderSize = 8U;
constexpr std::size_t kReadRespHeaderSize = 9U;
constexpr std::uint16_t kMaxReadBytes = 256U;
constexpr std::size_t kMaxCaptureRegions = 32U; // core::DspProgramBlob kMaxWriteCount
constexpr std::size_t kMaxRegionPayload = 16U * 1024U; // kMaxWritePayload
[[nodiscard]] unsigned wordSizeForAddress(std::uint16_t address) noexcept
{
if (address >= kProgRamStart && address <= kProgRamEnd) {
return 5U;
}
if (address >= kCtrlRegStart) {
return 2U;
}
return 4U;
}
[[nodiscard]] std::uint16_t readBe16(const std::uint8_t* p) noexcept
{
return static_cast<std::uint16_t>((static_cast<std::uint16_t>(p[0]) << 8)
| p[1]);
}
void writeBe16(std::uint8_t* p, std::uint16_t value) noexcept
{
p[0] = static_cast<std::uint8_t>((value >> 8) & 0xFFU);
p[1] = static_cast<std::uint8_t>(value & 0xFFU);
}
/**
* @brief PendingRegion — one coalesced contiguous write during a Download.
*/
struct PendingRegion {
std::uint16_t address;
std::vector<std::uint8_t> data;
};
/**
* @brief DownloadCapture — coalesces a Link Compile Download for persistence.
*
* Merges contiguous direct (non-safeload) DSP writes into a handful of
* regions (mirroring the ~5-block shape of EmbeddedDspProgramSource), then
* on finish() serialises and stores them via FlashDspProgramSource so the
* downloaded program becomes what DigiRadio boots with next time. Bails
* out (does not persist) if the session doesn't fit the DRAD blob's own
* caps — the DSP still runs fine from what was already written live.
*/
class DownloadCapture {
public:
void addWrite(std::uint16_t address, std::span<const std::uint8_t> data)
{
if (overflowed_ || data.empty()) {
return;
}
if (tryExtendLast(address, data)) {
return;
}
if (regions_.size() >= kMaxCaptureRegions) {
abandon();
return;
}
regions_.push_back(PendingRegion{
address, std::vector<std::uint8_t>(data.begin(), data.end())});
}
void finish()
{
if (!overflowed_ && !regions_.empty()) {
persist();
}
regions_.clear();
overflowed_ = false;
}
private:
[[nodiscard]] bool tryExtendLast(std::uint16_t address,
std::span<const std::uint8_t> data)
{
if (regions_.empty()) {
return false;
}
PendingRegion& last = regions_.back();
const unsigned wordSize = wordSizeForAddress(last.address);
const auto lastWords =
static_cast<std::uint32_t>(last.data.size() / wordSize);
const std::uint32_t lastEnd =
static_cast<std::uint32_t>(last.address) + lastWords;
if (lastEnd != address
|| last.data.size() + data.size() > kMaxRegionPayload) {
return false;
}
last.data.insert(last.data.end(), data.begin(), data.end());
return true;
}
void abandon()
{
overflowed_ = true;
regions_.clear();
ESP_LOGW(kTag,
"Download too fragmented to persist as boot program — "
"DSP still runs live, only reboot-persistence is skipped");
}
void persist()
{
std::vector<core::RegisterWrite> writes;
writes.reserve(regions_.size());
for (auto& region : regions_) {
writes.emplace_back(region.address, std::move(region.data));
}
const core::DspProgram program(std::move(writes));
const std::vector<std::uint8_t> blob =
core::serializeDspProgramBlob(program);
if (auto stored = adau1701::FlashDspProgramSource::storeBlob(blob);
!stored) {
ESP_LOGW(kTag, "SigmaStudio download not persisted (flash store failed)");
return;
}
ESP_LOGI(kTag,
"SigmaStudio download persisted as boot program (%u bytes)",
static_cast<unsigned>(blob.size()));
}
std::vector<PendingRegion> regions_;
bool overflowed_ = false;
};
/**
* @brief ConnectionState — per-TCP-connection Download tracking.
*/
struct ConnectionState {
DownloadCapture capture;
bool dspRunning = false;
};
void directWrite(std::uint16_t address, std::span<const std::uint8_t> data)
{
if (data.empty()) {
return;
}
sigma_studio_lock();
SIGMA_WRITE_REGISTER_BLOCK(
0U, address, static_cast<unsigned int>(data.size()),
const_cast<ADI_REG_TYPE*>(
reinterpret_cast<const ADI_REG_TYPE*>(data.data())));
sigma_studio_unlock();
}
void safeloadWrite(std::uint16_t address, std::span<const std::uint8_t> data)
{
const auto totalWords =
static_cast<unsigned>(data.size() / kWordBytesParam);
unsigned offset = 0U;
sigma_studio_lock();
while (offset < totalWords) {
const unsigned words =
std::min(totalWords - offset, kWordsPerSafeload);
unsigned addrs[kWordsPerSafeload];
for (unsigned i = 0U; i < words; ++i) {
addrs[i] = static_cast<unsigned>(address) + offset + i;
}
sigma_safeload_raw_block(
static_cast<unsigned char>(words), addrs,
data.data() + static_cast<std::size_t>(offset) * kWordBytesParam);
offset += words;
}
sigma_studio_unlock();
}
void trackDownloadCompletion(std::uint16_t address,
std::span<const std::uint8_t> payload,
ConnectionState& state)
{
if (address != kCoreControlReg || payload.size() < 2U) {
return;
}
const bool wasRunning = state.dspRunning;
state.dspRunning = (payload.back() & kDspRunBit) != 0U;
if (!wasRunning && state.dspRunning) {
state.capture.finish();
}
}
void dispatchWrite(std::uint16_t address, std::span<const std::uint8_t> payload,
std::uint8_t safeload, ConnectionState& state)
{
if (safeload != 0U) {
safeloadWrite(address, payload);
return;
}
if (!state.dspRunning) {
state.capture.addWrite(address, payload);
}
directWrite(address, payload);
trackDownloadCompletion(address, payload, state);
}
[[nodiscard]] std::size_t tryConsumeWrite(const std::uint8_t* p,
std::size_t avail,
ConnectionState& state)
{
if (avail < kWriteHeaderSize) {
return 0U;
}
const std::uint16_t totalLen = readBe16(p + 3U);
if (totalLen < kWriteHeaderSize) {
return 1U; // malformed frame — resync by one byte
}
if (avail < totalLen) {
return 0U;
}
const std::uint8_t safeload = p[1];
const std::uint8_t chipAddr = p[5];
const std::uint16_t dataLen = readBe16(p + 6U);
const std::uint16_t address = readBe16(p + 8U);
const std::uint16_t maxPayload =
static_cast<std::uint16_t>(totalLen - kWriteHeaderSize);
const std::uint16_t safeLen = std::min(dataLen, maxPayload);
if (isDspChipAddr(chipAddr)) {
const std::span<const std::uint8_t> payload(p + kWriteHeaderSize,
safeLen);
dispatchWrite(address, payload, safeload, state);
}
return totalLen;
}
void sendReadResponse(int clientFd, std::uint8_t chipAddr,
std::uint16_t address, std::uint16_t requested)
{
const std::uint16_t length = std::min(requested, kMaxReadBytes);
std::vector<std::uint8_t> data(length);
sigma_studio_lock();
const int result =
length == 0U ? 0 : sigma_i2c_read(address, data.data(), length);
sigma_studio_unlock();
if (result != 0) {
ESP_LOGW(kTag,
"read 0x%04x len=%u chipAddr=0x%02x: sigma_i2c_read failed",
static_cast<unsigned>(address),
static_cast<unsigned>(length),
static_cast<unsigned>(chipAddr));
return;
}
std::vector<std::uint8_t> resp(kReadRespHeaderSize + length);
resp[0] = kCtrlReadResp;
writeBe16(resp.data() + 1U,
static_cast<std::uint16_t>(kReadRespHeaderSize + length));
resp[3] = chipAddr;
writeBe16(resp.data() + 4U, length);
writeBe16(resp.data() + 6U, address);
resp[8] = 0x01U;
std::memcpy(resp.data() + kReadRespHeaderSize, data.data(), length);
const ssize_t sent = send(clientFd, resp.data(), resp.size(), 0);
ESP_LOGI(kTag,
"read 0x%04x len=%u chipAddr=0x%02x: sent %d/%u resp bytes",
static_cast<unsigned>(address), static_cast<unsigned>(length),
static_cast<unsigned>(chipAddr), static_cast<int>(sent),
static_cast<unsigned>(resp.size()));
}
[[nodiscard]] std::size_t tryConsumeRead(const std::uint8_t* p,
std::size_t avail, int clientFd)
{
if (avail < kReadReqHeaderSize) {
return 0U;
}
const std::uint16_t totalLen = readBe16(p + 1U);
if (totalLen < kReadReqHeaderSize) {
return 1U; // malformed frame — resync by one byte
}
if (avail < totalLen) {
return 0U;
}
const std::uint8_t chipAddr = p[3];
const std::uint16_t dataLen = readBe16(p + 4U);
const std::uint16_t address = readBe16(p + 6U);
if (isDspChipAddr(chipAddr)) {
sendReadResponse(clientFd, chipAddr, address, dataLen);
} else {
ESP_LOGW(kTag,
"read req dropped: chipAddr=0x%02x not DSP (want 0x%02x or "
"0x%02x), addr=0x%04x len=%u",
static_cast<unsigned>(chipAddr),
static_cast<unsigned>(kChipAddrDsp),
static_cast<unsigned>(kDspI2cAddr7),
static_cast<unsigned>(address),
static_cast<unsigned>(dataLen));
}
return totalLen;
}
[[nodiscard]] std::size_t processBuffer(std::uint8_t* buf, std::size_t len,
int clientFd, ConnectionState& state)
{
std::size_t pos = 0U;
while (pos < len) {
const std::uint8_t ctrl = buf[pos];
std::size_t consumed = 0U;
if (ctrl == kCtrlWrite) {
consumed = tryConsumeWrite(buf + pos, len - pos, state);
} else if (ctrl == kCtrlReadReq) {
consumed = tryConsumeRead(buf + pos, len - pos, clientFd);
} else {
ESP_LOGW(kTag, "unrecognized ctrl byte 0x%02x — resyncing",
static_cast<unsigned>(ctrl));
consumed = 1U;
}
if (consumed == 0U) {
break;
}
pos += consumed;
}
return pos;
}
/** @brief Diagnostic: hex-dump up to the first 48 bytes of a receive. */
void logRxHexDump(const std::uint8_t* p, std::size_t len)
{
constexpr std::size_t kMaxDump = 48U;
char hex[3U * kMaxDump + 1U];
const std::size_t n = std::min(len, kMaxDump);
for (std::size_t i = 0U; i < n; ++i) {
std::snprintf(hex + i * 3U, 4U, "%02x ", static_cast<unsigned>(p[i]));
}
ESP_LOGI(kTag, "rx %u bytes: %s%s", static_cast<unsigned>(len), hex,
len > kMaxDump ? "..." : "");
}
void serveClient(int clientFd)
{
ConnectionState state;
std::vector<std::uint8_t> buf(kRecvBufSize);
std::size_t len = 0U;
while (true) {
const ssize_t received =
recv(clientFd, buf.data() + len, buf.size() - len, 0);
if (received <= 0) {
break;
}
logRxHexDump(buf.data() + len, static_cast<std::size_t>(received));
len += static_cast<std::size_t>(received);
const std::size_t consumed = processBuffer(buf.data(), len, clientFd, state);
if (consumed > 0U && consumed < len) {
std::memmove(buf.data(), buf.data() + consumed, len - consumed);
}
len = consumed <= len ? len - consumed : 0U;
}
}
} // namespace
SigmaStudioTcpServer::SigmaStudioTcpServer()
: listenFd_(-1)
, task_(nullptr)
{
}
SigmaStudioTcpServer::~SigmaStudioTcpServer()
{
stop();
}
SigmaStudioTcpServer::SigmaStudioTcpServer(SigmaStudioTcpServer&& other) noexcept
: listenFd_(other.listenFd_)
, task_(other.task_)
{
other.listenFd_ = -1;
other.task_ = nullptr;
}
SigmaStudioTcpServer&
SigmaStudioTcpServer::operator=(SigmaStudioTcpServer&& other) noexcept
{
if (this != &other) {
stop();
listenFd_ = other.listenFd_;
task_ = other.task_;
other.listenFd_ = -1;
other.task_ = nullptr;
}
return *this;
}
void SigmaStudioTcpServer::stop() noexcept
{
if (task_ != nullptr) {
vTaskDelete(task_);
task_ = nullptr;
}
if (listenFd_ >= 0) {
// Only clear the singleton if it still points at *our* fd: every
// boot path constructs this as a named local, start()s it, then
// moves it into NetBootstrap, so the moved-from local's own
// destructor runs stop() right after. An unconditional
// activeListenFd().store(-1) here used to stomp the atomic the
// moved-to (real, running) instance had just inherited, making
// acceptLoopTask() spin on accept(-1, ...) == EBADF forever from
// the very first boot -- root cause of the 2026-08-25 field
// observation, not a Wi-Fi-layer event.
int expected = listenFd_;
activeListenFd().compare_exchange_strong(expected, -1,
std::memory_order_acq_rel);
close(listenFd_);
listenFd_ = -1;
}
}
std::expected<void, NetError> SigmaStudioTcpServer::start()
{
if (task_ != nullptr) {
return {};
}
const int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
ESP_LOGE(kTag, "socket() failed");
return std::unexpected(NetError::TcpServerStartFailed);
}
const int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(kPort);
if (bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0
|| listen(fd, 1) != 0) {
ESP_LOGE(kTag, "bind/listen failed");
close(fd);
return std::unexpected(NetError::TcpServerStartFailed);
}
listenFd_ = fd;
activeListenFd().store(fd, std::memory_order_release);
const BaseType_t created =
xTaskCreate(&SigmaStudioTcpServer::acceptLoopTask, "sigma_tcp",
kTaskStackBytes, nullptr, kTaskPriority, &task_);
if (created != pdPASS) {
ESP_LOGE(kTag, "xTaskCreate failed");
activeListenFd().store(-1, std::memory_order_release);
close(listenFd_);
listenFd_ = -1;
task_ = nullptr;
return std::unexpected(NetError::TcpServerStartFailed);
}
ESP_LOGI(kTag, "SigmaStudio TCP bridge listening on port %u",
static_cast<unsigned>(kPort));
return {};
}
namespace {
/**
* @brief recreateListenSocket — rebind a fresh listening socket on kPort.
*
* @dname recreateListenSocket
* @return The new fd on success (also stored in activeListenFd()), or -1.
* @pubstate closes the previous fd read from activeListenFd() if any, then
* publishes the new one.
*
* Self-healing counterpart to SigmaStudioTcpServer::start()'s socket setup.
* The 2026-08-25 field observation (accept() spinning on errno=EBADF
* forever) turned out to be a stop() lifetime bug, now fixed there: this
* function is kept as a safety net in case the singleton is ever cleared
* from underneath a running accept task by some future code path, not
* because it is expected to fire in normal operation.
*/
[[nodiscard]] int recreateListenSocket() noexcept
{
const int oldFd = activeListenFd().exchange(-1, std::memory_order_acq_rel);
if (oldFd >= 0) {
close(oldFd);
}
const int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd < 0) {
ESP_LOGE(kTag, "recreateListenSocket: socket() failed");
return -1;
}
const int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(kPort);
if (bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0
|| listen(fd, 1) != 0) {
ESP_LOGE(kTag, "recreateListenSocket: bind/listen failed, errno=%d",
errno);
close(fd);
return -1;
}
activeListenFd().store(fd, std::memory_order_release);
ESP_LOGW(kTag, "SigmaStudio TCP listen socket recreated after failure");
return fd;
}
} // namespace
void SigmaStudioTcpServer::acceptLoopTask(void* /*arg*/)
{
while (true) {
const int listenFd = activeListenFd().load(std::memory_order_acquire);
sockaddr_in clientAddr{};
socklen_t clientLen = sizeof(clientAddr);
const int clientFd = accept(
listenFd, reinterpret_cast<sockaddr*>(&clientAddr), &clientLen);
if (clientFd < 0) {
// EBADF means the listen socket itself is gone -- retrying
// accept() on the same fd forever can never recover from this,
// unlike a transient per-call error, so rebuild the socket
// instead of just backing off and looping.
if (errno == EBADF) {
ESP_LOGE(kTag,
"accept() failed: listen socket invalid (errno=%d) "
"-- recreating",
errno);
(void)recreateListenSocket();
vTaskDelay(pdMS_TO_TICKS(500));
} else {
ESP_LOGW(kTag, "accept() failed: errno=%d", errno);
vTaskDelay(pdMS_TO_TICKS(100));
}
continue;
}
ESP_LOGI(kTag, "SigmaStudio client connected");
serveClient(clientFd);
ESP_LOGI(kTag, "SigmaStudio client disconnected");
close(clientFd);
}
}
} // namespace net