1 Commits

25 changed files with 1858 additions and 286 deletions

View File

@@ -1 +1 @@
2.49
2.58

View File

@@ -1619,6 +1619,159 @@ esp_err_t WiFi::defaultDataGETHandler(httpd_req_t *req)
return cfgResult;
}
else if (uriStr == "backup")
{
// Stream the entire SD card as a tar archive.
// Uses POSIX/ustar tar format: 512-byte header per file + padded data.
// Streamed via chunked HTTP so memory usage is constant regardless of SD size.
httpd_resp_set_type(req, "application/x-tar");
httpd_resp_set_hdr(req, "Content-Disposition", "attachment; filename=\"picoZ80_backup.tar\"");
httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
// Recursive lambda to walk directories and stream tar entries.
// Uses a stack-based approach to avoid deep recursion.
struct TarEntry { std::string path; bool isDir; long size; time_t mtime; };
std::vector<std::string> dirStack;
dirStack.push_back(std::string(pThis->wifiCtrl.run.fsPath));
char hdr[512];
std::unique_ptr<char[]> fbuf(new char[MAX_CHUNK_SIZE]);
if (!fbuf) {
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Memory allocation failed");
return ESP_FAIL;
}
esp_err_t tarResult = ESP_OK;
int fileCount = 0;
std::string basePath = pThis->wifiCtrl.run.fsPath;
if (basePath.back() != '/') basePath += '/';
while (!dirStack.empty() && tarResult == ESP_OK)
{
std::string curDir = dirStack.back();
dirStack.pop_back();
DIR *d = opendir(curDir.c_str());
if (!d) continue;
// Collect entries first so we can close the dir handle quickly.
std::vector<TarEntry> entries;
struct dirent *ent;
while ((ent = readdir(d)) != NULL)
{
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
continue;
std::string fullPath = curDir + "/" + ent->d_name;
struct stat st;
if (stat(fullPath.c_str(), &st) != 0)
continue;
TarEntry te;
te.path = fullPath;
te.isDir = S_ISDIR(st.st_mode);
te.size = te.isDir ? 0 : st.st_size;
te.mtime = st.st_mtime;
entries.push_back(te);
}
closedir(d);
for (const auto &te : entries)
{
if (te.isDir) {
dirStack.push_back(te.path);
}
// Build relative path by stripping the SD mount prefix.
std::string relPath = te.path;
if (relPath.find(basePath) == 0)
relPath = relPath.substr(basePath.length());
if (te.isDir && relPath.back() != '/')
relPath += '/';
// Skip paths that are too long for tar (max 255 = 155 prefix + 100 name).
if (relPath.length() > 255)
continue;
// Build the 512-byte POSIX ustar header.
memset(hdr, 0, 512);
// Split into prefix (first 155 chars of dir) and name (last 100 chars).
std::string tarName, tarPrefix;
if (relPath.length() <= 100) {
tarName = relPath;
} else {
size_t splitAt = relPath.rfind('/', 99);
if (splitAt == std::string::npos) splitAt = 99;
tarPrefix = relPath.substr(0, splitAt);
tarName = relPath.substr(splitAt + 1);
}
strncpy(hdr + 0, tarName.c_str(), 100);
snprintf(hdr + 100, 8, "%07o", te.isDir ? 0755 : 0644); // mode
snprintf(hdr + 108, 8, "%07o", 0); // uid
snprintf(hdr + 116, 8, "%07o", 0); // gid
snprintf(hdr + 124, 12, "%011lo", te.isDir ? 0L : te.size); // size
snprintf(hdr + 136, 12, "%011lo", (long)te.mtime); // mtime
memset(hdr + 148, ' ', 8); // checksum placeholder
hdr[156] = te.isDir ? '5' : '0'; // typeflag
memcpy(hdr + 257, "ustar", 5); // magic
hdr[263] = '0'; hdr[264] = '0'; // version
if (!tarPrefix.empty())
strncpy(hdr + 345, tarPrefix.c_str(), 155);
// Compute header checksum (sum of all bytes, treating checksum field as spaces).
unsigned int cksum = 0;
for (int i = 0; i < 512; i++)
cksum += (unsigned char)hdr[i];
snprintf(hdr + 148, 7, "%06o", cksum);
hdr[155] = '\0';
// Send header.
if (httpd_resp_send_chunk(req, hdr, 512) != ESP_OK) {
tarResult = ESP_FAIL;
break;
}
// Send file data (not for directories).
if (!te.isDir && te.size > 0) {
FILE *f = fopen(te.path.c_str(), "rb");
if (f) {
long remaining = te.size;
while (remaining > 0 && tarResult == ESP_OK) {
size_t toRead = (remaining > MAX_CHUNK_SIZE) ? MAX_CHUNK_SIZE : remaining;
size_t got = fread(fbuf.get(), 1, toRead, f);
if (got > 0) {
if (httpd_resp_send_chunk(req, fbuf.get(), got) != ESP_OK)
tarResult = ESP_FAIL;
remaining -= got;
} else {
break;
}
}
fclose(f);
// Pad to 512-byte boundary.
int pad = (512 - (te.size % 512)) % 512;
if (pad > 0 && tarResult == ESP_OK) {
memset(hdr, 0, pad);
if (httpd_resp_send_chunk(req, hdr, pad) != ESP_OK)
tarResult = ESP_FAIL;
}
}
}
fileCount++;
}
}
// End-of-archive: two 512-byte zero blocks.
if (tarResult == ESP_OK) {
memset(hdr, 0, 512);
httpd_resp_send_chunk(req, hdr, 512);
httpd_resp_send_chunk(req, hdr, 512);
httpd_resp_send_chunk(req, NULL, 0);
ESP_LOGI(WIFITAG, "Backup complete: %d files streamed as tar", fileCount);
}
return tarResult;
}
else if (uriStr == "wifistatus")
{
// JSON endpoint for AJAX polling of WiFi + system + RP2350 status.
@@ -3699,9 +3852,9 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
htmlStr.append(" <table class=\"table table-borderless table-sm\" style=\"width:100%\">");
htmlStr.append(" <thead>");
htmlStr.append(" <tr>");
htmlStr.append(" <th style=\"width:50%;min-width:200px\"><b>Name</b></th>");
htmlStr.append(" <th style=\"width:5%\"><b>Type</b></th>");
htmlStr.append(" <th style=\"width:5%\"><b>Size (Bytes)</b></th>");
htmlStr.append(" <th style=\"width:50%;min-width:200px;cursor:pointer;\" data-sort-col=\"0\" data-sort-type=\"text\" title=\"Click to sort\"><b>Name</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
htmlStr.append(" <th style=\"width:5%;cursor:pointer;\" data-sort-col=\"1\" data-sort-type=\"text\" title=\"Click to sort\"><b>Type</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
htmlStr.append(" <th style=\"width:5%;cursor:pointer;\" data-sort-col=\"2\" data-sort-type=\"num\" title=\"Click to sort\"><b>Size (Bytes)</b> <i class=\"fa fa-sort\" style=\"color:#999;\"></i></th>");
htmlStr.append(" <th style=\"text-align:left;width:10%\"><b>Action</b></th>");
htmlStr.append(" </tr>");
htmlStr.append(" </thead>");
@@ -3769,24 +3922,50 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
"</div></td></tr>\n");
}
int entryCnt = 1;
std::string fileToStat;
// Collect all directory entries, then sort: directories first, then alphabetical (case-insensitive).
struct t_DirEntry {
std::string name;
bool isDir;
long size;
};
std::vector<t_DirEntry> dirEntries;
while ((entry = readdir(dir)) != NULL)
{
entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
fileToStat = sdpath + "/" + entry->d_name;
std::string fileToStat = sdpath + "/" + entry->d_name;
if (stat(fileToStat.c_str(), &entryStat) == -1)
{
ESP_LOGE(WIFITAG, "Failed to subdir stat %s : %s", entrytype, fileToStat.c_str());
ESP_LOGE(WIFITAG, "Failed to stat %s", fileToStat.c_str());
continue;
}
sprintf(entrysize, "%ld", entryStat.st_size);
t_DirEntry de;
de.name = entry->d_name;
de.isDir = (entry->d_type == DT_DIR);
de.size = entryStat.st_size;
dirEntries.push_back(de);
}
closedir(dir);
std::sort(dirEntries.begin(), dirEntries.end(), [](const t_DirEntry &a, const t_DirEntry &b) {
// Directories first, then files. Within each group, case-insensitive alphabetical.
if (a.isDir != b.isDir) return a.isDir > b.isDir;
std::string al = a.name, bl = b.name;
std::transform(al.begin(), al.end(), al.begin(), ::tolower);
std::transform(bl.begin(), bl.end(), bl.begin(), ::tolower);
return al < bl;
});
int entryCnt = 1;
for (const auto &de : dirEntries)
{
entrytype = de.isDir ? "directory" : "file";
sprintf(entrysize, "%ld", de.size);
htmlStr.append("<tr>");
htmlStr.append("<td>");
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=ren\">");
htmlStr.append("<input style=\"width:100%;\" type=\"text\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"oldname\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input style=\"width:100%;\" type=\"text\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"oldname\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("</td>");
@@ -3806,14 +3985,14 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
// Slot 2: Open Dir (dirs) or Download (files)
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
if (entry->d_type == DT_DIR)
if (de.isDir)
{
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=dir\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory);
if (htmlStr.back() != '/')
htmlStr.append("/");
htmlStr.append(entry->d_name).append("\">");
htmlStr.append(de.name).append("\">");
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"dir\" class=\"fa fa-folder-open-o wm-button-small\" "
"aria-hidden=\"true\" title=\"Open\"></button>");
htmlStr.append("</form>");
@@ -3822,7 +4001,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
{
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"/data/download\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" class=\"fa fa-download wm-button-small\" aria-hidden=\"true\" "
"title=\"Download\"></button>");
@@ -3832,15 +4011,15 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
// Slot 3: Edit (text files only, blank otherwise)
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
if (entry->d_type != DT_DIR)
if (!de.isDir)
{
std::filesystem::path filePath = entry->d_name;
std::filesystem::path filePath = de.name;
if (filePath.extension() == ".txt" || filePath.extension() == ".htm" || filePath.extension() == ".js" ||
filePath.extension() == ".css" || filePath.extension() == ".json")
{
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=edit\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"edit\" class=\"fa fa-pencil wm-button-small\" "
"aria-hidden=\"true\" title=\"Edit\"></button>");
@@ -3853,7 +4032,7 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
htmlStr.append("<span style=\"display:inline-block;width:30px;\">");
htmlStr.append("<form method=\"GET\" style=\"display:inline\" action=\"?cmd=copy\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" name=\"cmd\" value=\"copy\" class=\"fa fa-copy wm-button-small\" "
"aria-hidden=\"true\" title=\"Copy\"></button>");
@@ -3866,18 +4045,17 @@ esp_err_t WiFi::sendFileManagerDir(httpd_req_t *req)
htmlStr.append("<form method=\"GET\" style=\"display:inline\">");
htmlStr.append("<input type=\"hidden\" name=\"cmd\" value=\"del\">");
htmlStr.append("<input type=\"hidden\" name=\"id\" value=\"").append(std::to_string(entryCnt)).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(entry->d_name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"name\" value=\"").append(de.name).append("\">");
htmlStr.append("<input type=\"hidden\" name=\"dir\" value=\"").append(directory).append("\">");
htmlStr.append("<button type=\"submit\" class=\"fa fa-trash wm-button-small\" "
"aria-hidden=\"true\" title=\"Delete\" onclick=\"return confirmDelete(event, '")
.append(entry->d_name)
.append(de.name)
.append("');\"></button>");
htmlStr.append("</form>");
htmlStr.append("</span>");
htmlStr.append("</td></tr>\n");
entryCnt++;
}
closedir(dir);
htmlStr.append(" </tbody>");
htmlStr.append(" </table>");
@@ -4276,7 +4454,15 @@ esp_err_t WiFi::changeDiskHandler(httpd_req_t *req, enum DRIVETYPES driveType)
.then(arr => {
const tbody = document.getElementById('listing');
tbody.innerHTML = '';
// Sort: ".." first, then directories alphabetically, then files alphabetically.
arr.sort((a, b) => {
if (a.name === '..') return -1;
if (b.name === '..') return 1;
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
arr.forEach(e => {
const tr = document.createElement('tr');
const td = document.createElement('td');

View File

@@ -321,6 +321,26 @@ void buildVersionList(WiFi::t_versionList *versionList, NVS &nvs, SDCard &sdcard
static esp_netif_t *s_usb_ncm_netif = NULL;
static volatile bool g_usbReady = false; // Set by USB task when setup completes.
// NCM connection parameters — read from JSON config "esp32.core" block.
// Defaults are used if the config doesn't specify them.
typedef struct {
bool enabled; // NCM enable/disable (default: true)
int maxRetries; // Max connection cycles: 0=forever, 5-1000 (default: 0)
int retryPeriod; // Delay between retries: 0=increasing backoff, 1-120s fixed (default: 0)
} t_ncmConfig;
static t_ncmConfig g_ncmConfig = { true, 0, 0 };
#endif
// WiFi enable flag — read from JSON config "esp32.core.wifi" (1=enable, 0=disable).
// Defaults to enabled if compiled in, disabled if not compiled in.
#if defined(CONFIG_IF_WIFI_ENABLED)
static bool g_wifiEnabled = true;
#else
static bool g_wifiEnabled = false;
#endif
#if defined(CONFIG_IF_USB_NCM_ENABLED)
static void usb_ncm_l2_free(void *h, void *buffer)
{
free(buffer);
@@ -362,18 +382,29 @@ static esp_err_t usb_ncm_recv_callback(void *buffer, uint16_t len, void *ctx)
// CommandProcessor (SPI slave) without waiting for USB delays. The 2-second
// disconnect/reconnect cycle that macOS needs would otherwise block SPI
// slave init, causing the RP2350's first sector reads to fail.
// Maximum number of disconnect/connect cycles to attempt before giving up.
static constexpr int USB_NCM_MAX_RETRIES = 3;
// Time to hold the bus disconnected so the host tears down its NCM driver.
static constexpr int USB_NCM_DISCONNECT_MS = 2500;
// Time to wait after tud_connect() for the host to begin enumeration.
static constexpr int USB_NCM_CONNECT_SETTLE_MS = 500;
// Polling interval while waiting for tud_mounted().
static constexpr int USB_NCM_POLL_MS = 100;
// Maximum time to wait for tud_mounted() after a connect before retrying.
static constexpr int USB_NCM_MOUNT_TIMEOUT_MS = 5000;
// Time to wait after mount for the host DHCP client to obtain its IP.
static constexpr int USB_NCM_DHCP_WAIT_MS = 3000;
// USB NCM connection timing parameters.
static constexpr int USB_NCM_DISCONNECT_INIT_MS = 3000; // Initial disconnect hold time.
static constexpr int USB_NCM_DISCONNECT_MAX_MS = 60000; // Maximum disconnect hold (backoff cap).
static constexpr int USB_NCM_BACKOFF_STEP_MS = 1000; // Increase disconnect by this much each retry.
static constexpr int USB_NCM_CONNECT_SETTLE_MS = 500; // Post-connect settle before polling mount.
static constexpr int USB_NCM_POLL_MS = 100; // Polling interval for mount/DHCP checks.
static constexpr int USB_NCM_MOUNT_TIMEOUT_MS = 5000; // Max wait for tud_mounted() per attempt.
static constexpr int USB_NCM_LINK_RETRIES = 3; // Link-state UP retries per mount cycle.
static constexpr int USB_NCM_POST_MOUNT_MS = 1500; // Delay after mount for host NCM driver load.
static constexpr int USB_NCM_DHCP_WAIT_MS = 3000; // DHCP wait per link-state attempt.
// Flag set when the DHCP server assigns a lease to the host.
static volatile bool g_ncmDhcpLeased = false;
// Event handler: fired when the DHCP server assigns an IP to the connected host.
static void ncm_dhcp_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) {
ip_event_ap_staipassigned_t *evt = (ip_event_ap_staipassigned_t *)event_data;
ESP_LOGI(MAINTAG, "USB NCM: DHCP lease assigned to host (IP " IPSTR ")", IP2STR(&evt->ip));
g_ncmDhcpLeased = true;
}
}
void setupUSBTask(void *pvParameters)
{
@@ -384,77 +415,40 @@ void setupUSBTask(void *pvParameters)
esp_netif_init();
esp_event_loop_create_default();
// Register for DHCP server lease events so we know when the host has an IP.
esp_event_handler_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED,
&ncm_dhcp_event_handler, NULL);
// 1. Install TinyUSB driver on the OTG peripheral (GPIO 19/20).
tinyusb_config_t tusb_cfg = {};
tusb_cfg.external_phy = false;
esp_err_t ret = tinyusb_driver_install(&tusb_cfg);
if (ret != ESP_OK) {
ESP_LOGE(MAINTAG, "USB: TinyUSB driver install failed");
g_usbReady = true; // Signal ready (even on failure) so main loop doesn't wait forever.
g_usbReady = true;
vTaskDelete(NULL);
return;
}
// 2. Disconnect/reconnect with retry loop. macOS in particular can fail to
// enumerate an NCM device on the first attempt after a long power-off period
// because stale USB driver state persists in the kernel. Rather than using
// fixed blind delays, we poll tud_mounted() to confirm the host has actually
// completed enumeration, and retry the full cycle if it hasn't.
bool mounted = false;
for (int attempt = 1; attempt <= USB_NCM_MAX_RETRIES && !mounted; attempt++)
// 2. Initialise CDC-ACM for serial logging (independent of NCM connection).
{
ESP_LOGI(MAINTAG, "USB: disconnect/connect attempt %d/%d", attempt, USB_NCM_MAX_RETRIES);
// Disconnect — hold long enough for the host to fully tear down.
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(USB_NCM_DISCONNECT_MS));
// Reconnect and let the bus settle.
tud_connect();
vTaskDelay(pdMS_TO_TICKS(USB_NCM_CONNECT_SETTLE_MS));
// Poll tud_mounted() — the host has completed enumeration when this returns true.
int waited = 0;
while (waited < USB_NCM_MOUNT_TIMEOUT_MS)
{
if (tud_mounted())
{
mounted = true;
ESP_LOGI(MAINTAG, "USB: host enumerated device after %d ms (attempt %d)", waited, attempt);
break;
}
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
waited += USB_NCM_POLL_MS;
}
if (!mounted)
{
ESP_LOGW(MAINTAG, "USB: mount timeout after %d ms (attempt %d), %s",
USB_NCM_MOUNT_TIMEOUT_MS, attempt,
attempt < USB_NCM_MAX_RETRIES ? "retrying..." : "giving up");
}
tinyusb_config_cdcacm_t acm_cfg = {};
ret = tusb_cdc_acm_init(&acm_cfg);
cdcOk = (ret == ESP_OK);
}
if (!mounted)
// 3. Initialise the NCM network class handler.
{
ESP_LOGE(MAINTAG, "USB: host did not enumerate device after %d attempts", USB_NCM_MAX_RETRIES);
tinyusb_net_config_t net_config = {};
net_config.mac_addr[0] = 0x02; net_config.mac_addr[1] = 0x02;
net_config.mac_addr[2] = 0x11; net_config.mac_addr[3] = 0x22;
net_config.mac_addr[4] = 0x33; net_config.mac_addr[5] = 0x01;
net_config.on_recv_callback = usb_ncm_recv_callback;
ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config);
ncmOk = (ret == ESP_OK);
}
// 3. Initialise CDC-ACM for serial logging.
tinyusb_config_cdcacm_t acm_cfg = {};
ret = tusb_cdc_acm_init(&acm_cfg);
cdcOk = (ret == ESP_OK);
// 4. Initialise the NCM network class handler.
tinyusb_net_config_t net_config = {};
net_config.mac_addr[0] = 0x02; net_config.mac_addr[1] = 0x02;
net_config.mac_addr[2] = 0x11; net_config.mac_addr[3] = 0x22;
net_config.mac_addr[4] = 0x33; net_config.mac_addr[5] = 0x01;
net_config.on_recv_callback = usb_ncm_recv_callback;
ret = tinyusb_net_init(TINYUSB_USBDEV_0, &net_config);
ncmOk = (ret == ESP_OK);
// 5. Create the lwIP netif so received packets are handled immediately.
// 4. Create the lwIP netif and DHCP server (persists across reconnect cycles).
if (ncmOk) {
esp_netif_ip_info_t ip_info = {};
ip_info.ip.addr = ipaddr_addr(CONFIG_IF_USB_NCM_IP);
@@ -491,45 +485,136 @@ void setupUSBTask(void *pvParameters)
esp_netif_dhcps_option(s_usb_ncm_netif, ESP_NETIF_OP_SET,
ESP_NETIF_IP_ADDRESS_LEASE_TIME, &lease_opt, sizeof(lease_opt));
esp_netif_action_start(s_usb_ncm_netif, 0, 0, 0);
// Notify the USB host that the NCM network link is up. Without this
// the host never receives a ConnectionSpeedChange notification and will
// not start its DHCP client, leaving the interface in "no IP" state.
if (mounted) {
vTaskDelay(pdMS_TO_TICKS(200)); // Let netif settle before signalling host.
tud_network_link_state(0, true);
ESP_LOGI(MAINTAG, "USB NCM: link state set to UP");
}
}
}
// 6. Redirect console to TinyUSB CDC-ACM.
// 5. Redirect console to TinyUSB CDC-ACM.
if (cdcOk) {
esp_tusb_init_console(TINYUSB_CDC_ACM_0);
}
// 7. Wait for the host to complete DHCP and bring the network link up.
// Poll esp_netif_is_netif_up() rather than using a blind delay so we
// proceed as soon as the link is ready (or time out gracefully).
if (ncmOk && mounted && s_usb_ncm_netif != NULL) {
ESP_LOGI(MAINTAG, "USB NCM: waiting for host DHCP...");
int dhcpWait = 0;
while (dhcpWait < USB_NCM_DHCP_WAIT_MS) {
if (esp_netif_is_netif_up(s_usb_ncm_netif)) {
ESP_LOGI(MAINTAG, "USB NCM: netif is up after %d ms", dhcpWait);
// 6. Signal ready so the main loop can start WiFi and the webserver immediately.
// The NCM connection will continue retrying in the background below.
ESP_LOGI(MAINTAG, "USB init complete (CDC:%s NCM:%s), starting NCM connection loop...",
cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL");
g_usbReady = true;
if (!ncmOk || s_usb_ncm_netif == NULL) {
ESP_LOGE(MAINTAG, "USB NCM: init failed, connection loop not started");
vTaskDelete(NULL);
return;
}
// 7. NCM connection loop.
// Reads config from g_ncmConfig (populated by JSON parser after g_usbReady is set).
// Allow a moment for the main loop to parse JSON config before we read g_ncmConfig.
vTaskDelay(pdMS_TO_TICKS(500));
if (!g_ncmConfig.enabled) {
ESP_LOGI(MAINTAG, "USB NCM: disabled by config (ncm=0), connection loop not started");
vTaskDelete(NULL);
return;
}
int maxCycles = g_ncmConfig.maxRetries; // 0=forever, else limit
int fixedDelay = g_ncmConfig.retryPeriod; // 0=backoff, else fixed seconds
ESP_LOGI(MAINTAG, "USB NCM: config retries=%d period=%ds", maxCycles, fixedDelay);
vTaskDelay(pdMS_TO_TICKS(50)); // Let TinyUSB task initialise.
bool warmBoot = tud_mounted();
int disconnectMs = warmBoot ? (USB_NCM_DISCONNECT_INIT_MS + USB_NCM_BACKOFF_STEP_MS)
: USB_NCM_DISCONNECT_INIT_MS;
int cycle = 0;
while (!g_ncmDhcpLeased)
{
cycle++;
// Check retry limit (0 = forever).
if (maxCycles > 0 && cycle > maxCycles) {
ESP_LOGW(MAINTAG, "USB NCM: retry limit reached (%d cycles), giving up", maxCycles);
break;
}
ESP_LOGI(MAINTAG, "USB NCM: connection cycle %d%s (%s, disconnect=%d ms)",
cycle, maxCycles > 0 ? ("/" + std::to_string(maxCycles)).c_str() : "",
(cycle == 1 && warmBoot) ? "warm reboot" : "retry", disconnectMs);
// --- Phase 1: Disconnect/reconnect ---
tud_disconnect();
vTaskDelay(pdMS_TO_TICKS(disconnectMs));
tud_connect();
vTaskDelay(pdMS_TO_TICKS(USB_NCM_CONNECT_SETTLE_MS));
// --- Phase 2: Wait for host to enumerate ---
bool mounted = false;
int waited = 0;
while (waited < USB_NCM_MOUNT_TIMEOUT_MS)
{
if (tud_mounted()) {
mounted = true;
ESP_LOGI(MAINTAG, "USB NCM: host enumerated (cycle %d, %d ms)", cycle, waited);
break;
}
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
dhcpWait += USB_NCM_POLL_MS;
waited += USB_NCM_POLL_MS;
}
if (!mounted) {
ESP_LOGW(MAINTAG, "USB NCM: mount timeout (cycle %d)", cycle);
if (fixedDelay > 0) {
disconnectMs = fixedDelay * 1000;
} else {
disconnectMs += USB_NCM_BACKOFF_STEP_MS;
if (disconnectMs > USB_NCM_DISCONNECT_MAX_MS)
disconnectMs = USB_NCM_DISCONNECT_MAX_MS;
}
continue;
}
// --- Phase 3: Link-state UP with retry ---
ESP_LOGI(MAINTAG, "USB NCM: waiting %d ms for host NCM driver...", USB_NCM_POST_MOUNT_MS);
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POST_MOUNT_MS));
for (int linkAttempt = 1; linkAttempt <= USB_NCM_LINK_RETRIES && !g_ncmDhcpLeased; linkAttempt++)
{
if (linkAttempt > 1) {
tud_network_link_state(0, false);
vTaskDelay(pdMS_TO_TICKS(500));
}
tud_network_link_state(0, true);
ESP_LOGI(MAINTAG, "USB NCM: link UP (cycle %d, link attempt %d/%d)",
cycle, linkAttempt, USB_NCM_LINK_RETRIES);
waited = 0;
while (waited < USB_NCM_DHCP_WAIT_MS && !g_ncmDhcpLeased) {
vTaskDelay(pdMS_TO_TICKS(USB_NCM_POLL_MS));
waited += USB_NCM_POLL_MS;
}
if (g_ncmDhcpLeased) {
ESP_LOGI(MAINTAG, "USB NCM: DHCP confirmed (cycle %d, link attempt %d, %d ms)",
cycle, linkAttempt, waited);
}
}
if (!g_ncmDhcpLeased) {
ESP_LOGW(MAINTAG, "USB NCM: no DHCP after link-state retries (cycle %d), full reconnect...", cycle);
if (fixedDelay > 0) {
disconnectMs = fixedDelay * 1000;
} else {
disconnectMs += USB_NCM_BACKOFF_STEP_MS;
if (disconnectMs > USB_NCM_DISCONNECT_MAX_MS)
disconnectMs = USB_NCM_DISCONNECT_MAX_MS;
}
}
// Allow a little extra time for the host DHCP client to finish even
// after the interface reports up.
vTaskDelay(pdMS_TO_TICKS(500));
}
ESP_LOGI(MAINTAG, "USB setup complete (CDC:%s NCM:%s mounted:%s)",
cdcOk ? "OK" : "FAIL", ncmOk ? "OK" : "FAIL", mounted ? "YES" : "NO");
g_usbReady = true;
if (g_ncmDhcpLeased) {
ESP_LOGI(MAINTAG, "USB NCM: connection established after %d cycle(s)", cycle);
}
vTaskDelete(NULL);
}
#endif // CONFIG_IF_USB_NCM_ENABLED
@@ -736,6 +821,60 @@ void setupJSON(NVS &nvs, SDCard &sdcard, FSPI &fspi, cJSON *config)
}
}
// WiFi enable/disable from "esp32.core.wifi" — only effective if WiFi is compiled in.
{
cJSON *wifiEnable = cJSON_GetObjectItem(coreObj, "wifi_enable");
if (cJSON_IsNumber(wifiEnable)) {
#if defined(CONFIG_IF_WIFI_ENABLED)
g_wifiEnabled = (wifiEnable->valueint != 0);
#else
g_wifiEnabled = false; // Not compiled in, always disabled.
#endif
}
ESP_LOGI(MAINTAG, "WiFi config: enabled=%d (compiled=%s)",
g_wifiEnabled,
#if defined(CONFIG_IF_WIFI_ENABLED)
"yes"
#else
"no"
#endif
);
}
// NCM connection parameters from "esp32.core" — only effective if NCM is compiled in.
#if defined(CONFIG_IF_USB_NCM_ENABLED)
{
cJSON *ncmEnable = cJSON_GetObjectItem(coreObj, "ncm");
if (cJSON_IsNumber(ncmEnable))
g_ncmConfig.enabled = (ncmEnable->valueint != 0);
cJSON *ncmRetries = cJSON_GetObjectItem(coreObj, "ncmretries");
if (cJSON_IsNumber(ncmRetries)) {
int v = ncmRetries->valueint;
if (v == 0 || (v >= 5 && v <= 1000))
g_ncmConfig.maxRetries = v;
}
cJSON *ncmPeriod = cJSON_GetObjectItem(coreObj, "ncmperiod");
if (cJSON_IsNumber(ncmPeriod)) {
int v = ncmPeriod->valueint;
if (v >= 0 && v <= 120)
g_ncmConfig.retryPeriod = v;
}
ESP_LOGI(MAINTAG, "NCM config: enabled=%d retries=%d period=%d",
g_ncmConfig.enabled, g_ncmConfig.maxRetries, g_ncmConfig.retryPeriod);
}
#else
// NCM not compiled in — if config says enabled, log a note.
{
cJSON *ncmEnable = cJSON_GetObjectItem(coreObj, "ncm");
if (cJSON_IsNumber(ncmEnable) && ncmEnable->valueint != 0) {
ESP_LOGW(MAINTAG, "NCM config: enabled in config but NCM not compiled in — ignored");
}
}
#endif
return;
}
@@ -1039,8 +1178,12 @@ extern "C"
// Start WiFi (AP or Client mode). When USB NCM has already started
// the webserver, the WiFi event handlers detect that server != NULL
// and skip the redundant startWebserver() call.
ESP_LOGW(MAINTAG, "Starting WiFi (loopCount=%d).", loopCount);
wifi->run();
if (g_wifiEnabled) {
ESP_LOGW(MAINTAG, "Starting WiFi (loopCount=%d).", loopCount);
wifi->run();
} else {
ESP_LOGW(MAINTAG, "WiFi disabled by config (wifi_enable=0).");
}
#endif
wifiStarted = true;
}

View File

@@ -1 +1 @@
2.66
2.74

View File

@@ -138,7 +138,11 @@
<div class="col-lg-12">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-file"></i> SD Card Directory</h3>
<h3 class="panel-title"><i class="fa fa-file"></i> SD Card Directory
<button type="button" class="btn btn-default btn-xs" style="float:right;margin-top:-2px;" onclick="backupSD()" title="Download entire SD card as a tar archive">
<i class="fa fa-download"></i> Backup SD
</button>
</h3>
</div>
<div class="panel-body">
<div class="table-responsive">

View File

@@ -401,6 +401,13 @@ var ConfigGUI = (function($) {
$tbody.html('<tr><td style="color:#777;">Empty directory</td></tr>');
return;
}
// Sort: ".." first, then directories alphabetically, then files alphabetically.
arr.sort(function(a, b) {
if (a.name === '..') return -1;
if (b.name === '..') return 1;
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
});
$.each(arr, function(i, entry) {
var $tr = $('<tr>');
var $td = $('<td style="font-size:12px;cursor:pointer;">');
@@ -471,7 +478,7 @@ var ConfigGUI = (function($) {
// Core Settings Renderer
// -----------------------------------------------------------------------
function renderCoreSection(containerId, coreObj, title) {
function renderCoreSection(containerId, coreObj, title, isPartition) {
var id = uid('core');
var html = '<div class="cfg-section">';
html += '<div class="panel panel-info">';
@@ -506,6 +513,18 @@ var ConfigGUI = (function($) {
html += '<span class="help-block col-sm-6" style="font-size:11px;">50 - 166 MHz (warning above 133 MHz)</span>';
html += '</div>';
// z80refresh — only shown for partition cores (not global RP2350 core).
if (isPartition) {
var refreshChk = coreObj.z80refresh ? ' checked' : '';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">Z80 Refresh</label>';
html += '<div class="col-sm-3">';
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="z80refresh"' + refreshChk + ' title="Enable Z80 DRAM refresh cycles during virtual memory fetches"> Enable</div>';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">Insert DRAM refresh cycles when executing from virtual ROM/RAM (needed when mixing virtual ROM with physical DRAM)</span>';
html += '</div>';
}
html += '</form>';
html += '</div></div></div></div>';
@@ -516,13 +535,17 @@ var ConfigGUI = (function($) {
}
// Collect core settings from the form back into an object.
function collectCore(containerId) {
function collectCore(containerId, isPartition) {
var $el = $('#' + containerId);
return {
var obj = {
voltage: parseFloat($el.find('[data-field="voltage"]').val()) || 1.10,
cpufreq: mhzToFreq($el.find('[data-field="cpufreq"]').val()) || 240000000,
psramfreq: mhzToFreq($el.find('[data-field="psramfreq"]').val()) || 133000000
};
if (isPartition) {
obj.z80refresh = $el.find('[data-field="z80refresh"]').is(':checked') ? 1 : 0;
}
return obj;
}
// -----------------------------------------------------------------------
@@ -988,14 +1011,23 @@ var ConfigGUI = (function($) {
collectValue: function($td) {
return $td.find('[data-field="param-value"]').val() || '';
}
},
ip: {
label: 'IP Address',
renderValue: function(val) {
return '<input type="text" data-field="param-value" value="' + (val || '') + '"'
+ ' placeholder="xxx.xxx.xxx.xxx[:port]"'
+ ' title="IP address of the netfs fileserver with optional port (e.g. 192.168.1.100 or 192.168.1.100:8080)"'
+ ' style="font-size:11px;width:200px;font-family:monospace;">';
},
collectValue: function($td) {
return $td.find('[data-field="param-value"]').val() || '';
}
}
// Future parameter types can be added here, e.g.:
// integer: { label: 'Integer', renderValue: function(val) { ... }, collectValue: function($td) { ... } }
// string: { label: 'String', renderValue: function(val) { ... }, collectValue: function($td) { ... } }
};
// List of type keys for the dropdown, in display order.
var paramTypeOrder = ['file'];
var paramTypeOrder = ['file', 'ip'];
// Detect which param type a config object uses (by checking which known key is present).
function detectParamType(paramObj) {
@@ -1203,6 +1235,20 @@ var ConfigGUI = (function($) {
$(this).closest('[data-driver-idx]').remove();
});
// Bind add/remove/browse for driver-level ROM entries.
$container.on('click', '[data-action="add-drv-rom"]', function() {
var $tbody = $(this).closest('[data-section="drv-roms"]').find('table[data-section="drv-rom"] tbody');
$tbody.append(renderDrvRomRow({ enable: 0, file: '' }));
});
$container.on('click', '[data-action="remove-drv-rom"]', function() {
if (confirm('Remove this system ROM entry?'))
$(this).closest('tr').remove();
});
$container.on('click', '[data-action="browse-drv-rom"]', function() {
var $input = $(this).closest('td').find('[data-field="file"]');
openFileBrowser($input);
});
// Bind remove-interface handler
$container.on('click', '[data-action="remove-interface"]', function(e) {
e.stopPropagation();
@@ -1349,6 +1395,18 @@ var ConfigGUI = (function($) {
});
}
// Render a single driver-level ROM row (simple: enable + file, no loadaddr).
function renderDrvRomRow(rom) {
var chk = rom.enable ? ' checked' : '';
var html = '<tr>';
html += '<td style="width:30px;text-align:center;"><input type="checkbox" data-field="enable"' + chk + ' title="Enable this ROM to overwrite the host ROM"></td>';
html += '<td><div class="input-group" style="width:280px;"><input type="text" data-field="file" value="' + (rom.file || '') + '" style="font-size:11px;" title="ROM image file path on the SD card">';
html += '<span class="input-group-btn"><button type="button" class="btn btn-default btn-xs" data-action="browse-drv-rom" title="Browse SD card for ROM file" style="height:22px;padding:1px 6px;"><i class="fa fa-folder-open"></i></button></span></div></td>';
html += '<td style="width:30px;"><button type="button" class="btn btn-danger btn-xs cfg-btn-remove" data-action="remove-drv-rom" title="Remove this ROM entry"><i class="fa fa-times"></i></button></td>';
html += '</tr>';
return html;
}
function renderDriver(driver, drvIdx) {
var drvId = uid('drv');
var chk = driver.enable ? ' checked' : '';
@@ -1383,6 +1441,25 @@ var ConfigGUI = (function($) {
html += '<label style="font-size:12px;" title="Enable or disable this driver"><input type="checkbox" data-field="drv-enable"' + chk + ' title="Enable this driver"> Enabled</label>';
html += '</div>';
// Driver-level ROMs — system ROMs that overwrite the host's ROMs when enabled.
// The driver knows the load layout so no loadaddr is needed, just file + enable.
html += '<div data-section="drv-roms" style="margin-bottom:10px;">';
html += '<h5 style="font-size:13px;font-weight:bold;margin-bottom:8px;border-bottom:1px solid #eee;padding-bottom:4px;">System ROMs</h5>';
html += '<table class="table table-condensed table-bordered cfg-rom-table" data-section="drv-rom" style="width:auto !important;">';
html += '<thead><tr>';
html += '<th title="Enable loading this ROM to overwrite the host ROM">En</th>';
html += '<th title="ROM image file on the SD card">File</th>';
html += '<th></th>';
html += '</tr></thead><tbody>';
if (driver.rom && driver.rom.length > 0) {
for (var dr = 0; dr < driver.rom.length; dr++) {
html += renderDrvRomRow(driver.rom[dr]);
}
}
html += '</tbody></table>';
html += '<button type="button" class="btn btn-success btn-xs cfg-btn-add" data-action="add-drv-rom" title="Add a system ROM file"><i class="fa fa-plus"></i> Add ROM</button>';
html += '</div>';
// Interfaces - always show section with Add button
html += '<div data-section="interfaces">';
html += '<h5 style="font-size:13px;font-weight:bold;margin-bottom:8px;border-bottom:1px solid #eee;padding-bottom:4px;">Interfaces</h5>';
@@ -1415,6 +1492,19 @@ var ConfigGUI = (function($) {
type: $drv.find('[data-field="drv-type"]').first().val()
};
// Collect driver-level ROMs
var drvRoms = [];
$drv.find('table[data-section="drv-rom"] tbody tr').each(function() {
var $tr = $(this);
drvRoms.push({
enable: $tr.find('[data-field="enable"]').is(':checked') ? 1 : 0,
file: $tr.find('[data-field="file"]').val() || ''
});
});
if (drvRoms.length > 0) {
driver.rom = drvRoms;
}
// Collect interfaces
var $ifs = $drv.find('[data-if-idx]');
if ($ifs.length > 0) {
@@ -1460,6 +1550,46 @@ var ConfigGUI = (function($) {
html += '<span class="help-block col-sm-6" style="font-size:11px;">Operating mode</span>';
html += '</div>';
// WiFi settings
html += '<hr style="margin:10px 0;">';
html += '<div style="font-size:12px;font-weight:bold;margin-bottom:8px;">Network Interfaces</div>';
var wifiChk = (espCore.wifi_enable === undefined || espCore.wifi_enable) ? ' checked' : '';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">WiFi Enable</label>';
html += '<div class="col-sm-3">';
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="wifi_enable"' + wifiChk + ' title="Enable WiFi network interface"> Enable</div>';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">Enable/disable WiFi (requires reboot). Ignored if WiFi not compiled in.</span>';
html += '</div>';
// NCM (USB Network) settings
var ncmChk = (espCore.ncm === undefined || espCore.ncm) ? ' checked' : '';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">NCM Enable</label>';
html += '<div class="col-sm-3">';
html += '<div style="padding-top:5px;"><input type="checkbox" data-field="ncm"' + ncmChk + ' title="Enable USB NCM network interface"> Enable</div>';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">Enable/disable USB NCM network (requires reboot)</span>';
html += '</div>';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">NCM Retries</label>';
html += '<div class="col-sm-3">';
html += '<input type="number" class="form-control" data-field="ncmretries" min="0" max="1000" value="' + (espCore.ncmretries || 0) + '" title="Max connection retry cycles (0=forever, 5-1000)">';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">0 = retry forever, 5-1000 = max attempts</span>';
html += '</div>';
html += '<div class="form-group">';
html += '<label class="col-sm-3 control-label">NCM Retry Period</label>';
html += '<div class="col-sm-3">';
html += '<input type="number" class="form-control" data-field="ncmperiod" min="0" max="120" value="' + (espCore.ncmperiod || 0) + '" title="Delay between retries in seconds (0=increasing backoff, 1-120=fixed)">';
html += '</div>';
html += '<span class="help-block col-sm-6" style="font-size:11px;">0 = increasing backoff (1s per retry, max 60s), 1-120 = fixed seconds</span>';
html += '</div>';
html += '</form>';
html += '</div></div></div></div>';
@@ -1469,8 +1599,12 @@ var ConfigGUI = (function($) {
function collectESP32Core(containerId) {
var $el = $('#' + containerId);
return {
device: $el.find('[data-field="device"]').val() || 'Z80',
mode: parseInt($el.find('[data-field="mode"]').val(), 10) || 0
device: $el.find('[data-field="device"]').val() || 'Z80',
mode: parseInt($el.find('[data-field="mode"]').val(), 10) || 0,
wifi_enable: $el.find('[data-field="wifi_enable"]').is(':checked') ? 1 : 0,
ncm: $el.find('[data-field="ncm"]').is(':checked') ? 1 : 0,
ncmretries: parseInt($el.find('[data-field="ncmretries"]').val(), 10) || 0,
ncmperiod: parseInt($el.find('[data-field="ncmperiod"]').val(), 10) || 0
};
}
@@ -1587,18 +1721,18 @@ var ConfigGUI = (function($) {
var z80 = rp.z80 || [];
// RP2350 Global Core
renderCoreSection('cfg-rp2350-core', rp.core || {}, 'RP2350 Global Core Settings');
renderCoreSection('cfg-rp2350-core', rp.core || {}, 'RP2350 Global Core Settings', false);
// Partition 1 (z80[1])
var p1 = z80[1] || {};
renderCoreSection('cfg-p1-core', p1.core || {}, 'Partition 1 Core Settings');
renderCoreSection('cfg-p1-core', p1.core || {}, 'Partition 1 Core Settings', true);
renderMemorySection('cfg-p1-memory', p1.memory || []);
renderIOSection('cfg-p1-io', p1.io || []);
renderDriversSection('cfg-p1-drivers', p1.drivers || []);
// Partition 2 (z80[2])
var p2 = z80[2] || {};
renderCoreSection('cfg-p2-core', p2.core || {}, 'Partition 2 Core Settings');
renderCoreSection('cfg-p2-core', p2.core || {}, 'Partition 2 Core Settings', true);
renderMemorySection('cfg-p2-memory', p2.memory || []);
renderIOSection('cfg-p2-io', p2.io || []);
renderDriversSection('cfg-p2-drivers', p2.drivers || []);
@@ -1619,20 +1753,20 @@ var ConfigGUI = (function($) {
function collectConfig() {
var cfg = {
rp2350: {
core: collectCore('cfg-rp2350-core'),
core: collectCore('cfg-rp2350-core', false),
z80: [
// z80[0] = Bootloader (currently empty)
{},
// z80[1] = Partition 1
{
core: collectCore('cfg-p1-core'),
core: collectCore('cfg-p1-core', true),
memory: collectMemory('cfg-p1-memory'),
io: collectIO('cfg-p1-io'),
drivers: collectDrivers('cfg-p1-drivers')
},
// z80[2] = Partition 2
{
core: collectCore('cfg-p2-core'),
core: collectCore('cfg-p2-core', true),
memory: collectMemory('cfg-p2-memory'),
io: collectIO('cfg-p2-io'),
drivers: collectDrivers('cfg-p2-drivers')
@@ -1756,6 +1890,8 @@ var ConfigGUI = (function($) {
}
showMsg(msg, 'alert-success');
$('#cfgSaveBtn').prop('disabled', false);
// Reload the config from the device to reflect the saved state.
setTimeout(function() { fetchConfig(); }, 1000);
},
error: function(jqXHR, textStatus, errorThrown) {
var msg = jqXHR.responseText || 'Failed to save configuration: ' + (errorThrown || textStatus);

View File

@@ -1,5 +1,20 @@
var lastStatus = 0;
// Backup the entire SD card as a tar download.
function backupSD() {
if (!confirm('Download the entire SD card as a tar archive?\n\nThis may take several minutes depending on the SD card size and connection speed.'))
return;
// Create a temporary hidden link to trigger the download.
var a = document.createElement('a');
a.href = '/data/backup';
a.download = 'picoZ80_backup.tar';
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Confirm delete with "Don't ask again" checkbox.
// Uses localStorage to persist the preference across sessions.
// To re-enable: localStorage.removeItem('skipDeleteConfirm') in browser console.
@@ -126,6 +141,11 @@ function uploadFile() {
var fileSize = file.size;
var xhttp = new XMLHttpRequest();
// Detect if the file is an archive that the server will unpack.
var fileName = file.name.toLowerCase();
var isArchive = fileName.endsWith('.tar') || fileName.endsWith('.gz') ||
fileName.endsWith('.tgz') || fileName.endsWith('.tar.gz');
// Track upload progress.
xhttp.upload.addEventListener("progress", function(e) {
if (e.lengthComputable && progressBar && progressText) {
@@ -143,14 +163,24 @@ function uploadFile() {
}
});
// When upload transfer completes, show "Unpacking..." for archives while
// waiting for the server to finish processing and send its response.
xhttp.upload.addEventListener("load", function() {
if (isArchive && progressBar && progressText) {
progressBar.style.width = "100%";
progressBar.style.background = "#f0ad4e"; // amber/orange
progressText.textContent = "Unpacking archive on device... please wait";
}
});
xhttp.onreadystatechange = function()
{
if (xhttp.readyState == 4)
{
if (xhttp.status == 200)
{
if (progressBar) progressBar.style.width = "100%";
if (progressText) progressText.textContent = "Upload complete!";
if (progressBar) { progressBar.style.width = "100%"; progressBar.style.background = "#5cb85c"; }
if (progressText) progressText.textContent = isArchive ? "Unpacked successfully!" : "Upload complete!";
setTimeout(function() { location.reload(); }, 1000);
} else if (xhttp.status == 0)
{
@@ -251,3 +281,88 @@ function mkdir() {
alert('Failed to create directory');
});
}
// ---------------------------------------------------------------------------
// Client-side column sorting for the file manager table.
// Clicking a column header toggles ascending/descending sort.
// The first two rows (up-level link + upload controls) are kept at the top;
// only the file/directory data rows are sorted.
// ---------------------------------------------------------------------------
(function() {
// Track current sort state.
var currentCol = -1;
var ascending = true;
document.addEventListener('click', function(e) {
var th = e.target.closest('th[data-sort-col]');
if (!th) return;
var col = parseInt(th.getAttribute('data-sort-col'), 10);
var sortType = th.getAttribute('data-sort-type') || 'text';
var table = th.closest('table');
if (!table) return;
var tbody = table.querySelector('tbody');
if (!tbody) return;
// Toggle direction if clicking the same column, otherwise ascending.
if (col === currentCol) {
ascending = !ascending;
} else {
currentCol = col;
ascending = true;
}
// Collect all rows. The first rows may be [up level] and upload controls —
// identify data rows by having a form with cmd=ren (the rename form).
var allRows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
var fixedRows = [];
var dataRows = [];
allRows.forEach(function(row) {
if (row.querySelector('input[name="oldname"]')) {
dataRows.push(row);
} else {
fixedRows.push(row);
}
});
// Sort data rows.
dataRows.sort(function(a, b) {
var cellA = a.cells[col];
var cellB = b.cells[col];
if (!cellA || !cellB) return 0;
var valA, valB;
if (sortType === 'num') {
// Extract numeric value (from size column text content).
valA = parseFloat(cellA.textContent.replace(/[^0-9.-]/g, '')) || 0;
valB = parseFloat(cellB.textContent.replace(/[^0-9.-]/g, '')) || 0;
} else {
// Text sort: use the input value for column 0 (name), textContent otherwise.
var inputA = cellA.querySelector('input[name="name"]');
var inputB = cellB.querySelector('input[name="name"]');
valA = (inputA ? inputA.value : cellA.textContent).toLowerCase();
valB = (inputB ? inputB.value : cellB.textContent).toLowerCase();
}
if (valA < valB) return ascending ? -1 : 1;
if (valA > valB) return ascending ? 1 : -1;
return 0;
});
// Re-append rows: fixed rows first, then sorted data rows.
fixedRows.forEach(function(row) { tbody.appendChild(row); });
dataRows.forEach(function(row) { tbody.appendChild(row); });
// Update sort indicator icons.
table.querySelectorAll('th[data-sort-col] .fa').forEach(function(icon) {
icon.className = 'fa fa-sort';
icon.style.color = '#999';
});
var icon = th.querySelector('.fa');
if (icon) {
icon.className = ascending ? 'fa fa-sort-asc' : 'fa fa-sort-desc';
icon.style.color = '#333';
}
});
})();

View File

@@ -1 +1 @@
2.49
2.58

View File

@@ -105,8 +105,8 @@ volatile uint8_t g_dbgForceIOWait;
// IRQ 0 - SM0 - ADDRESS LOAD
// IRQ 1 - SM1 - DATA LOAD
static uint sm_addr = PIO_SM_0; // 4 words
static uint sm_data = PIO_SM_1; // 6 words
// 10 total words
static uint sm_data = PIO_SM_1; // 7 words
// 11 total words
// PIO 1 state machines.
// IRQ 0 - SM0 - CYCLE START
// IRQ 5 - SM3 - REFRESH
@@ -632,6 +632,45 @@ bool Z80CPU_configDriversFromJSON(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConf
continue;
}
/////////////////////////////
// Driver-level "rom" array
/////////////////////////////
// Optional ROM files at the driver level (not interface level).
// These are system ROMs where the driver knows the load layout (no loadaddr needed).
cJSON *drvROMArr = cJSON_GetObjectItem(driver, "rom");
if (cJSON_IsArray(drvROMArr))
{
size_t drvROMCnt = cJSON_GetArraySize(drvROMArr);
if (drvROMCnt > 0)
{
if (!cpu->_drivers.driver[validDrivers].romConfig)
cpu->_drivers.driver[validDrivers].romConfig = (t_drvROMConfig *) calloc(drvROMCnt, sizeof(t_drvROMConfig));
if (cpu->_drivers.driver[validDrivers].romConfig)
{
int validDrvROMs = 0;
for (size_t ri = 0; ri < drvROMCnt; ri++)
{
cJSON *romItem = cJSON_GetArrayItem(drvROMArr, ri);
if (!cJSON_IsObject(romItem))
continue;
cJSON *romEnable = cJSON_GetObjectItem(romItem, "enable");
if (!cJSON_IsNumber(romEnable) || romEnable->valueint != 1)
continue;
cJSON *romFile = cJSON_GetObjectItem(romItem, "file");
if (!cJSON_IsString(romFile) || strlen(romFile->valuestring) == 0)
continue;
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romFile = strdup(romFile->valuestring);
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romAddrCount = 0;
cpu->_drivers.driver[validDrivers].romConfig[validDrvROMs].romAddr = NULL;
validDrvROMs++;
}
cpu->_drivers.driver[validDrivers].romCount = validDrvROMs;
debugf("Z80 Drivers ROM: Driver ROM Maps(%d)\r\n", validDrvROMs);
}
}
}
//////////////
// "if" array
//////////////
@@ -981,14 +1020,24 @@ bool Z80CPU_configDriversFromJSON(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConf
continue;
}
// Extract fields
// Extract known key/value pairs. Each param entry has one key
// that identifies the parameter type ("file", "ip", etc.).
cJSON *file = cJSON_GetObjectItem(paramObj, "file");
cJSON *ip = cJSON_GetObjectItem(paramObj, "ip");
if (!cJSON_IsString(file) || strlen(file->valuestring) == 0)
bool hasValue = false;
if (cJSON_IsString(file) && strlen(file->valuestring) > 0)
{
continue;
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].file = strdup(file->valuestring);
hasValue = true;
}
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].file = strdup(file->valuestring);
if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
{
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParam[validParams].ip = strdup(ip->valuestring);
hasValue = true;
}
if (!hasValue)
continue;
validParams++;
}
cpu->_drivers.driver[validDrivers].ifConfig[validDriverIF].ifParamCount = validParams;
@@ -1626,6 +1675,79 @@ bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, cha
}
debugf("Z80 Drivers: enable(%d) name(%s) physical(%s)\r\n", enableDriver->valueint, driverName->valuestring, driverIsPhysical->valuestring);
/////////////////////////////
// Driver-level "rom" array
/////////////////////////////
// Driver-level ROM files (no loadaddr — the driver knows the layout).
// Parse and store to flash so Z80CPU_ReadROM can find them at startup.
cJSON *drvROMDrv = cJSON_GetObjectItem(driver, "rom");
if (cJSON_IsArray(drvROMDrv))
{
size_t drvROMDrvCount = cJSON_GetArraySize(drvROMDrv);
for (size_t ri = 0; ri < drvROMDrvCount; ri++)
{
cJSON *romItem = cJSON_GetArrayItem(drvROMDrv, ri);
if (!cJSON_IsObject(romItem))
continue;
cJSON *romEnable = cJSON_GetObjectItem(romItem, "enable");
if (cJSON_IsNumber(romEnable) && romEnable->valueint != 1)
continue;
cJSON *romFile = cJSON_GetObjectItem(romItem, "file");
if (!cJSON_IsString(romFile) || strlen(romFile->valuestring) == 0)
continue;
debugf("Z80 Drivers ROM: file(%s)\r\n", romFile->valuestring);
// Store to flash (same pattern as interface ROMs).
if (appConfig->s.romCount >= (FLASH_MAX_ROM_IMAGES - 1))
{
debugf("Error: No free slots in ROM Header for file:%s\r\n", romFile->valuestring);
continue;
}
int found = -1;
for (int idx = 0; idx < appConfig->s.romCount; idx++)
{
if (strcmp(appConfig->s.ROMS[idx].filename, romFile->valuestring) == 0)
found = idx;
}
if (found >= 0)
{
debugf("Warning: ROM (file=%s) already stored in slot %d, skipping.\r\n", romFile->valuestring, found);
continue;
}
memset((uint8_t *) romBuffer, 0, FLASH_MAX_ROM_IMAGE_SIZE);
watchdog_update();
debugf("Reading driver ROM file:%s\r\n", romFile->valuestring);
int fileSize = ESP_readFile(romFile->valuestring, NULL, NULL, NULL, &romBuffer, FLASH_MAX_ROM_IMAGE_SIZE, false, 0);
watchdog_update();
if (fileSize <= 0)
{
debugf("Error: ROM file '%s' not found or empty\r\n", romFile->valuestring);
continue;
}
if ((romNextPos + fileSize) > (FLASH_APP_START_CONFIG_POS + ((cfgApp - 1) * FLASH_APP_CONFIG_SIZE) + FLASH_APP_CONFIG_SIZE))
{
debugf("Error: ROM Image (%s, %dbytes) exceeds available FlashRAM storage\r\n", romFile->valuestring, fileSize);
continue;
}
uint32_t flashSize = ((fileSize % FLASH_SECTOR_SIZE) == 0) ? fileSize : ((fileSize / FLASH_SECTOR_SIZE) + 1) * FLASH_SECTOR_SIZE;
if ((romNextPos + flashSize) > (FLASH_APP_START_CONFIG_POS + ((cfgApp - 1) * FLASH_APP_CONFIG_SIZE) + FLASH_APP_CONFIG_SIZE))
{
debugf("Error: ROM Image (%s) rounded up to sector exceeds FlashRAM\r\n", romFile->valuestring);
continue;
}
updateFlashBytes(romNextPos, (uint8_t *) romBuffer, flashSize);
appConfig->s.ROMS[appConfig->s.romCount].size = fileSize;
appConfig->s.ROMS[appConfig->s.romCount].addr = romNextPos;
strcpy(appConfig->s.ROMS[appConfig->s.romCount].filename, romFile->valuestring);
romNextPos += flashSize;
appConfig->s.romCount++;
debugf("Z80 Drivers ROM: stored %d bytes to flash slot %d\r\n", fileSize, appConfig->s.romCount - 1);
}
}
////////////////
// "if" array
////////////////
@@ -2014,16 +2136,24 @@ bool Z80CPU_parseJSONStore(t_Z80CPU *cpu, cJSON *configRoot, uint8_t cfgApp, cha
continue;
}
// Extract fields
// Extract known key/value pairs.
cJSON *file = cJSON_GetObjectItem(paramObj, "file");
cJSON *ip = cJSON_GetObjectItem(paramObj, "ip");
if (!cJSON_IsString(file) || strlen(file->valuestring) == 0)
if (cJSON_IsString(file) && strlen(file->valuestring) > 0)
{
debugf("Z80 Drivers IF Params: file(%s)\r\n", file->valuestring);
}
else if (cJSON_IsString(ip) && strlen(ip->valuestring) > 0)
{
debugf("Z80 Drivers IF Params: ip(%s)\r\n", ip->valuestring);
}
else
{
debugf("Error: Virtual driver \"param\" item %zu, driver %zu invalid, skipping\r\n", i7, drvConfig);
result = false;
continue;
}
debugf("Z80 Drivers IF Params: file(%s)\r\n", file->valuestring);
}
}
@@ -2228,7 +2358,11 @@ int Z80CPU_init_pio(void)
}
pio_sm_set_enabled(pio_0, sm_addr, true);
// Data Bus - Setup data on the bus, tri-state on BUSACK.
// Data Bus - Setup data on the bus, tristate after /WR completes.
// After driving data, the SM waits for /WR low then high, ensuring the
// data bus is held for the entire write cycle (including wait states),
// then tristates. This prevents stale data persisting on the physical
// bus in virtual mode where no subsequent bus cycle would clear it.
offset_data = pio_add_program(pio_0, &z80_data_program);
pio_sm_config c_data = z80_data_program_get_default_config(offset_data);
sm_config_set_out_pins(&c_data, Z80_PIN_DATA_0, 8);
@@ -2236,6 +2370,13 @@ int Z80CPU_init_pio(void)
sm_config_set_out_shift(&c_data, true, true, 24); // Shift right, auto pull 24 bits (8 final direction, 8 data, 8 start direction).
pio_sm_set_consecutive_pindirs(pio_0, sm_data, Z80_PIN_DATA_0, 8, false); // Set as input initially.
pio_sm_set_consecutive_pindirs(pio_0, sm_data, Z80_PIN_BUSACK, 1, false);
// Enable internal pull-ups on D0-D7. A real Z80 has weak internal pull-ups
// on its data bus; when no device is driving, the bus drifts to 0xFF.
// Without pull-ups the RP2350 GPIOs are true high-Z — bus capacitance holds
// the last driven value indefinitely, causing false device detection in
// virtual mode (e.g. IPL FDC probe writes 0xA5, reads it back after 80µs).
for (uint i = Z80_PIN_DATA_0; i < Z80_PIN_DATA_0 + 8; i++)
gpio_pull_up(i);
sm_config_set_clkdiv(&c_data, 1.0f);
pio_sm_clear_fifos(pio_0, sm_data);
pio_sm_restart(pio_0, sm_data);
@@ -3178,19 +3319,22 @@ uint8_t __func_in_RAM(Z80CPU_readIntAck)(void *context, uint16_t address)
switch (cpu->_Z80.im)
{
case 0:
debugf("INT IM0 not implemented.\r\n");
break;
// Interrupt Mode 0
case 0:
debugf("INT IM0 not implemented.\r\n");
break;
case 2:
debugf("ReadInt:%04x, mode:%02x\r\n", address, cpu->_Z80.im);
// Get the vector from the bus, used with I register to form the address of the ISR.
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
break;
// Interrupt Mode 2 - Fetch the address to branch to from the bus.
case 2:
// Get the vector from the bus, used with I register to form the address of the ISR.
//debugf("ReadInt:%04x, mode:%02x\r\n", address, cpu->_Z80.im);
vector = Z80CPU_fetchPhysicalIntVector(cpu, address);
break;
case 1:
default:
break;
// Interrupt Mode 1 - Basic branch to 0x0038
case 1:
default:
break;
}
return (vector);
@@ -3223,14 +3367,14 @@ void __func_in_RAM(Z80CPU_ldia)(void *context)
{
// Locals.
Z_UNUSED(context)
debugf("LDIA\r\n");
//debugf("LDIA\r\n");
}
void __func_in_RAM(Z80CPU_ldra)(void *context)
{
// Locals.
Z_UNUSED(context)
debugf("LDRA\r\n");
//debugf("LDRA\r\n");
}
// Z80 RETI instruction, ie. return from interrupt.

View File

@@ -1434,7 +1434,7 @@ static void cmdSaveFile(t_Z80CPU *cpu, int argc, char **argv)
if (argc < 5)
{
shPuts("Usage: save <p|pf|v> <filename> <addr> <len>\r\n"
" p = physical read, pf = physical fetch (M1), v = virtual PSRAM bank 0\r\n"
" p = physical read, pf = physical fetch (M1), v = virtual PSRAM (full addr, e.g. 0x27D000 = bank 39)\r\n"
" filename relative to /sdcard/ on ESP32\r\n");
return;
}
@@ -1490,7 +1490,8 @@ static void cmdSaveFile(t_Z80CPU *cpu, int argc, char **argv)
}
else
{
scratch[i] = cpu->_z80PSRAM->RAM[z80Addr];
// Use full 32-bit address for virtual PSRAM access (supports bank > 0).
scratch[i] = cpu->_z80PSRAM->RAM[(addr + i) % sizeof(cpu->_z80PSRAM->RAM)];
}
}

View File

@@ -114,6 +114,17 @@ static void celestiteProcessSocketCmd(uint8_t sockNum)
*sr = SOCK_CLOSED;
break;
}
// Reset RX state for the new socket (clear stale data from previous connection).
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0 + 1] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RD0] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RD0 + 1] = 0;
celestiteCtrl->netRecvPending[sockNum] = false;
// Discard any stale RECV result from previous connection on this socket.
if (celestiteCtrl->netRecvReady && celestiteCtrl->netRecvSock == sockNum)
{
celestiteCtrl->netRecvReady = false;
}
// Queue ESP32 socket creation (fire-and-forget — local state already set).
// Don't set netSockPending so that the next operation (CONNECT/LISTEN)
// can properly track its response.
@@ -163,6 +174,16 @@ static void celestiteProcessSocketCmd(uint8_t sockNum)
*sr = SOCK_CLOSED;
celestiteCtrl->netSockPending[sockNum] = false;
celestiteCtrl->netRecvPending[sockNum] = false;
// Discard any in-flight RECV result for this socket.
// Without this, a stale RECV from a large TCP response (>2KB) survives
// through CLOSE/OPEN and corrupts the next connection's RX buffer.
if (celestiteCtrl->netRecvReady && celestiteCtrl->netRecvSock == sockNum)
{
celestiteCtrl->netRecvReady = false;
}
// Clear RSR so stale data doesn't affect the next OPEN on this socket.
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0] = 0;
celestiteCtrl->w5100Mem[sockBase + Sn_RX_RSR0 + 1] = 0;
break;
case Sn_CR_SEND:
@@ -220,7 +241,7 @@ static void celestiteW5100Reset(void)
memset(celestiteCtrl->w5100Mem, 0, W5100_MEM_SIZE);
// Set defaults per W5100 datasheet.
celestiteCtrl->w5100Mem[W5100_MR] = W5100_MR_IND; // IDM mode enabled after reset.
celestiteCtrl->w5100Mem[W5100_MR] = W5100_MR_IND | W5100_MR_AI; // IDM mode + auto-increment.
celestiteCtrl->w5100Mem[W5100_RTR0] = 0x07; // Default retry time = 200ms (0x07D0 × 100µs).
celestiteCtrl->w5100Mem[W5100_RTR0 + 1] = 0xD0;
celestiteCtrl->w5100Mem[W5100_RCR] = 0x08; // Default retry count = 8.
@@ -288,64 +309,97 @@ uint8_t Celestite_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
celestiteCtrl->romWriteUnprotected = false;
memset(celestiteCtrl->ufm, 0xFF, CELESTITE_UFM_SIZE);
// Integrated MZ-1R12 CMOS RAM — enabled if params[0].file is set.
// params[0].file = SD card backing file for MZ-1R12 (e.g., "ram/CELESTITE_R12.ram").
// params[1].file = SD card backing file for MZ-1R37 (e.g., "ram/CELESTITE_R37.ram").
// Initialise optional features to defaults.
celestiteCtrl->mz1r12Ram = NULL;
celestiteCtrl->mz1r12Size = 0;
celestiteCtrl->mz1r12Addr = 0;
celestiteCtrl->mz1r12FileName = NULL;
celestiteCtrl->mz1r12WritePending = false;
celestiteCtrl->mz1r12NextReqId = 1;
celestiteCtrl->mz1r37Ram = NULL;
celestiteCtrl->mz1r37AddrLatch = 0;
celestiteCtrl->mz1r37FileName = NULL;
celestiteCtrl->mz1r37WritePending = false;
celestiteCtrl->netSrvIP[0] = 0;
celestiteCtrl->netSrvIP[1] = 0;
celestiteCtrl->netSrvIP[2] = 0;
celestiteCtrl->netSrvIP[3] = 0;
celestiteCtrl->netSrvPort = 6800;
celestiteCtrl->cfgIndex = 0;
// Parse params for memory backing files.
if (config->ifParamCount >= 1 && config->ifParam[0].file && config->ifParam[0].file[0] != '\0')
// Parse params by key type — order and count don't matter.
// "file" params: first = MZ-1R12 backing, second = MZ-1R37 backing.
// "ip" param: NET file server address as "a.b.c.d" or "a.b.c.d:port".
int fileIdx = 0;
for (int p = 0; p < config->ifParamCount; p++)
{
// Allocate MZ-1R12 CMOS RAM (32KB, doubler to 64KB via unlock D1h+12h).
// Allocate 64KB up front so doubling doesn't need realloc.
celestiteCtrl->mz1r12Ram = (uint8_t *) calloc(1, CELESTITE_R12_DBL);
celestiteCtrl->mz1r12Size = CELESTITE_R12_SIZE; // 32KB initially.
if (celestiteCtrl->mz1r12Ram)
if (config->ifParam[p].ip && config->ifParam[p].ip[0] != '\0')
{
celestiteCtrl->mz1r12FileName = strdup(config->ifParam[0].file);
debugf("Celestite: MZ-1R12 32KB enabled, file=%s\r\n", celestiteCtrl->mz1r12FileName);
// Queue load from SD card.
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFC; // MZ-1R12 load sentinel.
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r12FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r12Ram;
msg.fileOp.size = CELESTITE_R12_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
unsigned int ip0, ip1, ip2, ip3, port;
int matched = sscanf(config->ifParam[p].ip, "%u.%u.%u.%u:%u", &ip0, &ip1, &ip2, &ip3, &port);
if (matched >= 4)
{
celestiteCtrl->netSrvIP[0] = (uint8_t) ip0;
celestiteCtrl->netSrvIP[1] = (uint8_t) ip1;
celestiteCtrl->netSrvIP[2] = (uint8_t) ip2;
celestiteCtrl->netSrvIP[3] = (uint8_t) ip3;
if (matched >= 5)
celestiteCtrl->netSrvPort = (uint16_t) port;
}
debugf("Celestite: NET server=%u.%u.%u.%u:%u\r\n",
celestiteCtrl->netSrvIP[0], celestiteCtrl->netSrvIP[1],
celestiteCtrl->netSrvIP[2], celestiteCtrl->netSrvIP[3],
celestiteCtrl->netSrvPort);
}
}
if (config->ifParamCount >= 2 && config->ifParam[1].file && config->ifParam[1].file[0] != '\0')
{
// Allocate MZ-1R37 EMM (640KB).
celestiteCtrl->mz1r37Ram = (uint8_t *) calloc(1, CELESTITE_R37_SIZE);
if (celestiteCtrl->mz1r37Ram)
else if (config->ifParam[p].file && config->ifParam[p].file[0] != '\0')
{
celestiteCtrl->mz1r37FileName = strdup(config->ifParam[1].file);
debugf("Celestite: MZ-1R37 640KB enabled, file=%s\r\n", celestiteCtrl->mz1r37FileName);
if (fileIdx == 0)
{
// First file param: MZ-1R12 CMOS RAM (32KB, doubler to 64KB via unlock D1h+12h).
// Initialise to 0xFF (empty battery-backed SRAM state). calloc zeros
// would cause the MZ-1500 IPL to think a valid extended ROM is present
// at offset 0x8000 (it checks for non-zero at that address to skip).
celestiteCtrl->mz1r12Ram = (uint8_t *) malloc(CELESTITE_R12_DBL);
if (celestiteCtrl->mz1r12Ram)
memset(celestiteCtrl->mz1r12Ram, 0xFF, CELESTITE_R12_DBL);
celestiteCtrl->mz1r12Size = CELESTITE_R12_SIZE;
if (celestiteCtrl->mz1r12Ram)
{
celestiteCtrl->mz1r12FileName = strdup(config->ifParam[p].file);
debugf("Celestite: MZ-1R12 32KB enabled, file=%s\r\n", celestiteCtrl->mz1r12FileName);
// Queue load from SD card.
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFD; // MZ-1R37 load sentinel.
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r37FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r37Ram;
msg.fileOp.size = CELESTITE_R37_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFC;
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r12FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r12Ram;
msg.fileOp.size = CELESTITE_R12_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
}
}
else if (fileIdx == 1)
{
// Second file param: MZ-1R37 EMM (640KB).
celestiteCtrl->mz1r37Ram = (uint8_t *) calloc(1, CELESTITE_R37_SIZE);
if (celestiteCtrl->mz1r37Ram)
{
celestiteCtrl->mz1r37FileName = strdup(config->ifParam[p].file);
debugf("Celestite: MZ-1R37 640KB enabled, file=%s\r\n", celestiteCtrl->mz1r37FileName);
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
msg.type = MSG_LOAD_RAMFILE;
msg.context = celestiteCtrl;
msg.requestId = 0xFD;
strncpy(msg.fileOp.filename, celestiteCtrl->mz1r37FileName, MAX_IC_FILENAME_LEN - 1);
msg.fileOp.buffer = celestiteCtrl->mz1r37Ram;
msg.fileOp.size = CELESTITE_R37_SIZE;
queue_try_add(&cpu->requestQueue, &msg);
}
}
fileIdx++;
}
}
@@ -415,6 +469,10 @@ uint8_t Celestite_Init(t_Z80CPU *cpu, t_drvIFConfig *config)
case 0x69:
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) Celestite_IO_UFM;
break;
case 0x6A:
case 0x6B:
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) Celestite_IO_Cfg;
break;
case 0x6F:
cpu->_z80PSRAM->ioPtr[idx] = (t_MemoryFunc) Celestite_IO_Unlock;
break;
@@ -523,8 +581,17 @@ uint8_t __func_in_RAM(Celestite_PollCB)(t_Z80CPU *cpu)
// Ping results use the memoReg which is already written by PollCB's ping response handling.
// NET_CFG and socket results are now handled inline in the IDM read handler.
// Periodic recv polling: throttled, only when established sockets exist.
// Clear write-pending flags periodically (~6s at 3.5MHz) so the next batch
// of writes can queue a new scheduled write. The coalesce timeouts on Core 0
// (2s for R12, 5s for R37) will have completed by then.
celestiteCtrl->pollCounter++;
if ((celestiteCtrl->pollCounter & 0x1FFFFF) == 0)
{
celestiteCtrl->mz1r12WritePending = false;
celestiteCtrl->mz1r37WritePending = false;
}
// Periodic recv polling: throttled, only when established sockets exist.
if ((celestiteCtrl->pollCounter & 0xFFF) == 0)
{
for (int s = 0; s < W5100_NUM_SOCKETS; s++)
@@ -723,27 +790,51 @@ uint8_t __func_in_RAM(Celestite_IO_DR)(t_Z80CPU *cpu, bool read, uint16_t addr,
__dmb();
uint8_t rsn = celestiteCtrl->netRecvSock;
uint32_t rlen = celestiteCtrl->netRecvLen;
celestiteCtrl->netRecvReady = false;
uint8_t rsr = celestiteCtrl->w5100Mem[rsn < W5100_NUM_SOCKETS ?
W5100_SOCK_BASE + (rsn * W5100_SOCK_SIZE) + Sn_SR : 0];
if (rsn < W5100_NUM_SOCKETS && rlen > 0)
// Only process if socket is valid and still connected.
// Discard RECV results for closed sockets (ghost data from old connection).
if (rsn >= W5100_NUM_SOCKETS || rsr == SOCK_CLOSED)
{
celestiteCtrl->netRecvReady = false;
if (rsn < W5100_NUM_SOCKETS)
celestiteCtrl->netRecvPending[rsn] = false;
}
// Only copy data when the RX buffer is empty (RSR == 0).
// If RSR > 0, leave netRecvReady=true so data is copied on the
// next DR read after the Z80 has consumed the current buffer.
else if (rsn < W5100_NUM_SOCKETS)
{
uint16_t sb = W5100_SOCK_BASE + (rsn * W5100_SOCK_SIZE);
uint16_t rxBase = W5100_RX_BASE + (rsn * 0x0800);
uint16_t rxMask = 0x07FF;
uint16_t rxRd = ((uint16_t)celestiteCtrl->w5100Mem[sb + Sn_RX_RD0] << 8) |
celestiteCtrl->w5100Mem[sb + Sn_RX_RD0 + 1];
for (uint32_t i = 0; i < rlen && i < 0x0800; i++)
{
uint16_t rxAddr = rxBase + ((rxRd + i) & rxMask);
celestiteCtrl->w5100Mem[rxAddr] = celestiteCtrl->netRecvBuf[i];
}
uint16_t curRsr = ((uint16_t)celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0] << 8) |
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0 + 1];
curRsr += (uint16_t)rlen;
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0] = (uint8_t)(curRsr >> 8);
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0 + 1] = (uint8_t)(curRsr & 0xFF);
celestiteCtrl->w5100Mem[sb + Sn_IR] |= Sn_IR_RECV;
celestiteCtrl->netRecvPending[rsn] = false;
if (curRsr == 0 || rlen == 0)
{
// Buffer empty or empty RECV result — safe to process.
celestiteCtrl->netRecvReady = false;
celestiteCtrl->netRecvPending[rsn] = false;
if (rlen > 0)
{
uint16_t rxBase = W5100_RX_BASE + (rsn * 0x0800);
uint16_t rxMask = 0x07FF;
uint16_t rxRd = ((uint16_t)celestiteCtrl->w5100Mem[sb + Sn_RX_RD0] << 8) |
celestiteCtrl->w5100Mem[sb + Sn_RX_RD0 + 1];
uint16_t copyLen = (rlen > 0x0800) ? 0x0800 : (uint16_t)rlen;
for (uint16_t i = 0; i < copyLen; i++)
{
uint16_t rxAddr = rxBase + ((rxRd + i) & rxMask);
celestiteCtrl->w5100Mem[rxAddr] = celestiteCtrl->netRecvBuf[i];
}
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0] = (uint8_t)(copyLen >> 8);
celestiteCtrl->w5100Mem[sb + Sn_RX_RSR0 + 1] = (uint8_t)(copyLen & 0xFF);
celestiteCtrl->w5100Mem[sb + Sn_IR] |= Sn_IR_RECV;
}
}
// else: RSR > 0 and rlen > 0 — leave netRecvReady=true,
// data will be copied when RSR reaches 0 (after CMD_RECV).
}
}
@@ -934,6 +1025,43 @@ uint8_t __func_in_RAM(Celestite_IO_UFM)(t_Z80CPU *cpu, bool read, uint16_t addr,
return 0;
}
// ---------------------------------------------------------------------------
// Ports 6Ah/6Bh — Configuration registers (index/data pair).
// Write index to 6Ah, read value from 6Bh. Extensible for future config items.
// Index 0-3: NET file server IP address bytes [0..3].
// Index 4: NET file server port high byte.
// Index 5: NET file server port low byte.
// ---------------------------------------------------------------------------
uint8_t __func_in_RAM(Celestite_IO_Cfg)(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data)
{
(void)cpu;
uint8_t port = addr & 0xFF;
if (port == 0x6A)
{
// Index register (write only).
if (!read)
celestiteCtrl->cfgIndex = data;
return 0xFF;
}
// Port 0x6B — data register (read only).
if (read)
{
switch (celestiteCtrl->cfgIndex)
{
case 0: return celestiteCtrl->netSrvIP[0];
case 1: return celestiteCtrl->netSrvIP[1];
case 2: return celestiteCtrl->netSrvIP[2];
case 3: return celestiteCtrl->netSrvIP[3];
case 4: return (uint8_t)(celestiteCtrl->netSrvPort >> 8);
case 5: return (uint8_t)(celestiteCtrl->netSrvPort & 0xFF);
default: return 0xFF;
}
}
return 0;
}
// ---------------------------------------------------------------------------
// Port 6Fh — Unlock register (write only, two-byte sequence: D1h then keyword).
// Keywords: 12h=double MZ-1R12, 37h=enable EMM, 05h=unprotect ROM,
@@ -1045,8 +1173,11 @@ uint8_t __func_in_RAM(Celestite_IO_R12)(t_Z80CPU *cpu, bool read, uint16_t addr,
{
celestiteCtrl->mz1r12Ram[celestiteCtrl->mz1r12Addr] = data;
// Schedule SD card write (coalesced, same pattern as standalone MZ-1R12).
if (celestiteCtrl->mz1r12FileName && celestiteCtrl->requestQueue)
// Schedule SD card write (coalesced). Only queue one message
// per coalesce window — the 2s timeout ensures the final state
// is written. Without this guard, every byte write floods the
// inter-core queue and starves other operations.
if (celestiteCtrl->mz1r12FileName && celestiteCtrl->requestQueue && !celestiteCtrl->mz1r12WritePending)
{
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
@@ -1057,8 +1188,8 @@ uint8_t __func_in_RAM(Celestite_IO_R12)(t_Z80CPU *cpu, bool read, uint16_t addr,
msg.fileOp.timeout = 2000; // 2 second coalesce window.
msg.fileOp.buffer = celestiteCtrl->mz1r12Ram;
msg.fileOp.size = celestiteCtrl->mz1r12Size;
queue_try_add(celestiteCtrl->requestQueue, &msg);
celestiteCtrl->mz1r12WritePending = true;
if (queue_try_add(celestiteCtrl->requestQueue, &msg))
celestiteCtrl->mz1r12WritePending = true;
}
}
celestiteCtrl->mz1r12Addr++;
@@ -1113,8 +1244,8 @@ uint8_t __func_in_RAM(Celestite_IO_R37Data)(t_Z80CPU *cpu, bool read, uint16_t a
{
celestiteCtrl->mz1r37Ram[fullAddr] = data;
// Schedule SD card write (coalesced).
if (celestiteCtrl->mz1r37FileName && celestiteCtrl->requestQueue)
// Schedule SD card write (coalesced, one message per window).
if (celestiteCtrl->mz1r37FileName && celestiteCtrl->requestQueue && !celestiteCtrl->mz1r37WritePending)
{
t_CoreMsg msg;
memset(&msg, 0, sizeof(msg));
@@ -1125,7 +1256,8 @@ uint8_t __func_in_RAM(Celestite_IO_R37Data)(t_Z80CPU *cpu, bool read, uint16_t a
msg.fileOp.timeout = 5000; // 5 second coalesce window (640KB is large).
msg.fileOp.buffer = celestiteCtrl->mz1r37Ram;
msg.fileOp.size = CELESTITE_R37_SIZE;
queue_try_add(celestiteCtrl->requestQueue, &msg);
if (queue_try_add(celestiteCtrl->requestQueue, &msg))
celestiteCtrl->mz1r37WritePending = true;
}
}
return 0;

View File

@@ -537,6 +537,88 @@ void MZ1500_readFileData(void *ctx, void *cfg, int filepos, char *buf, int len)
return;
}
// Callbacks for driver-level ROM loading. Each ROM type has its own callback
// that knows where to place the data. The config.json "rom" array has 3 entries:
// rom[0] = Monitor ROM I (4K) → Bank 0 at 0x0000-0x0FFF
// rom[1] = Extended ROM (6K) → Bank 0 at 0xE800-0xFFFF (E000-E7FF is hardware I/O, not enabled by ROM select)
// rom[2] = CG-ROM (4K) → Bank 39 at 0xD000-0xDFFF
// No loadaddr needed — the romIndex determines the destination.
// ROM index constants for driver-level ROM array.
#define MZ1500_ROM_MONITOR 0 // 4K Monitor ROM at 0x0000-0x0FFF.
#define MZ1500_ROM_EXTENDED 1 // 6K Extended ROM at 0xE800-0xFFFF.
#define MZ1500_ROM_CGROM 2 // 4K CG-ROM at Bank 39 0xD000-0xDFFF.
static int mz1500_romLoadIndex = 0;
void MZ1500_readDriverROM(void *ctx, void *cfg, char *buf, int len)
{
t_Z80CPU *cpu = (t_Z80CPU *) ctx;
(void)cfg;
if (cpu == NULL || buf == NULL || len <= 0)
return;
int romIdx = mz1500_romLoadIndex++;
switch (romIdx)
{
case MZ1500_ROM_MONITOR:
{
// Monitor ROM I: 4K → Bank 0 at 0x0000.
uint32_t copyLen = (len > 0x1000) ? 0x1000 : (uint32_t)len;
uint32_t dstAddr = (MZ1500_MEMBANK_0 * MEMORY_PAGE_SIZE) + 0x0000;
for (uint32_t i = 0; i < copyLen; i++)
cpu->_z80PSRAM->RAM[dstAddr + i] = buf[i];
debugf("MZ1500_ROM: Monitor ROM loaded (%d bytes → Bank 0 : 0x0000)\r\n", copyLen);
break;
}
case MZ1500_ROM_EXTENDED:
{
// Extended ROM: 6K → Bank 0 at 0xE800.
// E000-E7FF is hardware I/O (not ROM), so ROM maps E800-FFFF.
// If file is >6K (8K = full E000-FFFF dump), skip first 2K (the E000-E7FF gap).
uint32_t srcOfs = 0;
uint32_t dataLen = (uint32_t)len;
if (dataLen > 0x1800)
{
srcOfs = dataLen - 0x1800; // Skip leading bytes (E000-E7FF region).
dataLen = 0x1800;
}
uint32_t dstAddr = (MZ1500_MEMBANK_0 * MEMORY_PAGE_SIZE) + 0xE800;
for (uint32_t i = 0; i < dataLen; i++)
cpu->_z80PSRAM->RAM[dstAddr + i] = buf[srcOfs + i];
debugf("MZ1500_ROM: Extended ROM loaded (%d bytes, srcOfs=%d → Bank 0 : 0xE800)\r\n", dataLen, srcOfs);
break;
}
case MZ1500_ROM_CGROM:
{
// CG-ROM: 4K → Bank 39 at 0xD000.
uint32_t copyLen = (len > 0x1000) ? 0x1000 : (uint32_t)len;
uint32_t dstAddr = (MZ1500_MEMBANK_CGROM * MEMORY_PAGE_SIZE) + 0xD000;
for (uint32_t i = 0; i < copyLen; i++)
cpu->_z80PSRAM->RAM[dstAddr + i] = buf[i];
debugf("MZ1500_ROM: CG-ROM loaded (%d bytes → PSRAM 0x%06X)\r\n", copyLen, dstAddr);
// Dump at offset 0xA00 where Japanese/MZ-700 ROMs differ (lowercase chars).
if (copyLen > 0xA08)
{
debugf("MZ1500_ROM: CG buf[A00]: %02X %02X %02X %02X %02X %02X %02X %02X\r\n",
(uint8_t)buf[0xA00], (uint8_t)buf[0xA01], (uint8_t)buf[0xA02], (uint8_t)buf[0xA03],
(uint8_t)buf[0xA04], (uint8_t)buf[0xA05], (uint8_t)buf[0xA06], (uint8_t)buf[0xA07]);
debugf("MZ1500_ROM: PSRAM[DA00]: %02X %02X %02X %02X %02X %02X %02X %02X\r\n",
cpu->_z80PSRAM->RAM[dstAddr+0xA00], cpu->_z80PSRAM->RAM[dstAddr+0xA01],
cpu->_z80PSRAM->RAM[dstAddr+0xA02], cpu->_z80PSRAM->RAM[dstAddr+0xA03],
cpu->_z80PSRAM->RAM[dstAddr+0xA04], cpu->_z80PSRAM->RAM[dstAddr+0xA05],
cpu->_z80PSRAM->RAM[dstAddr+0xA06], cpu->_z80PSRAM->RAM[dstAddr+0xA07]);
}
break;
}
default:
debugf("MZ1500_ROM: unexpected ROM index %d, ignoring\r\n", romIdx);
break;
}
}
// Method to write out a stored ROM into the internal emulation RAM as directed by the configuration array.
// Each config element specifies a position in the ROM and the target within the memory.
void MZ1500_readROMData(void *ctx, void *cfg, char *buf, int len)
@@ -651,14 +733,13 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
MZ1500Ctrl.loDRAMen = true;
}
// E1h: D000-FFFF -> DRAM. Does NOT close PCG bank on MZ-1500 (bank switch happens behind the scenes).
// E1h: D000-EFFF -> DRAM. Does NOT close PCG bank on MZ-1500.
// F000-FFFF excluded — handled by priority logic below (PCG > DRAM > ROM).
// E000-E7FF (8255/8253 hardware I/O) must ALWAYS remain physical — skip blocks 112-115.
if (!MZ1500Ctrl.inhibit && port == 0xe1 && !MZ1500Ctrl.hiDRAMen)
{
// Enable upper 12K block as DRAM, but preserve E000-E7FF as PHYSICAL_HW.
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0xF000 / MEMORY_BLOCK_SIZE); idx++)
{
// Skip E000-E7FF (blocks 112-115) — 8255/8253 must always be on physical bus.
if (idx >= (0xE000 / MEMORY_BLOCK_SIZE) && idx < (0xE800 / MEMORY_BLOCK_SIZE))
continue;
MZ1500Ctrl.upmembankPtr[idx - (0xd000 / MEMORY_BLOCK_SIZE)] = cpu->_membankPtr[idx];
@@ -669,7 +750,7 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
if ((MZ1500Ctrl.loDRAMen && port == 0xe2) || port == 0xe4)
{
// Enable lower 4K block as Monitor ROM
// Enable lower 4K block as Monitor ROM.
for (int idx = 0x0000; idx < (0x1000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
@@ -677,11 +758,11 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
MZ1500Ctrl.loDRAMen = false;
}
// E3h: D000-FFFF -> VRAM/KEY/TIMER. Does NOT close PCG bank on MZ-1500.
// E3h/E4h: D000-EFFF -> VRAM/KEY/TIMER. Does NOT close PCG bank on MZ-1500.
// F000-FFFF excluded — handled by priority logic below.
if ((!MZ1500Ctrl.inhibit && MZ1500Ctrl.hiDRAMen && port == 0xe3) || (MZ1500Ctrl.hiDRAMen && port == 0xe4))
{
// Restore upper 12K block to Memory mapped I/O, skip E000-E7FF (always physical).
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
for (int idx = (0xd000 / MEMORY_BLOCK_SIZE); idx < (0xF000 / MEMORY_BLOCK_SIZE); idx++)
{
if (idx >= (0xE000 / MEMORY_BLOCK_SIZE) && idx < (0xE800 / MEMORY_BLOCK_SIZE))
continue;
@@ -696,11 +777,6 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
MZ1500Ctrl.inhibit = false;
if (MZ1500Ctrl.mode == MZ1500_MODE_MZ1500 && MZ1500Ctrl.pcgBankOpen)
{
// Restore F000-FFFF from saved mapping.
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)];
}
MZ1500Ctrl.pcgBankOpen = false;
}
}
@@ -711,16 +787,6 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
{
// MZ-1500: Open PCG bank at F000-FFFF.
// Bits 0-1 select: 00=CGROM, 01=PCG blue, 10=PCG red, 11=PCG green.
// Save current F000-FFFF mapping (e.g., FDD ROM) and switch to PHYSICAL
// so reads go to the real CGROM/PCG-RAM on the bus.
if (!MZ1500Ctrl.pcgBankOpen)
{
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)] = cpu->_membankPtr[idx];
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24) | (idx * MEMORY_BLOCK_SIZE);
}
}
MZ1500Ctrl.pcgBankSelect = data & 0x03;
MZ1500Ctrl.pcgBankOpen = true;
}
@@ -742,14 +808,6 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
{
if (MZ1500Ctrl.mode == MZ1500_MODE_MZ1500)
{
// MZ-1500: Close PCG bank. Restore F000-FFFF to saved mapping (FDD ROM etc).
if (MZ1500Ctrl.pcgBankOpen)
{
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)];
}
}
MZ1500Ctrl.pcgBankOpen = false;
}
else
@@ -768,6 +826,62 @@ uint8_t __func_in_RAM(MZ1500_IO_MemoryBankPorts)(t_Z80CPU *cpu, bool read, uint1
}
}
// MZ-1500 mode: compute memory mapping for D000-FFFF from current flags.
// Per manual p.62-63: $E5 affects the entire $D000~$FFFF range.
// E000-E7FF (hardware I/O) is always physical and skipped.
if (MZ1500Ctrl.mode == MZ1500_MODE_MZ1500)
{
if (MZ1500Ctrl.pcgBankOpen)
{
// $E5 active: D000-FFFF (minus E000-E7FF) changes based on bank select.
if (MZ1500Ctrl.pcgBankSelect == MZ1500_PCG_BANK_CGROM)
{
// Bank=0 (CG-ROM): D000-DFFF → PSRAM Bank 39 at D000 (virtual CG-ROM,
// allows user override via config.json rom[2]).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_CGROM << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
else
{
// Bank=1-3 (PCG planes): D000-DFFF → PHYSICAL (writes must hit real PCG RAM
// so the display hardware can read them for video output).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
// E800-FFFF → PHYSICAL when $E5 active (hardware routing).
for (int idx = (0xE800 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
else if (MZ1500Ctrl.hiDRAMen)
{
// $E1 active: D000-FFFF (minus E000-E7FF) → DRAM (Bank 1).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
if (idx >= (0xE000 / MEMORY_BLOCK_SIZE) && idx < (0xE800 / MEMORY_BLOCK_SIZE))
continue;
cpu->_membankPtr[idx] = (MEMBANK_TYPE_RAM << 24) | (MZ1500_MEMBANK_1 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
else
{
// Default ($E3/$E4): D000-DFFF=VRAM, E800-FFFF=ROM (MZ-MON-ROM II in PSRAM).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL_VRAM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
for (int idx = (0xE800 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
}
}
return (0);
}
@@ -866,19 +980,23 @@ uint8_t MZ1500_Reset(t_Z80CPU *cpu)
MZ1500Ctrl.loDRAMen = false;
MZ1500Ctrl.hiDRAMen = false;
// Reset MZ-1500-specific PCG state. Restore F000-FFFF if PCG bank was open.
if (MZ1500Ctrl.pcgBankOpen)
{
for (int idx = (0xF000 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = MZ1500Ctrl.pcgSavedBankPtr[idx - (0xF000 / MEMORY_BLOCK_SIZE)];
}
}
// Reset MZ-1500-specific PCG state.
MZ1500Ctrl.pcgBankOpen = false;
MZ1500Ctrl.pcgBankSelect = MZ1500_PCG_BANK_CGROM;
MZ1500Ctrl.pcgPriority = 0;
MZ1500Ctrl.pcgPalette = 0;
// Restore D000-FFFF to default state (hardware RESET already resets physical latches).
// D000-DFFF=VRAM, E000-E7FF stays PHYSICAL_HW, E800-FFFF=ROM (PSRAM).
for (int idx = (0xD000 / MEMORY_BLOCK_SIZE); idx < (0xE000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_PHYSICAL_VRAM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
for (int idx = (0xE800 / MEMORY_BLOCK_SIZE); idx < (0x10000 / MEMORY_BLOCK_SIZE); idx++)
{
cpu->_membankPtr[idx] = (MEMBANK_TYPE_ROM << 24) | (MZ1500_MEMBANK_0 << 16) | (idx * MEMORY_BLOCK_SIZE);
}
// Reset PIT emulation state so it re-initializes on next access.
pitInitialized = false;
@@ -1035,9 +1153,15 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
switch (idx)
{
// 0x0000:0x0FFF = Monitor ROM.
// Stays physical for fetches unless overridden by ROM config.
// Physical ROM is mirrored into PSRAM at init for bank switching reference.
// Virtual mode: runs from PSRAM (mirrored from physical during init,
// or overridden by user ROM via config.json).
case 0 ... 7:
if (!isPhysical)
{
memType = MEMBANK_TYPE_ROM;
waitStates = 1;
tCycSync = true;
}
break;
// 0x1000:0xCFFF = RAM
@@ -1062,7 +1186,7 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
memType = MEMBANK_TYPE_PHYSICAL_HW;
break;
// 0xE800:0xEFFF = Empty
// 0xE800:0xEFFF = MZ-MON-ROM II (part 1).
case 116 ... 119:
if (!isPhysical)
{
@@ -1072,13 +1196,13 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
}
break;
// 0xF000:0xFFFF = Empty
// 0xF000:0xFFFF = MZ-MON-ROM II (part 2, F88C+ unused/0xFF).
case 120 ... 127:
if (!isPhysical)
{
memType = MEMBANK_TYPE_ROM;
waitStates = 1;
tCycSync = true;
memType = MEMBANK_TYPE_ROM;
}
break;
}
@@ -1134,11 +1258,23 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
// Copy monitor ROM to Bank 0.
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_0 << 16) | 0x0000, 0x0000, 0x1000);
// Copy User ROM to Bank 0.
// Copy MZ-MON-ROM II to Bank 0 (E800-FFFF, contiguous IPL ROM).
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_0 << 16) | 0xE800, 0xE800, 0x800);
// Copy Floppy ROM to Bank 0.
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_0 << 16) | 0xF000, 0xF000, 0x1000);
// Mirror CG-ROM to Bank 39 at D000-DFFF (4K).
// The CG-ROM is only accessible via $E5 bank=0, which maps it to D000-DFFF.
// Select CG-ROM on the physical bus, copy it, then restore default state.
// User-provided CG-ROM (rom[2] in config.json) overrides this mirror.
Z80CPU_writePhysicalIO(cpu, 0xE5, 0x00); // Select CG-ROM at D000-DFFF.
Z80CPU_taskMirrorPhysicalToInternalRAM(cpu, (MZ1500_MEMBANK_CGROM << 16) | 0xD000, 0xD000, 0x1000);
Z80CPU_writePhysicalIO(cpu, 0xE4, 0x00); // Restore default memory map.
// Note: The physical CGROM chip (Japanese) is read directly by the display
// hardware for text-only programs. The virtual CG-ROM in Bank 39 is only
// visible to the Z80 CPU via E5h bank=0. Programs that use PCG will read
// the virtual CG-ROM and program PCG planes, which the display then uses.
// Text-only programs (BASIC, monitor) show physical CGROM characters.
}
// Initialise driver control state variables.
@@ -1168,6 +1304,27 @@ uint8_t MZ1500_Init(t_Z80CPU *cpu, t_FlashAppConfigHeader *appConfig, t_drvConfi
// Install task processing handler.
config->taskPtr = MZ1500_TaskProcessor;
// Load driver-level ROM(s) if provided in config.json.
// rom[0]=Monitor(4K), rom[1]=Extended(6K), rom[2]=CG-ROM(4K).
// These override the physical ROM mirrors with user-supplied ROM data.
mz1500_romLoadIndex = 0;
debugf("MZ1500_Init: romCount=%d\r\n", config->romCount);
for (int romIdx = 0; romIdx < config->romCount; romIdx++)
{
if (config->romConfig[romIdx].romFile != NULL)
{
debugf("MZ1500_Init: loading ROM '%s'\r\n", config->romConfig[romIdx].romFile);
Z80CPU_ReadROM(appConfig,
config->romConfig[romIdx].romFile,
cpu,
NULL,
MZ1500_readDriverROM,
NULL,
0,
0);
}
}
// Go through each interface and perform necessary configuration.
for (int ifIdx = 0; ifIdx < config->ifCount; ifIdx++)
{

View File

@@ -354,9 +354,11 @@ typedef struct
} t_ioReMap;
// Struct for storing interface configuration parameters.
// Each param entry is a key/value pair — the key determines the parameter type.
typedef struct
{
char *file; // Name of a file required by the interface for initialisation.
char *file; // "file" key: name of a file required by the interface.
char *ip; // "ip" key: IP address string, e.g. "192.168.1.210:6800".
} t_ifParam;
// Struct for storing a drivers interface configuration.
@@ -379,6 +381,8 @@ struct t_drvConfig
{
const char *name; // Name of the driver. 1:1 mapping with t_VirtualFuncMap.virtualFuncName
bool isPhysical; // Should the driver use physical mappings (ROM/RAM etc) or virtual?
int romCount; // Number of driver-level ROM's (e.g., system ROM replacement).
t_drvROMConfig *romConfig; // Struct array of driver-level ROM descriptions.
int ifCount; // Number of interfaces stored.
t_drvIFConfig *ifConfig; // Driver interface configuration. Each driver can have multiple optional interfaces.
t_ResetFunc resetPtr; // Pointer to a reset handler for this driver.

View File

@@ -22,6 +22,11 @@
// 68h (W) : UFM address register
// 68h (R) : UFM status (bit 7: WP, bit 1: READY, bit 0: BUSY)
// 69h (R/W) : UFM data register
// 6Ah (W) : Config index register (selects config byte)
// 6Bh (R) : Config data register (returns selected byte)
// Index 0-3: NET file server IP[0..3]
// Index 4: NET file server port high byte
// Index 5: NET file server port low byte
// 6Fh (W) : Unlock (write D1h then keyword)
//
// Credits: Board designed by Oh!Ishi (Oh!石)
@@ -210,6 +215,13 @@ typedef struct
uint8_t *mz1r37Ram; // 640KB RAM buffer.
uint32_t mz1r37AddrLatch; // Latched address bits [19:8].
char *mz1r37FileName; // SD card backing file name (NULL = no persistence).
bool mz1r37WritePending; // True while a scheduled write is queued/in-flight.
// NET file server configuration (read via ports 6Ah/6Bh).
// Parsed from ifParam[2].file = "ip:port" (e.g., "192.168.1.210:6800").
uint8_t netSrvIP[4]; // File server IP address (default 0.0.0.0 = unconfigured).
uint16_t netSrvPort; // File server TCP port (default 6800).
uint8_t cfgIndex; // Config register index (port 6Ah write).
// Network state (Phase 2).
queue_t *requestQueue; // Pointer to inter-core request queue.
@@ -256,5 +268,6 @@ uint8_t Celestite_IO_Unlock(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t dat
uint8_t Celestite_IO_R12(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t Celestite_IO_R37Latch(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t Celestite_IO_R37Data(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
uint8_t Celestite_IO_Cfg(t_Z80CPU *cpu, bool read, uint16_t addr, uint8_t data);
#endif // CELESTITE_H

View File

@@ -69,6 +69,7 @@
// Constants.
#define MZ1500_MEMBANK_0 0 // Primary RAM bank.
#define MZ1500_MEMBANK_1 1 // RAM bank to use for paging in RAM.
#define MZ1500_MEMBANK_CGROM 39 // Bank for CG-ROM storage (D000-DFFF = 4K). Banks 40-63 reserved for RFS.
#define MZ1500_UPPERMEM_BLOCKS 64 // Number of blocks in the upper paged 12K RAM.
// PCG constants.

View File

@@ -57,6 +57,13 @@
#define IPCF_CMD_NET_RECV 0x13 // Receive data from socket (response payload = data)
#define IPCF_CMD_NET_PING 0x14 // ICMP echo request (ping)
// Network file server commands (BASIC NET: device → ESP32 → PC file server)
#define IPCF_CMD_NETFS_DIR 0x20 // Get directory from file server (response: 32-byte entries)
#define IPCF_CMD_NETFS_INFO 0x21 // Get file info (filename in header, response: atrb+size+addrs)
#define IPCF_CMD_NETFS_READ 0x22 // Read file data (filename+offset in header, response: data)
#define IPCF_CMD_NETFS_WRITE 0x23 // Write file (filename+size in header, payload: data)
#define IPCF_CMD_NET_PING 0x14 // ICMP echo request (ping)
// ---------------------------------------------------------------------------
// Status codes (t_IpcFrameHdr::status — response frames only)
// ---------------------------------------------------------------------------

View File

@@ -1097,7 +1097,7 @@ void processInterCoreCommands(void)
{
responseType = MSG_LOAD_COMPLETE;
uint8_t *bufBefore = qmsg.fileOp.buffer;
for (int attempt = 0; attempt < 10; attempt++)
for (int attempt = 0; attempt < 3; attempt++)
{
qmsg.fileOp.buffer = bufBefore;
bytesXfer = ESP_readRamFile(qmsg.fileOp.filename, NULL, NULL, NULL, (uint8_t **)&qmsg.fileOp.buffer, qmsg.fileOp.size, true, 0);
@@ -1111,6 +1111,16 @@ void processInterCoreCommands(void)
success = true;
response.response.size = bytesXfer;
}
else
{
// File doesn't exist — treat as fresh uninitialised RAM board.
// Buffer is already pre-filled by the driver (0xFF for Celestite
// MZ-1R12, matching empty battery-backed SRAM). The backing file
// will be created on first save from the Z80.
debugf("RAMFILE: '%s' not found, using pre-filled buffer (%d bytes)\r\n", qmsg.fileOp.filename, (int) qmsg.fileOp.size);
success = true;
response.response.size = qmsg.fileOp.size;
}
}
else
{

View File

@@ -1 +1 @@
2.534
2.579

Binary file not shown.

View File

@@ -0,0 +1,67 @@
A2DSP: equ 14A7H
ATBL: equ 1568H
ATRB: equ 10F0H
CLR: equ 122EH
COMNT: equ 1108H
DBGCR: equ 1563H
DBGLP: equ 155BH
DLY1: equ 143FH
DLY2: equ 1442H
DTADR: equ 1104H
EXADR: equ 1106H
HTCHK: equ 1405H
HTDISP: equ 13BFH
HTEND: equ 140DH
HTNL: equ 13D4H
HTNL2: equ 13E1H
HTNL3: equ 13E6H
HTNL4: equ 13EFH
HTNL5: equ 13F8H
HTNL6: equ 13FEH
HTNXT: equ 1405H
HTTPDN: equ 1419H
HTTPGOT: equ 13A0H
HTTPRD: equ 138DH
HTTPREQ: equ 1548H
HTTPRW: equ 1376H
HTTPWD: equ 1354H
HTTPWR: equ 134BH
LOOPCNT: equ 1566H
MAINLP: equ 1228H
MEND: equ 15E8H
NAME: equ 10F1H
PDECV: equ 145BH
PH1: equ 14CBH
PH2: equ 14D8H
PHEX: equ 14BCH
PHEXV: equ 148CH
PHXN: equ 1499H
PHXN1: equ 14A2H
PNGD: equ 1274H
PNGDN: equ 1292H
PNGGOT: equ 1286H
PNGW: equ 1269H
PSTR: equ 14B4H
PVD1: equ 147CH
PVD10: equ 1467H
PVD100: equ 145EH
PVD10L: equ 1473H
PVD10N: equ 1470H
PVD1N: equ 1485H
PVRAM: equ 144CH
SIZE: equ 1102H
START: equ 1200H
S_BYT: equ 1546H
S_FAIL: equ 1539H
S_HTTP: equ 1523H
S_LOOP: equ 14F5H
S_MS: equ 1543H
S_PASS: equ 1534H
S_PING: equ 14FBH
S_TCP: equ 150CH
S_TITLE: equ 14DBH
S_TOUT: equ 153EH
TCPD: equ 130BH
TCPFL: equ 1312H
TCPOK: equ 131EH
TCPW: equ 12F3H

Binary file not shown.

View File

@@ -0,0 +1,93 @@
A2DSP: equ 170DH
ATBL: equ 1A51H
ATRB: equ 10F0H
BNRMSG: equ 172DH
BYTEMSG: equ 19C0H
COMNT: equ 1108H
CRLF: equ 19F6H
DONEMSG: equ 1A08H
DTADR: equ 1104H
EXADR: equ 1106H
FAILMSG: equ 18D0H
HTTPREQ: equ 1A27H
MEND: equ 1AD1H
MSMSG: equ 1A03H
NAME: equ 10F1H
NETMSG: equ 18D7H
OKMSG: equ 19C9H
PASSMSG: equ 18C9H
PAUSMSG: equ 19D7H
PBANNER: equ 1726H
PD10: equ 16D0H
PD100: equ 16B9H
PD10N: equ 16CDH
PD10S: equ 16C2H
PD1N: equ 16E7H
PD1P: equ 16E2H
PD1S: equ 16D9H
PDEC: equ 16B2H
PHEX: equ 16EEH
PHX1: equ 16FDH
PHX2: equ 170AH
PMSG: equ 171DH
PSTR: equ 16AAH
RESULT: equ 169AH
RPASS: equ 16A3H
RXGMSG: equ 19B6H
RXWMSG: equ 199EH
SCRHDR: equ 1A3AH
SIZE: equ 1102H
START: equ 1200H
T10MSG: equ 1875H
T11MSG: equ 1891H
T12MSG: equ 18ADH
T13DLY: equ 1407H
T13DONE: equ 141AH
T13GOT: equ 1416H
T13MSG: equ 191BH
T13WAIT: equ 13F6H
T14MSG: equ 1937H
T15DLY: equ 1463H
T15DONE: equ 147DH
T15GOT: equ 1472H
T15MSG: equ 1948H
T15WAIT: equ 1458H
T16DLY: equ 14ECH
T16DONE: equ 150BH
T16FAIL: equ 14F3H
T16MSG: equ 1964H
T16OK: equ 14FBH
T16WAIT: equ 14D5H
T17CD: equ 1581H
T17CLR: equ 161BH
T17CONN: equ 158BH
T17CW: equ 1569H
T17DISP: equ 162FH
T17DONE: equ 1685H
T17END: equ 165FH
T17FAIL: equ 167FH
T17GOT: equ 15EDH
T17HALT: equ 167DH
T17LIM: equ 1612H
T17LOK: equ 1614H
T17MSG: equ 1980H
T17NL: equ 1640H
T17NXT: equ 1657H
T17RD: equ 15DDH
T17RW: equ 15C6H
T17WR: equ 1595H
T17WRD: equ 159EH
T1MSG: equ 1779H
T2MSG: equ 1795H
T3DONE: equ 128EH
T3FAIL: equ 1288H
T3MSG: equ 17B1H
T4DONE: equ 12D6H
T4FAIL: equ 12D0H
T4MSG: equ 17CDH
T5MSG: equ 17E9H
T6MSG: equ 1805H
T7MSG: equ 1821H
T8MSG: equ 183DH
T9MSG: equ 1859H
TOMSG: equ 19F9H

View File

@@ -106,16 +106,21 @@ public start_addr:
.wrap
; State machine to output an 8bit byte onto the Z80 Data Bus.
; The state machine sets IRQ 1 to indicate it is waiting, waits for the flag
; to clear then loads the 8bit data byte out of the fifo onto the pins.
; After driving data, waits for /WR to go active (low) then inactive (high),
; indicating the write cycle has completed, then tristates the data bus.
; This prevents stale data from persisting on the physical bus in virtual mode,
; where no subsequent physical bus cycle (M1 fetch / refresh) would otherwise
; clear it. Works with any number of wait states since it tracks the actual
; /WR signal rather than using a fixed timeout.
.program z80_data
.wrap_target
public start_data:
irq set 1
wait 0 irq 1 ; Wait till control starts the sequence.
out pindirs, 8 ; Set direction to output (or input during tri-state BUSRQ).
out pins, 8 ; Output DATA (Z80_PIN_DATA_0-7)
wait 0 irq 0 ; Wait till next address change.
out pins, 8 ; Output DATA (Z80_PIN_DATA_0-7).
wait 0 gpio Z80_PIN_WR ; Wait for /WR active (low) — write cycle in progress.
wait 1 gpio Z80_PIN_WR ; Wait for /WR inactive (high) — write cycle complete.
out pindirs, 8 ; Set final direction, normally input mode.
.wrap

354
projects/tzpuPico/tools/NetFileServer/netfs.py vendored Executable file
View File

@@ -0,0 +1,354 @@
#!/usr/bin/env python3
"""
NetFS — Binary File Server for Sharp MZ BASIC Network Drive
Serves MZF files from a local directory over TCP using a simple binary protocol
designed for easy Z80 assembly parsing.
Binary Protocol:
All requests start with 1-byte command. All responses start with 1-byte status.
Status: 0x00=OK, 0xFF=error, 0xFE=end of data.
DIR (0x01):
Request: [0x01] [unit:1]
Response: [0x00] [count:1] [entry0:32] ... [entryN:32]
Each entry is 32 bytes: atrb(1) + name(17) + size(2LE) + dtadr(2LE) + exadr(2LE) + pad(8)
Returns up to 63 entries from the directory mapped to unit (1-7). Max 63 (2KB RX buffer).
INFO (0x02):
Request: [0x02] [filename:17]
Response: [0x00] [entry:32] (or [0xFF] if not found)
READ (0x03):
Request: [0x03] [unit:1] [filename:17] [offset:2LE] [length:2LE]
Response: [0x00] [actual_len:2LE] [data:actual_len] (or [0xFF] if not found)
WRITE (0x04):
Request: [0x04] [unit:1] [filename:17] [atrb:1] [size:2LE] [dtadr:2LE] [exadr:2LE] [data:size]
Response: [0x00] (or [0xFF] on error)
CLOSE (0x05):
Request: [0x05]
Response: [0x00] (server closes connection)
DELETE (0x06):
Request: [0x06] [filename:17]
Response: [0x00] (or [0xFF] if not found)
All commands include a unit byte (1-7) selecting the server directory.
Usage:
python3 netfs.py [--port 6800] [--dir ./mzf_files] [--dir2 path] ... [--dir7 path]
--dir maps to NET1: (or NET: with no digit), --dir2 maps to NET2:, etc.
Units without a --dirN argument fall back to --dir.
(c) 2026 Philip Smart <philip.smart@net2net.org>
"""
import argparse
import os
import socket
import struct
import threading
import sys
DEFAULT_PORT = 6800
DEFAULT_DIR = "."
MZF_HEADER_SIZE = 128
# Protocol constants
CMD_DIR = 0x01
CMD_INFO = 0x02
CMD_READ = 0x03
CMD_WRITE = 0x04
CMD_CLOSE = 0x05
CMD_DELETE = 0x06
STATUS_OK = 0x00
STATUS_ERR = 0xFF
STATUS_END = 0xFE
def parse_mzf_header(data):
"""Parse a 128-byte MZF header."""
if len(data) < MZF_HEADER_SIZE:
return None
atrb = data[0]
# Remap RB/BSD/BRD types to BTX (type 2) so BASIC can LOAD them
if atrb in (0x03, 0x04, 0x05):
atrb = 0x02
name_raw = data[1:18]
name = name_raw.rstrip(b'\x00\x0d').decode('ascii', errors='replace')
size = struct.unpack('<H', data[18:20])[0]
dtadr = struct.unpack('<H', data[20:22])[0]
exadr = struct.unpack('<H', data[22:24])[0]
return {
'atrb': atrb, 'name': name, 'name_raw': name_raw,
'size': size, 'dtadr': dtadr, 'exadr': exadr
}
def make_dir_entry(atrb, name_raw, size, dtadr, exadr):
"""Create a 32-byte binary directory entry (standard MZF format).
Z80 handler remaps fields as needed for the target BASIC."""
entry = bytearray(32)
entry[0] = atrb & 0xFF
entry[1:18] = name_raw[:17].ljust(17, b'\x00')
struct.pack_into('<H', entry, 18, size & 0xFFFF)
struct.pack_into('<H', entry, 20, dtadr & 0xFFFF)
struct.pack_into('<H', entry, 22, exadr & 0xFFFF)
return bytes(entry)
def scan_directory(dirpath):
"""Scan directory for MZF files."""
entries = []
for fname in sorted(os.listdir(dirpath)):
fpath = os.path.join(dirpath, fname)
if not os.path.isfile(fpath):
continue
ext = os.path.splitext(fname)[1].lower()
if ext in ('.mzf', '.mzt', '.m12'):
try:
with open(fpath, 'rb') as f:
hdr_data = f.read(MZF_HEADER_SIZE)
hdr = parse_mzf_header(hdr_data)
if hdr:
hdr['filepath'] = fpath
hdr['data_offset'] = MZF_HEADER_SIZE
entries.append(hdr)
except Exception as e:
print(f" Warning: {fname}: {e}")
elif ext in ('.bin', '.rom', '.dat'):
fsize = os.path.getsize(fpath)
name_clean = os.path.splitext(fname)[0][:17]
name_raw = name_clean.encode('ascii', errors='replace').ljust(17, b'\x00')
entries.append({
'atrb': 0x01, 'name': name_clean, 'name_raw': name_raw,
'size': fsize & 0xFFFF, 'dtadr': 0x1200, 'exadr': 0x1200,
'filepath': fpath, 'data_offset': 0
})
return entries
def find_file(entries, name_bytes):
"""Find a file by 17-byte name field (binary match)."""
# Strip trailing nulls/CRs for comparison
search = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip().upper()
for ent in entries:
if ent['name'].strip().upper() == search:
return ent
return None
def handle_client(conn, addr, dirs):
"""Handle a binary protocol client connection.
dirs: dict mapping unit number (1-7) to directory path."""
conn.settimeout(30) # 30s timeout — recovers if host crashes mid-transfer.
print(f"[{addr}] Connected")
try:
while True:
# Read 1-byte command
cmd_data = conn.recv(1)
if not cmd_data:
break
cmd = cmd_data[0]
if cmd == CMD_DIR:
# Read 1-byte unit number.
unit_data = conn.recv(1)
if not unit_data:
break
unit = unit_data[0]
dp = dirs.get(unit)
entries = scan_directory(dp) if dp else []
count = min(len(entries), 63)
resp = bytearray()
resp.append(STATUS_OK)
resp.append(count)
for i in range(count):
ent = entries[i]
resp.extend(make_dir_entry(ent['atrb'], ent['name_raw'], ent['size'], ent['dtadr'], ent['exadr']))
conn.sendall(bytes(resp))
print(f"[{addr}] DIR unit={unit} ({dp or 'not configured'}) → {count}/{len(entries)} entries ({len(resp)} bytes)")
elif cmd == CMD_INFO:
# Read 17-byte filename
name_bytes = conn.recv(17)
if len(name_bytes) < 17:
conn.sendall(bytes([STATUS_ERR]))
continue
# INFO searches unit 1 directory (no unit byte in protocol).
dp = dirs.get(1)
entries = scan_directory(dp) if dp else []
found = find_file(entries, name_bytes)
if found:
entry = make_dir_entry(found['atrb'], found['name_raw'], found['size'], found['dtadr'], found['exadr'])
conn.sendall(bytes([STATUS_OK]) + entry)
print(f"[{addr}] INFO '{found['name']}' → size={found['size']}")
else:
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
print(f"[{addr}] INFO '{name_str}' → NOT FOUND")
elif cmd == CMD_READ:
# [unit:1][name:17][offset:2][length:2] = 22 bytes.
params = conn.recv(22)
if len(params) < 22:
conn.sendall(bytes([STATUS_ERR]))
continue
unit = params[0]
name_bytes = params[1:18]
offset = struct.unpack('<H', params[18:20])[0]
length = struct.unpack('<H', params[20:22])[0]
dp = dirs.get(unit)
entries = scan_directory(dp) if dp else []
found = find_file(entries, name_bytes) if entries else None
if found:
with open(found['filepath'], 'rb') as f:
f.seek(found['data_offset'] + offset)
max_read = found['size'] - offset if offset < found['size'] else 0
data = f.read(min(length, max_read))
actual_len = len(data)
resp = bytearray()
resp.append(STATUS_OK)
resp.extend(struct.pack('<H', actual_len))
resp.extend(data)
conn.sendall(bytes(resp))
print(f"[{addr}] READ unit={unit} '{found['name']}' ofs={offset} len={length}{actual_len} bytes")
else:
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
print(f"[{addr}] READ unit={unit} '{name_str}' → NOT FOUND")
elif cmd == CMD_WRITE:
# [unit:1][name:17][atrb:1][size:2][dtadr:2][exadr:2] = 25 bytes.
params = conn.recv(25)
if len(params) < 25:
conn.sendall(bytes([STATUS_ERR]))
continue
unit = params[0]
name_bytes = params[1:18]
atrb = params[18]
size = struct.unpack('<H', params[19:21])[0]
dtadr = struct.unpack('<H', params[21:23])[0]
exadr = struct.unpack('<H', params[23:25])[0]
dp = dirs.get(unit)
if not dp:
# Drain the data bytes then return error.
remaining = size
while remaining > 0:
chunk = conn.recv(min(remaining, 4096))
if not chunk:
break
remaining -= len(chunk)
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip()
print(f"[{addr}] WRITE unit={unit} '{name_str}' → unit not configured")
continue
# Read file data
data = b''
remaining = size
while remaining > 0:
chunk = conn.recv(min(remaining, 4096))
if not chunk:
break
data += chunk
remaining -= len(chunk)
# Build MZF file
name_str = name_bytes.rstrip(b'\x00\x0d').decode('ascii', errors='replace').strip()
fname = name_str.replace(' ', '_') + '.mzf'
fpath = os.path.join(dp, fname)
hdr = bytearray(MZF_HEADER_SIZE)
hdr[0] = atrb
hdr[1:18] = name_bytes[:17]
struct.pack_into('<H', hdr, 18, size)
struct.pack_into('<H', hdr, 20, dtadr)
struct.pack_into('<H', hdr, 22, exadr)
with open(fpath, 'wb') as f:
f.write(bytes(hdr))
f.write(data)
conn.sendall(bytes([STATUS_OK]))
print(f"[{addr}] WRITE unit={unit} '{name_str}' atrb={atrb:02X} size={size}{fpath}")
elif cmd == CMD_DELETE:
# Read 17-byte filename (no unit byte — uses unit 1 dir).
name_bytes = conn.recv(17)
if len(name_bytes) < 17:
conn.sendall(bytes([STATUS_ERR]))
continue
dp = dirs.get(1)
entries = scan_directory(dp) if dp else []
found = find_file(entries, name_bytes)
if found:
try:
os.remove(found['filepath'])
conn.sendall(bytes([STATUS_OK]))
print(f"[{addr}] DELETE '{found['name']}'{found['filepath']}")
except Exception as e:
conn.sendall(bytes([STATUS_ERR]))
print(f"[{addr}] DELETE '{found['name']}' FAILED: {e}")
else:
conn.sendall(bytes([STATUS_ERR]))
name_str = name_bytes.rstrip(b'\x00').decode('ascii', errors='replace')
print(f"[{addr}] DELETE '{name_str}' → NOT FOUND")
elif cmd == CMD_CLOSE:
conn.sendall(bytes([STATUS_OK]))
print(f"[{addr}] CLOSE")
break
else:
conn.sendall(bytes([STATUS_ERR]))
print(f"[{addr}] Unknown cmd: 0x{cmd:02X}")
except (ConnectionResetError, BrokenPipeError):
pass
except socket.timeout:
print(f"[{addr}] Timeout (host crash recovery)")
except Exception as e:
print(f"[{addr}] Error: {e}")
finally:
print(f"[{addr}] Disconnected")
conn.close()
def main():
parser = argparse.ArgumentParser(description="NetFS — Binary File Server for Sharp MZ BASIC")
parser.add_argument('--port', '-p', type=int, default=DEFAULT_PORT, help=f"TCP port (default: {DEFAULT_PORT})")
parser.add_argument('--dir', '-d', type=str, default=DEFAULT_DIR, help="Directory for NET1:/NET: (default: .)")
for i in range(2, 8):
parser.add_argument(f'--dir{i}', type=str, default=None, help=f"Directory for NET{i}:")
args = parser.parse_args()
# Build unit → directory mapping.
dirs = {1: os.path.abspath(args.dir)}
for i in range(2, 8):
d = getattr(args, f'dir{i}', None)
if d:
dirs[i] = os.path.abspath(d)
# Validate all directories exist.
for unit, path in dirs.items():
if not os.path.isdir(path):
print(f"Error: directory '{path}' (NET{unit}:) does not exist")
sys.exit(1)
print(f"NetFS v4.0 — Sharp MZ BASIC Network File Server")
for unit in sorted(dirs):
entries = scan_directory(dirs[unit])
print(f" NET{unit}: {dirs[unit]} ({len(entries)} files)")
print(f"Listening on port {args.port}")
print()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', args.port))
sock.listen(5)
try:
while True:
conn, addr = sock.accept()
t = threading.Thread(target=handle_client, args=(conn, addr, dirs), daemon=True)
t.start()
except KeyboardInterrupt:
print("\nShutting down...")
finally:
sock.close()
if __name__ == '__main__':
main()

View File

@@ -1 +1 @@
2.518
2.566