Updates to fix Z80 write cycles, MZ1500 persona and other addtions including updates to ESP32 web interface and fixing NCM reconnection should the host not connect on first try
This commit is contained in:
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