Add internet radio streaming with runtime API, modernize web UI, remove auto-tune/beep at boot
Streaming (main feature this session): - New WebRadioConfig/WebRadioJson core types, ISecureStore-backed persistence - New webradio::WebRadioService (thread-safe live config) + GET/POST /api/streaming - web_radio_stream task now runtime-toggleable (no reboot), no hardcoded URL - Content-Type diagnostic: warns clearly when a URL is a webpage, not an audio stream Boot cleanup: - Removed boot-time auto FM/DAB tune, auto-beep, and the (now-concluded) Si4684 crystal IBIAS/CTUN empirical sweep from main.cpp — tuning/beep are on-demand via the existing REST API only Web UI: - Modernized styling (cards, gradients, toggle switches, light/dark theme) - New Stream tab wired to /api/streaming Fixes found via real idf.py build (not just clangd): - Restored wrongly-removed si4684/Si4684Tuner.hpp include in main.cpp - Fixed MP3Decode() argument types in web_radio_stream.cpp (unsigned char**/int*) Quality-gate fixes: - Host-test stub headers (esp_log.h, freertos/*) so TunerService.cpp's scanForStation logging/pacing compiles for station_service_test / integration_service_test instead of running stale binaries - Added WifiScanner and WebRadioService manual sections; filled in missing Doxygen docs on BluetoothService, i2s_sdata_probe, test_firmware, Bt1035At - Ignore clangd's .cache/ index directory Also includes prior uncommitted work carried in the tree: Wi-Fi/Bluetooth device scan REST API and UI (WifiScanner, BT scan), SigmaStudio TCP bridge, and the current ADAU1701 SigmaStudio DSP program export. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,570 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
activeListenFd().store(-1, std::memory_order_release);
|
||||
if (listenFd_ >= 0) {
|
||||
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 {};
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
Reference in New Issue
Block a user