Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6758c2892c |
2
projects/tzpuPico/esp32/filepack_version.txt
vendored
2
projects/tzpuPico/esp32/filepack_version.txt
vendored
@@ -1 +1 @@
|
||||
2.49
|
||||
2.58
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
2
projects/tzpuPico/esp32/version.txt
vendored
2
projects/tzpuPico/esp32/version.txt
vendored
@@ -1 +1 @@
|
||||
2.66
|
||||
2.74
|
||||
|
||||
@@ -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">
|
||||
|
||||
166
projects/tzpuPico/esp32/webserver/js/configgui.js
vendored
166
projects/tzpuPico/esp32/webserver/js/configgui.js
vendored
@@ -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);
|
||||
|
||||
119
projects/tzpuPico/esp32/webserver/js/filemanager.js
vendored
119
projects/tzpuPico/esp32/webserver/js/filemanager.js
vendored
@@ -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';
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.49
|
||||
2.58
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
6
projects/tzpuPico/src/include/Z80CPU.h
vendored
6
projects/tzpuPico/src/include/Z80CPU.h
vendored
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
7
projects/tzpuPico/src/include/ipc_protocol.h
vendored
7
projects/tzpuPico/src/include/ipc_protocol.h
vendored
@@ -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)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.534
|
||||
2.579
|
||||
|
||||
BIN
projects/tzpuPico/src/test/Celestite/celestite_stress.mzf
vendored
Normal file
BIN
projects/tzpuPico/src/test/Celestite/celestite_stress.mzf
vendored
Normal file
Binary file not shown.
67
projects/tzpuPico/src/test/Celestite/celestite_stress.sym
vendored
Normal file
67
projects/tzpuPico/src/test/Celestite/celestite_stress.sym
vendored
Normal 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
|
||||
BIN
projects/tzpuPico/src/test/Celestite/celestite_test.mzf
vendored
Normal file
BIN
projects/tzpuPico/src/test/Celestite/celestite_test.mzf
vendored
Normal file
Binary file not shown.
93
projects/tzpuPico/src/test/Celestite/celestite_test.sym
vendored
Normal file
93
projects/tzpuPico/src/test/Celestite/celestite_test.sym
vendored
Normal 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
|
||||
13
projects/tzpuPico/src/z80.pio
vendored
13
projects/tzpuPico/src/z80.pio
vendored
@@ -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
354
projects/tzpuPico/tools/NetFileServer/netfs.py
vendored
Executable 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()
|
||||
2
projects/tzpuPico/version.txt
vendored
2
projects/tzpuPico/version.txt
vendored
@@ -1 +1 @@
|
||||
2.518
|
||||
2.566
|
||||
|
||||
Reference in New Issue
Block a user