Files
pico/projects/tzpuPico/esp32/main/CommandProcessor.cpp
Philip Smart 4196e58420 MZ-1500 persona, expansion boards, Celestite LAN, QDF format, virtual mode
New drivers:
- MZ-1500 persona driver with MZ-700/MZ-1500 mode switch, PCG bank switching,
  PSG stubs, physical I/O forwarding for virtual mode
- MZ-2200 persona driver (based on MZ-2000)
- MZ-1R23/MZ-1R24 Kanji ROM / Dictionary ROM board (B8h/B9h IDM)
- MZ-1R37 640KB EMM (ACh/ADh, no auto-increment, 20-bit addressing)
- PIO-3034 320KB EMM (configurable base, 19-bit, auto-increment)
- Celestite LAN/Memory composite board:
  - W5100 TCP/IP via ESP32 WiFi (connect, send, recv, ping)
  - Integrated MZ-1R12 32/64KB CMOS RAM with SD persistence
  - Integrated MZ-1R37 640KB EMM with SD persistence
  - UFM flash, unlock state machine, interrupt controller

Celestite networking (Phase 2):
- New IPC commands: NET_CFG, NET_SOCK, NET_SEND, NET_RECV, NET_PING
- ESP32 BSD socket handlers with non-blocking connect and recv
- Shared volatile struct for cross-core results (bypasses responseQueue)
- Inline IDM read check for socket status and recv data
- Z80 test programs: celestite_test.asm (17 tests), celestite_stress.asm (loop)

MZ-1500 virtual mode fixes:
- I/O writes forwarded to physical hardware (PSG, bank switching, PCG)
- E000-E7FF always stays physical during bank switching
- PCG bank (F000-FFFF) properly remapped to PHYSICAL when open

QDF format support:
- Japanese standard QD format auto-detected on load (81936 bytes)
- Hunt pattern changed to 00+16 (mark+sync) for inter-block gap handling
- Sync stripping handles long preambles (9+ bytes)
- MZQDTool updated with -j flag and format conversion

Other:
- Debug shell load command: len parameter now optional
- FSPI filename field: memcpy instead of strncpy for binary data
- Interface availability expanded across MZ-700/1500/80A/2000/2200
- Web GUI: param hints for Celestite, updated driver interface lists

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-17 11:34:55 +01:00

897 lines
33 KiB
C++

/////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Name: CommandProcessor.cpp
// Created: Jan 2025
// Version: v1.1
// Author(s): Philip Smart
// Description: Command processor — receives binary IPC commands from the RP2350 over SPI
// and dispatches to the appropriate handler.
// v1.1: Binary SPI IPC replaces UART ASCII command/response round-trips.
// vTaskDelay(1) polling loop eliminated; SPI slave receive blocks
// (CMD_SPI_POLL_TICKS timeout) — no wasted tick delays on idle bus.
// UART path retained for OOB handling by IO_stdinReaderTask.
// Binary opcode dispatch (O(1) switch) replaces std::map lookup.
// Credits:
// Copyright: (c) 2019-2026 Philip Smart <philip.smart@net2net.org>
//
// History: v1.00 Jan 2025 - Initial write.
// v1.10 Mar 2026 - Binary SPI IPC, opcode dispatch, semaphore-free blocking wait.
//
// Notes: See Makefile to enable/disable conditional components
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// This source file is free software: you can redistribute it and#or modify
// it under the terms of the GNU General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This source file is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
/////////////////////////////////////////////////////////////////////////////////////////////////////////
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_slave.h"
#include "esp_system.h"
#include "esp_log.h"
#include "sdmmc_cmd.h"
#include "driver/sdspi_host.h"
#include "driver/spi_common.h"
#include "driver/uart.h"
#include "driver/gpio.h"
#include "esp_vfs_fat.h"
#include <string>
#include <unordered_map>
#include <vector>
#include <functional>
#include "IO.h"
#include "CommandProcessor.h"
#include "ipc_protocol.h"
#include "esp_attr.h" // RTC_NOINIT_ATTR
#include "esp_timer.h"
#include "esp_rom_crc.h" // esp_rom_crc32_le for network payload CRC
// Declared in IO.cpp — set before esp_restart() in the OOB handler so we know
// this SW reset was clean (no mid-transaction state) and can use the fast path.
#define OOB_RESTART_MAGIC 0xAA55CC33u
// Declared in IO.cpp — set by OOB handler to request SPI slave reinitalization.
extern volatile bool g_spi_clear_requested;
// ---------------------------------------------------------------------------
// ESP32→RP2350 reverse command queue.
//
// Commands are delivered to the RP2350 via the NOP poll T3 response filename
// field. Two dispatch modes are supported:
//
// CP_queueCmd(cmd) — async: push and return immediately.
// Use when the result is not needed.
//
// CP_sendCmd(cmd, timeoutMs) — sync: push and block until the NOP poll
// has delivered the command to the RP2350
// (i.e. it appeared in a T3 response).
// Returns true if delivered within timeout.
//
// Multiple commands can be queued simultaneously up to CP_CMD_QUEUE_DEPTH.
// Async commands beyond the queue depth are silently dropped (non-blocking
// xQueueSend with timeout 0). Sync commands wait up to 100 ms to enqueue.
// ---------------------------------------------------------------------------
#define CP_CMD_QUEUE_DEPTH 8
typedef struct
{
char cmd[IPCF_FILENAME_LEN];
SemaphoreHandle_t doneSem; // NULL = async; semaphore = sync (signalled on delivery)
} t_CpCmd;
static QueueHandle_t s_cmdQueue = NULL;
static t_CpCmd s_pendingCmd = {}; // Command currently being delivered to RP2350 via NOP
// ---------------------------------------------------------------------------
// Initial SPI-done flag.
//
// Set to true the first time storeRP2350Info() succeeds (i.e. the RP2350 has
// sent an INF command, which it only does after completing its own startup
// file-load sequence). app_main() polls this via CP_initialSpiDone() to
// delay WiFi startup until after the RP2350's initial SPI exchanges are
// complete — prevents high-priority WiFi FreeRTOS tasks from preempting the
// CommandProcessor during the critical startup window and causing T3 HS
// timeouts on the INF exchange.
// ---------------------------------------------------------------------------
static volatile bool s_initialSpiDone = false;
bool CP_initialSpiDone(void)
{
return s_initialSpiDone;
}
void CP_markInitialSpiDone(void)
{
s_initialSpiDone = true;
}
void CP_queueCmd(const char *cmd)
{
if (!s_cmdQueue)
{
return;
}
t_CpCmd entry = {};
strncpy(entry.cmd, cmd, IPCF_FILENAME_LEN - 1);
entry.doneSem = NULL;
xQueueSend(s_cmdQueue, &entry, 0); // drop silently if queue full
}
bool CP_sendCmd(const char *cmd, uint32_t timeoutMs)
{
if (!s_cmdQueue)
{
return false;
}
SemaphoreHandle_t done = xSemaphoreCreateBinary();
if (!done)
{
return false;
}
t_CpCmd entry = {};
strncpy(entry.cmd, cmd, IPCF_FILENAME_LEN - 1);
entry.doneSem = done;
// Wait up to 100 ms to enqueue (queue full is unlikely but possible).
if (xQueueSend(s_cmdQueue, &entry, pdMS_TO_TICKS(100)) != pdTRUE)
{
vSemaphoreDelete(done);
return false;
}
// Block until cmdNop delivers the command to RP2350 (or timeout).
bool delivered = (xSemaphoreTake(done, pdMS_TO_TICKS(timeoutMs)) == pdTRUE);
vSemaphoreDelete(done);
return delivered;
}
extern uint32_t g_oob_restart_magic;
#define CMDPROCTAG "CMDPROC"
// ---------------------------------------------------------------------------
// String splitter (unchanged — used for legacy / debug paths if needed)
// ---------------------------------------------------------------------------
std::vector<std::string> CommandProcessor::split(const std::string &s, const std::string &delimiter)
{
size_t posStart = 0, posEnd, delimLen = delimiter.length();
std::string token;
std::vector<std::string> retVal;
while ((posEnd = s.find(delimiter, posStart)) != std::string::npos)
{
token = s.substr(posStart, posEnd - posStart);
posStart = posEnd + delimLen;
retVal.push_back(token);
}
retVal.push_back(s.substr(posStart));
return (retVal);
}
// ---------------------------------------------------------------------------
// waitForCommand — main command receive loop.
//
// The RP2350 now sends all disk I/O commands as binary IPC frames over SPI.
// This loop blocks on fspi.receiveBinaryCmd() with a short timeout
// (CMD_SPI_POLL_TICKS). On timeout it checks the UART frame queue for
// any legacy frames (OOB echo, debug, etc.) then loops back to SPI receive.
//
// The SPI receive post_setup_cb asserts HS HIGH each time a receive is
// queued, signalling the RP2350 that the ESP32 is ready for a command.
// This replaces the old vTaskDelay(1) polling loop entirely.
// ---------------------------------------------------------------------------
void CommandProcessor::waitForCommand(void)
{
// Startup delay — behaviour depends on reset reason:
//
// POWERON: fresh boot; SPI slave is uninitialised, HS stays LOW, so the
// RP2350 simply waits. A short delay is enough to let FreeRTOS settle
// before the first spi_slave_transmit() call.
//
// PANIC / SW restart: the ESP32 may have crashed mid-transaction while the
// RP2350 was blocked in waitForHandShake() for T2/T3 (timeout = 3000 ms).
// If we raise HS (via post_setup_cb) before that timeout expires, the
// RP2350 mistakes the T1-ready pulse for a T2/T3 HS and sends the wrong
// payload into the 64-byte T1 DMA window, breaking frame synchronisation
// for the session. 3500 ms gives 500 ms margin beyond the RP2350 timeout.
{
// Proven fixed delays — simple and reliable:
//
// POWERON (2000 ms): Both RP2350 and ESP32 boot from scratch. The delay
// gives the RP2350 time to reach FSPI_init() and drive
// CS HIGH before we register the SPI slave ISR.
//
// SW reset + OOB magic (100 ms): The RP2350 is already running with CS
// firmly driven HIGH. Only used when OOB explicitly
// sent (currently disabled, but kept for future use).
//
// SW reset, no OOB (3500 ms): spihost corruption from RP2350 reset.
// The RP2350 is rebooting — 3500 ms gives it time to
// complete boot + FSPI_init + stabilize SPI bus.
bool isOobRestart = (esp_reset_reason() != ESP_RST_POWERON) && (g_oob_restart_magic == OOB_RESTART_MAGIC);
g_oob_restart_magic = 0u; // consume flag
uint32_t delayMs;
if (esp_reset_reason() == ESP_RST_POWERON)
{
delayMs = 2000u;
}
else if (isOobRestart)
{
delayMs = 100u;
}
else
{
delayMs = 3500u;
}
ESP_LOGI(CMDPROCTAG, "Startup delay %lu ms (reset reason %d, oob=%d).", delayMs, (int) esp_reset_reason(), (int) isOobRestart);
vTaskDelay(pdMS_TO_TICKS(delayMs));
}
// Initialize the SPI slave hardware after the delay.
// By this point the RP2350 has had time to call FSPI_init() which drives
// CS (GPIO 45) HIGH — no more floating CS noise from SPI pin glitches.
// HS was configured LOW in setupEarly() so RP2350 knew to wait.
if (!fspi.init())
{
// SPI init failed — log but do NOT restart. The ESP32 must stay online
// for WiFi/web interface (firmware updates, config). SPI will be retried
// if/when the RP2350 stabilizes.
ESP_LOGE(CMDPROCTAG, "SPI slave init failed — SPI disabled, WiFi/web still active.");
}
t_IpcFrameHdr cmdFrame;
int badFrameCount = 0;
int spiErrorCount = 0;
for (;;)
{
// --- Binary SPI command receive ---
// Block indefinitely (portMAX_DELAY) waiting for the RP2350 to send a
// command frame. Using a finite timeout is UNSAFE: a timed-out call
// leaves the spi_slave_transaction_t on the stack (which goes out of
// scope) still referenced by the SPI slave ISR queue. When a real
// command later arrives, the ISR processes the stale (now dangling)
// transaction, puts its result in the queue, and the next call's
// spi_slave_get_trans_result() gets the wrong pointer → assert failure
// (spi_slave.c:524 "ret_trans == trans_desc").
//
// A secondary symptom: the stale pre-arm fires post_setup_cb (HS HIGH)
// immediately after a completed transaction's post_trans_cb (HS LOW),
// so the LOW→HIGH gap is sub-100µs and invisible to the RP2350's 100µs
// poll in waitForHandShake(false) → T3 starts while ESP32 still has a
// recv transaction armed → RP2350 receives zeros → bad frameType=00.
//
// portMAX_DELAY guarantees exactly one transaction in flight at all times.
memset(&cmdFrame, 0, IPCF_HEADER_SIZE);
esp_err_t spiRet = fspi.receiveBinaryCmd((uint8_t *) &cmdFrame, portMAX_DELAY);
// Check if OOB requested SPI slave reinit. This runs AFTER
// spi_slave_transmit returns (transaction complete, no pending DMA),
// so spi_slave_free is safe — no GDMA deadlock risk.
if (g_spi_clear_requested)
{
g_spi_clear_requested = false;
ESP_LOGW(CMDPROCTAG, "Reinitializing SPI slave (OOB request)...");
spi_slave_free(FSPI_HOST);
if (fspi.init())
{
ESP_LOGI(CMDPROCTAG, "SPI slave reinit OK.");
}
else
{
ESP_LOGE(CMDPROCTAG, "SPI slave reinit FAILED.");
}
badFrameCount = 0;
spiErrorCount = 0;
continue; // Re-arm spi_slave_transmit with fresh state
}
if (spiRet == ESP_OK)
{
spiErrorCount = 0; // SPI slave is working — reset error counter
if (cmdFrame.frameType == IPCF_TYPE_COMMAND)
{
// Valid binary command — dispatch immediately.
badFrameCount = 0;
processCommand(cmdFrame);
}
else if (cmdFrame.frameType == IPCF_TYPE_NOP || cmdFrame.frameType == 0)
{
// NOP or zero frame — normal idle. Reset bad frame counter.
badFrameCount = 0;
}
else
{
// Unknown/garbage frameType — protocol desync from RP2350 reset.
// After too many consecutive bad frames, restart to reinit SPI slave.
badFrameCount++;
if (badFrameCount <= 10 || (badFrameCount % 100) == 0)
ESP_LOGW(CMDPROCTAG, "Bad frameType=0x%02X (count=%d)", cmdFrame.frameType, badFrameCount);
}
}
else
{
// SPI slave is in an unrecoverable state (e.g. spihost NULL after a
// spurious CS/SCK glitch during RP2350 reboot).
//
// Call esp_restart() immediately — no spi_slave_free(), no esp_wifi_stop().
//
// DO NOT call spi_slave_free() here:
// If spihost[FSPI_HOST] is NULL (corrupted by SCK/CS glitches during
// RP2350 reset), spi_slave_free() returns ESP_ERR_INVALID_ARG without
// deregistering the ISR. If spihost is non-NULL but GDMA is in a bad
// state, spi_slave_free() holds the GDMA spinlock, esp_intr_free()
// re-enables interrupts, the SPI ISR fires and tries to acquire the
// same spinlock → deadlock → interrupt WDT.
//
// DO NOT call esp_wifi_stop() here:
// When WiFi is in a connection-retry cycle (as seen in the log), the
// WiFi state machine mutex is already held by the WiFi task.
// esp_wifi_stop() tries to take that mutex recursively →
// assert failed: xQueueTakeMutexRecursive queue.c:821 (pxMutex).
// Even outside a retry cycle, calling esp_wifi_stop() during WiFi
// driver init (<2 s) blocks on an internal driver lock → IWDT.
//
// By the time we reach this error path, the RP2350 has long since
// completed its own reboot and is driving SCK/CS/MOSI normally —
// no SPI ISR starvation risk. esp_restart() (a ROM function) resets
// the CPU without going through normal FreeRTOS task teardown; it
// also flushes the UART TX FIFO internally so the log line below is
// fully emitted before the reset.
// SPI slave error — likely spihost corruption from RP2350 reset.
// Do NOT restart — keep the ESP32 online for WiFi/web interface.
// Just log and keep retrying. If spihost is truly NULL, every call
// will fail, but WiFi and web interface remain functional for
// firmware updates and config management.
spiErrorCount++;
if (spiErrorCount <= 5 || (spiErrorCount % 100) == 0)
ESP_LOGE(CMDPROCTAG, "receiveBinaryCmd failed (%s), count=%d — retrying", esp_err_to_name(spiRet), spiErrorCount);
vTaskDelay(pdMS_TO_TICKS(2000));
continue;
}
}
}
// ---------------------------------------------------------------------------
// Initialisation (called by start()).
// ---------------------------------------------------------------------------
void CommandProcessor::init(void)
{
s_cmdQueue = xQueueCreate(CP_CMD_QUEUE_DEPTH, sizeof(t_CpCmd));
if (!s_cmdQueue)
{
ESP_LOGE(CMDPROCTAG, "Failed to create reverse-command queue.");
}
}
void CommandProcessor::cmdNop(const t_IpcFrameHdr &frame)
{
// NOP poll from RP2350 — deliver pending ESP32→RP2350 commands via the
// T3 response filename field.
//
// Reliability: the previous NOP's command is kept in s_pendingCmd until
// THIS NOP arrives — proving the RP2350 is alive and processed it (or
// at least received the frame). Only then do we dequeue the next entry.
// This prevents command loss when a CRC mismatch causes the RP2350 to
// discard the response: the next NOP re-sends the same command.
// --- Retire the previously-sent command (RP2350 got it) ---
if (s_pendingCmd.cmd[0] != '\0')
{
if (s_pendingCmd.doneSem)
xSemaphoreGive(s_pendingCmd.doneSem); // unblock CP_sendCmd caller
memset(&s_pendingCmd, 0, sizeof(s_pendingCmd));
}
// --- Dequeue the next command (if any) into the pending slot ---
if (s_cmdQueue && s_pendingCmd.cmd[0] == '\0')
{
xQueueReceive(s_cmdQueue, &s_pendingCmd, 0);
}
// --- Build NOP response with any pending command embedded ---
t_IpcFrameHdr hdr = {};
hdr.frameType = IPCF_TYPE_RESPONSE;
hdr.command = IPCF_CMD_NOP;
hdr.status = IPCF_STATUS_OK;
hdr.payloadLen = 0;
if (s_pendingCmd.cmd[0] != '\0')
{
strncpy(hdr.filename, s_pendingCmd.cmd, IPCF_FILENAME_LEN - 1);
}
uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE;
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
}
// ---------------------------------------------------------------------------
// Network command handlers (Celestite W5100 emulation).
// ---------------------------------------------------------------------------
void CommandProcessor::cmdNetCfg(const t_IpcFrameHdr &frame)
{
// Return ESP32's WiFi STA network configuration.
t_IpcFrameHdr hdr = {};
hdr.frameType = IPCF_TYPE_RESPONSE;
hdr.command = frame.command;
// Get the default STA interface.
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (!netif)
{
hdr.status = IPCF_STATUS_ERR;
hdr.payloadLen = 0;
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
return;
}
esp_netif_ip_info_t ipInfo;
esp_netif_get_ip_info(netif, &ipInfo);
uint8_t mac[6];
esp_netif_get_mac(netif, mac);
// Pack: IP[4] + GW[4] + Subnet[4] + MAC[6] = 18 bytes.
uint8_t payload[18];
memcpy(payload, &ipInfo.ip.addr, 4);
memcpy(payload + 4, &ipInfo.gw.addr, 4);
memcpy(payload + 8, &ipInfo.netmask.addr, 4);
memcpy(payload + 12, mac, 6);
hdr.status = IPCF_STATUS_OK;
hdr.payloadLen = 18;
uint32_t respSize = IPCF_HEADER_SIZE + 18 + IPCF_CRC_SIZE;
fspi.sendBinaryResp(&hdr, payload, 18, respSize, portMAX_DELAY);
}
void CommandProcessor::cmdNetSocket(const t_IpcFrameHdr &frame)
{
t_IpcFrameHdr hdr = {};
hdr.frameType = IPCF_TYPE_RESPONSE;
hdr.command = frame.command;
uint8_t sockNum = frame.diskNo;
uint8_t op = (uint8_t)(frame.sectorCount & 0xFF);
uint8_t protocol = (uint8_t)frame.filename[2];
uint16_t port = ((uint8_t)frame.filename[0] << 8) | (uint8_t)frame.filename[1];
uint32_t ipAddr = frame.fileOffset;
if (sockNum >= 4)
{
hdr.status = IPCF_STATUS_ERR;
hdr.payloadLen = 0;
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
return;
}
uint8_t resultStatus = 0x00; // SOCK_CLOSED by default.
switch (op)
{
case 0x01: // OPEN
{
int type = (protocol == 1) ? SOCK_STREAM : SOCK_DGRAM;
int fd = socket(AF_INET, type, 0);
if (fd >= 0)
{
netSockFd[sockNum] = fd;
resultStatus = (protocol == 1) ? 0x13 : 0x22; // SOCK_INIT or SOCK_UDP.
ESP_LOGI("NET", "Socket %d opened: fd=%d proto=%d", sockNum, fd, protocol);
}
else
{
ESP_LOGE("NET", "Socket %d open failed: errno=%d", sockNum, errno);
}
break;
}
case 0x04: // CONNECT
{
int fd = netSockFd[sockNum];
if (fd < 0)
break;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = ipAddr; // Already in network byte order.
addr.sin_port = htons(port);
ESP_LOGI("NET", "Socket %d connecting to %d.%d.%d.%d:%d",
(int)sockNum, (int)(ipAddr & 0xFF), (int)((ipAddr >> 8) & 0xFF), (int)((ipAddr >> 16) & 0xFF), (int)((ipAddr >> 24) & 0xFF), (int)port);
// Non-blocking connect with select() timeout to avoid blocking the SPI command loop.
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
int ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
if (ret == 0)
{
// Immediate connect (localhost or cached).
resultStatus = 0x17; // SOCK_ESTABLISHED.
ESP_LOGI("NET", "Socket %d connected (immediate)", sockNum);
}
else if (errno == EINPROGRESS)
{
// Wait for connection with 5-second timeout via select().
fd_set wfds;
FD_ZERO(&wfds);
FD_SET(fd, &wfds);
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
int sel = select(fd + 1, NULL, &wfds, NULL, &tv);
if (sel > 0)
{
// Check if connect succeeded.
int err = 0;
socklen_t errLen = sizeof(err);
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &errLen);
if (err == 0)
{
resultStatus = 0x17; // SOCK_ESTABLISHED.
ESP_LOGI("NET", "Socket %d connected", sockNum);
}
else
{
resultStatus = 0x00;
ESP_LOGE("NET", "Socket %d connect error: %d", sockNum, err);
close(fd);
netSockFd[sockNum] = -1;
}
}
else
{
resultStatus = 0x00; // Timeout or error.
ESP_LOGE("NET", "Socket %d connect timeout", sockNum);
close(fd);
netSockFd[sockNum] = -1;
}
}
else
{
resultStatus = 0x00; // SOCK_CLOSED on failure.
ESP_LOGE("NET", "Socket %d connect failed: errno=%d", sockNum, errno);
close(fd);
netSockFd[sockNum] = -1;
}
// Restore blocking mode if still open.
if (netSockFd[sockNum] >= 0)
fcntl(fd, F_SETFL, flags);
break;
}
case 0x02: // LISTEN
{
int fd = netSockFd[sockNum];
if (fd < 0)
break;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0 &&
listen(fd, 1) == 0)
{
resultStatus = 0x14; // SOCK_LISTEN.
ESP_LOGI("NET", "Socket %d listening on port %d", sockNum, port);
}
else
{
ESP_LOGE("NET", "Socket %d listen failed: errno=%d", sockNum, errno);
}
break;
}
case 0x10: // CLOSE
case 0x08: // DISCON
{
int fd = netSockFd[sockNum];
if (fd >= 0)
{
if (op == 0x08)
shutdown(fd, SHUT_WR);
close(fd);
netSockFd[sockNum] = -1;
ESP_LOGI("NET", "Socket %d closed", sockNum);
}
resultStatus = 0x00; // SOCK_CLOSED.
break;
}
default:
ESP_LOGW("NET", "Unknown socket op: 0x%02X", op);
break;
}
// Send 1-byte response with new socket status.
hdr.status = IPCF_STATUS_OK;
hdr.payloadLen = 1;
uint32_t respSize = IPCF_HEADER_SIZE + 1 + IPCF_CRC_SIZE;
fspi.sendBinaryResp(&hdr, &resultStatus, 1, respSize, portMAX_DELAY);
}
void CommandProcessor::cmdNetSend(const t_IpcFrameHdr &frame)
{
t_IpcFrameHdr hdr = {};
hdr.frameType = IPCF_TYPE_RESPONSE;
hdr.command = frame.command;
uint8_t sockNum = frame.diskNo;
if (sockNum >= 4 || netSockFd[sockNum] < 0)
{
hdr.status = IPCF_STATUS_ERR;
hdr.payloadLen = 0;
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
return;
}
// Receive the T2 write payload (data to send over network).
// Inline T2 receive: same logic as SDCard::receiveWritePayload but without private access.
uint32_t txLen = frame.payloadLen;
bool gotPayload = false;
if (txLen > 0 && txLen <= IPCF_MAX_PAYLOAD)
{
uint32_t frameLen = (txLen + IPCF_CRC_SIZE + 3u) & ~3u;
spi_slave_transaction_t t2 = {};
t2.length = frameLen * 8;
t2.rx_buffer = fspi.ipcCmdBuf;
t2.tx_buffer = fspi.ipcRespBuf;
if (spi_slave_transmit(FSPI_HOST, &t2, portMAX_DELAY) == ESP_OK)
{
// Verify CRC32.
uint32_t recvCrc, calcCrc;
memcpy(&recvCrc, fspi.ipcCmdBuf + txLen, IPCF_CRC_SIZE);
calcCrc = esp_rom_crc32_le(0, fspi.ipcCmdBuf, txLen);
if (calcCrc == recvCrc)
gotPayload = true;
else
ESP_LOGE("NET", "Send payload CRC mismatch: calc=%08lx recv=%08lx", (unsigned long)calcCrc, (unsigned long)recvCrc);
}
}
int32_t sent = 0;
if (gotPayload && txLen > 0)
{
sent = send(netSockFd[sockNum], fspi.ipcCmdBuf, txLen, 0);
if (sent < 0)
{
ESP_LOGE("NET", "Socket %d send failed: errno=%d", sockNum, errno);
sent = 0;
}
else
{
ESP_LOGI("NET", "Socket %d sent %d bytes", sockNum, (int)sent);
}
}
// Response: header + CRC only (no payload).
// FSPI_sendBinaryCmd expects write-command responses to have no payload
// (it calculates T3 size as IPCF_HEADER_SIZE + CRC_SIZE for write ops).
// Including a payload causes CRC offset mismatch.
hdr.status = (sent > 0) ? IPCF_STATUS_OK : IPCF_STATUS_ERR;
hdr.payloadLen = 0;
uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE;
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
}
void CommandProcessor::cmdNetRecv(const t_IpcFrameHdr &frame)
{
t_IpcFrameHdr hdr = {};
hdr.frameType = IPCF_TYPE_RESPONSE;
hdr.command = frame.command;
uint8_t sockNum = frame.diskNo;
if (sockNum >= 4 || netSockFd[sockNum] < 0)
{
hdr.status = IPCF_STATUS_OK;
hdr.payloadLen = 0;
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
return;
}
// Set non-blocking for poll.
int flags = fcntl(netSockFd[sockNum], F_GETFL, 0);
fcntl(netSockFd[sockNum], F_SETFL, flags | O_NONBLOCK);
static uint8_t recvBuf[IPCF_MAX_PAYLOAD];
uint16_t reqSize = frame.sectorCount;
if (reqSize == 0 || reqSize > IPCF_MAX_PAYLOAD)
reqSize = IPCF_MAX_PAYLOAD;
int recvd = recv(netSockFd[sockNum], recvBuf, reqSize, 0);
// Restore blocking mode.
fcntl(netSockFd[sockNum], F_SETFL, flags);
if (recvd <= 0)
{
// No data or error — return empty response.
// Check if connection was closed by peer.
if (recvd == 0)
{
ESP_LOGI("NET", "Socket %d: peer closed connection", sockNum);
}
hdr.status = IPCF_STATUS_OK;
hdr.payloadLen = 0;
// Use flags to signal peer close: flags bit 0 = peer disconnected.
if (recvd == 0)
hdr.flags = 0x04; // Signal peer close.
fspi.sendBinaryResp(&hdr, NULL, 0, IPCF_HEADER_SIZE + IPCF_CRC_SIZE, portMAX_DELAY);
return;
}
ESP_LOGI("NET", "Socket %d recv %d bytes", sockNum, recvd);
hdr.status = IPCF_STATUS_OK;
hdr.payloadLen = (uint16_t)recvd;
uint32_t respSize = IPCF_HEADER_SIZE + recvd + IPCF_CRC_SIZE;
fspi.sendBinaryResp(&hdr, recvBuf, recvd, respSize, portMAX_DELAY);
}
void CommandProcessor::cmdNetPing(const t_IpcFrameHdr &frame)
{
t_IpcFrameHdr hdr = {};
hdr.frameType = IPCF_TYPE_RESPONSE;
hdr.command = frame.command;
uint32_t targetIp = frame.fileOffset;
// Use lwIP raw socket for ICMP echo.
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
uint32_t rtt = 0xFFFFFFFF; // Timeout sentinel.
if (sock >= 0)
{
struct timeval tv = { .tv_sec = 3, .tv_usec = 0 };
setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = targetIp;
// Build ICMP echo request.
uint8_t icmpPkt[64];
memset(icmpPkt, 0, sizeof(icmpPkt));
icmpPkt[0] = 8; // Type: Echo Request.
icmpPkt[1] = 0; // Code: 0.
icmpPkt[4] = 0x12; // ID high.
icmpPkt[5] = 0x34; // ID low.
icmpPkt[6] = 0; // Seq high.
icmpPkt[7] = 1; // Seq low.
// Checksum.
uint32_t sum = 0;
for (int i = 0; i < 64; i += 2)
sum += (icmpPkt[i] << 8) | icmpPkt[i + 1];
while (sum >> 16)
sum = (sum & 0xFFFF) + (sum >> 16);
uint16_t cksum = ~sum;
icmpPkt[2] = (uint8_t)(cksum >> 8);
icmpPkt[3] = (uint8_t)(cksum & 0xFF);
int64_t t0 = esp_timer_get_time();
int sent = sendto(sock, icmpPkt, 64, 0, (struct sockaddr *)&addr, sizeof(addr));
if (sent > 0)
{
uint8_t pingRecvBuf[128];
struct sockaddr_in from;
socklen_t fromLen = sizeof(from);
int pingRecvd = recvfrom(sock, pingRecvBuf, sizeof(pingRecvBuf), 0, (struct sockaddr *)&from, &fromLen);
if (pingRecvd > 0)
{
int64_t t1 = esp_timer_get_time();
rtt = (uint32_t)((t1 - t0) / 1000); // Convert us to ms.
ESP_LOGI("NET", "Ping %d.%d.%d.%d: RTT=%d ms",
(int)(targetIp & 0xFF), (int)((targetIp >> 8) & 0xFF), (int)((targetIp >> 16) & 0xFF), (int)((targetIp >> 24) & 0xFF), (int)rtt);
}
else
{
ESP_LOGW("NET", "Ping timeout");
}
}
close(sock);
}
else
{
ESP_LOGE("NET", "Cannot create ICMP socket: errno=%d", errno);
}
// Response: no payload (FSPI expects write-style response for non-sector commands).
// RTT encoded in sectorCount field (16-bit, ms). 0xFFFF = timeout.
hdr.status = (rtt < 0xFFFFFFFF) ? IPCF_STATUS_OK : IPCF_STATUS_ERR;
hdr.payloadLen = 0;
hdr.sectorCount = (uint16_t)(rtt < 0xFFFF ? rtt : 0xFFFF);
uint32_t respSize = IPCF_HEADER_SIZE + IPCF_CRC_SIZE;
fspi.sendBinaryResp(&hdr, NULL, 0, respSize, portMAX_DELAY);
}
// ---------------------------------------------------------------------------
// start — spawn the waitForCommand task.
// ---------------------------------------------------------------------------
void CommandProcessor::start(void)
{
init();
xTaskCreate([](void *param) { static_cast<CommandProcessor *>(param)->waitForCommand(); }, "waitForCommand", TASK_STACK_SIZE, this, 12, NULL);
}
// ---------------------------------------------------------------------------
// processCommand — binary opcode dispatch.
//
// O(1) switch dispatch on frame.command (uint8_t), replacing the previous
// O(log n) std::map lookup on 3-char ASCII strings.
// ---------------------------------------------------------------------------
void CommandProcessor::processCommand(const t_IpcFrameHdr &frame)
{
ESP_LOGD(CMDPROCTAG, "CMD opcode=%02X seq=%u file=%.32s", frame.command, frame.seqNum, frame.filename);
// Any non-NOP command proves the RP2350 is alive and has moved past the
// last NOP exchange — retire any pending reverse command.
if (frame.command != IPCF_CMD_NOP && s_pendingCmd.cmd[0] != '\0')
{
if (s_pendingCmd.doneSem)
xSemaphoreGive(s_pendingCmd.doneSem);
memset(&s_pendingCmd, 0, sizeof(s_pendingCmd));
}
switch (frame.command)
{
case IPCF_CMD_NOP:
cmdNop(frame);
break;
case IPCF_CMD_RDS:
cmdReadSector(frame);
break;
case IPCF_CMD_RBURST:
cmdReadBurst(frame);
break;
case IPCF_CMD_WRS:
cmdWriteSector(frame);
break;
case IPCF_CMD_WBURST:
cmdWriteBurst(frame);
break;
case IPCF_CMD_RFILE:
case IPCF_CMD_RFD:
case IPCF_CMD_RQD:
case IPCF_CMD_RRF:
cmdReadFile(frame);
break;
case IPCF_CMD_WFILE:
cmdWriteFile(frame);
break;
case IPCF_CMD_INF:
cmdReadInfo(frame);
break;
case IPCF_CMD_DIR:
cmdReadDir(frame);
break;
case IPCF_CMD_NET_CFG:
cmdNetCfg(frame);
break;
case IPCF_CMD_NET_SOCK:
cmdNetSocket(frame);
break;
case IPCF_CMD_NET_SEND:
cmdNetSend(frame);
break;
case IPCF_CMD_NET_RECV:
cmdNetRecv(frame);
break;
case IPCF_CMD_NET_PING:
cmdNetPing(frame);
break;
default:
cmdUnknown(frame);
break;
}
}