jsbeeb 1.12.0 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/package.json +8 -8
- package/public/roms/atom/ATMMC3E.rom +0 -0
- package/public/roms/atom/Atom_Basic.rom +0 -0
- package/public/roms/atom/Atom_DOS.rom +0 -0
- package/public/roms/atom/Atom_FloatingPoint.rom +0 -0
- package/public/roms/atom/Atom_Kernel.rom +0 -0
- package/public/roms/atom/Atom_Kernel_E.rom +0 -0
- package/public/roms/atom/PCHARME.ROM +0 -0
- package/public/roms/atom/gags.rom +0 -0
- package/public/roms/atom/werom.rom +0 -0
- package/src/6502.js +344 -44
- package/src/6847.js +724 -0
- package/src/6847_fontdata.js +124 -0
- package/src/app/app.js +2 -15
- package/src/app/args.js +26 -0
- package/src/disc.js +2 -20
- package/src/fake6502.js +3 -2
- package/src/jsbeeb.css +23 -0
- package/src/keyboard.js +45 -23
- package/src/machine-session.js +85 -59
- package/src/main.js +142 -41
- package/src/mmc.js +1053 -0
- package/src/models.js +42 -1
- package/src/ppia.js +477 -0
- package/src/soundchip.js +99 -1
- package/src/tapes.js +73 -16
- package/src/url-params.js +7 -2
- package/src/utils.js +74 -1
- package/src/utils_atom.js +508 -0
- package/src/video.js +12 -1
- package/src/web/audio-handler.js +8 -3
- package/tests/test-machine.js +133 -8
package/src/mmc.js
ADDED
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
import { loadData, unzip, createZipBlob } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
highly adapted from:
|
|
5
|
+
https://github.com/hoglet67/Atomulator
|
|
6
|
+
in the src/atommc folder
|
|
7
|
+
|
|
8
|
+
Simulate the AtoMMC2 device, which is a MMC/SD card reader for the Acorn Atom.
|
|
9
|
+
|
|
10
|
+
First create a dictionary of the MMC data from a zipped file, into this.allfiles.
|
|
11
|
+
|
|
12
|
+
The Acorn Atom MMC2 eprom will communicate with the MMC via 0xb400-0xb40c, which is the Atom side of the MMC2 device.
|
|
13
|
+
The write() is called when 0xb400-0xb40c is written, read() is called when 0xb400-0xb40c is read.
|
|
14
|
+
|
|
15
|
+
the low byte determines which register is being accessed.
|
|
16
|
+
The registers are:
|
|
17
|
+
CMD_REG: 0xb400
|
|
18
|
+
LATCH_REG: 0xb401
|
|
19
|
+
READ_DATA_REG: 0xb402
|
|
20
|
+
WRITE_DATA_REG: 0xb403
|
|
21
|
+
STATUS_REG: 0xb404
|
|
22
|
+
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Access an MMC file (zipped)
|
|
26
|
+
const CMD_REG = 0x0;
|
|
27
|
+
const LATCH_REG = 0x1;
|
|
28
|
+
const READ_DATA_REG = 0x2;
|
|
29
|
+
const WRITE_DATA_REG = 0x3;
|
|
30
|
+
const STATUS_REG = 0x4;
|
|
31
|
+
|
|
32
|
+
const CMD_DIR_OPEN = 0x0;
|
|
33
|
+
const CMD_DIR_READ = 0x1;
|
|
34
|
+
const CMD_DIR_CWD = 0x2;
|
|
35
|
+
// const CMD_DIR_GETCWD = 0x3;
|
|
36
|
+
const CMD_DIR_MKDIR = 0x4;
|
|
37
|
+
const CMD_DIR_RMDIR = 0x5;
|
|
38
|
+
|
|
39
|
+
const CMD_RENAME = 0x8;
|
|
40
|
+
|
|
41
|
+
const CMD_FILE_CLOSE = 0x10;
|
|
42
|
+
const CMD_FILE_OPEN_READ = 0x11;
|
|
43
|
+
// const CMD_FILE_OPEN_IMG = 0x12;
|
|
44
|
+
const CMD_FILE_OPEN_WRITE = 0x13;
|
|
45
|
+
const CMD_FILE_DELETE = 0x14;
|
|
46
|
+
const CMD_FILE_GETINFO = 0x15;
|
|
47
|
+
const CMD_FILE_SEEK = 0x16;
|
|
48
|
+
const CMD_FILE_OPEN_RAF = 0x17;
|
|
49
|
+
|
|
50
|
+
const CMD_INIT_READ = 0x20;
|
|
51
|
+
const CMD_INIT_WRITE = 0x21;
|
|
52
|
+
const CMD_READ_BYTES = 0x22;
|
|
53
|
+
const CMD_WRITE_BYTES = 0x23;
|
|
54
|
+
|
|
55
|
+
// EXEC_PACKET_REG "commands"
|
|
56
|
+
const CMD_EXEC_PACKET = 0x3f;
|
|
57
|
+
|
|
58
|
+
// UTIL_CMD_REG commands
|
|
59
|
+
const CMD_GET_CARD_TYPE = 0x80;
|
|
60
|
+
const CMD_GET_PORT_DDR = 0xa0;
|
|
61
|
+
const CMD_SET_PORT_DDR = 0xa1;
|
|
62
|
+
const CMD_READ_PORT = 0xa2;
|
|
63
|
+
const CMD_WRITE_PORT = 0xa3;
|
|
64
|
+
const CMD_GET_FW_VER = 0xe0;
|
|
65
|
+
const CMD_GET_BL_VER = 0xe1;
|
|
66
|
+
const CMD_GET_CFG_BYTE = 0xf0;
|
|
67
|
+
const CMD_SET_CFG_BYTE = 0xf1;
|
|
68
|
+
const CMD_READ_AUX = 0xfd;
|
|
69
|
+
const CMD_GET_HEARTBEAT = 0xfe;
|
|
70
|
+
|
|
71
|
+
// Status codes
|
|
72
|
+
const STATUS_OK = 0x3f;
|
|
73
|
+
const STATUS_COMPLETE = 0x40;
|
|
74
|
+
const STATUS_EOF = 0x60;
|
|
75
|
+
const STATUS_BUSY = 0x80;
|
|
76
|
+
|
|
77
|
+
// STATUS_REG bit masks
|
|
78
|
+
const MMC_MCU_BUSY = 0x01;
|
|
79
|
+
const MMC_MCU_READ = 0x02;
|
|
80
|
+
const MMC_MCU_WROTE = 0x04;
|
|
81
|
+
|
|
82
|
+
const VSN_MAJ = 2;
|
|
83
|
+
const VSN_MIN = 10;
|
|
84
|
+
const FA_OPEN_EXISTING = 0;
|
|
85
|
+
const FA_READ = 1;
|
|
86
|
+
const FA_WRITE = 2;
|
|
87
|
+
const FA_CREATE_NEW = 4;
|
|
88
|
+
|
|
89
|
+
// Simulate constants
|
|
90
|
+
const FR_OK = 0,
|
|
91
|
+
FR_EXIST = 8,
|
|
92
|
+
FR_NO_FILE = 4,
|
|
93
|
+
FR_NO_PATH = 5,
|
|
94
|
+
FR_INVALID_NAME = 6;
|
|
95
|
+
const FA_CREATE_ALWAYS = 8,
|
|
96
|
+
FA_OPEN_ALWAYS = 16,
|
|
97
|
+
ERROR_TOO_MANY_OPEN = 0x12,
|
|
98
|
+
FILENUM_OFFSET = 0x20;
|
|
99
|
+
// Simulate open modes
|
|
100
|
+
const O_CREAT = 0x100,
|
|
101
|
+
O_RDWR = 0x2,
|
|
102
|
+
O_RDONLY = 0x0,
|
|
103
|
+
O_BINARY = 0x8000;
|
|
104
|
+
|
|
105
|
+
// Reference: https://github.com/hoglet67/AtoMMC2Firmware (atmmmc2def.h)
|
|
106
|
+
|
|
107
|
+
/** WFN functions
|
|
108
|
+
*
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Represents a file with a path and data.
|
|
112
|
+
*/
|
|
113
|
+
export class WFNFile {
|
|
114
|
+
/**
|
|
115
|
+
* @param {string} path - The file path. prepended with § if deleted
|
|
116
|
+
* @param {Uint8Array} data - The file data.
|
|
117
|
+
*/
|
|
118
|
+
constructor(path, data) {
|
|
119
|
+
this.path = path;
|
|
120
|
+
this.data = data instanceof Uint8Array ? data : new Uint8Array();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Sanitize a WFNFile path for use as a ZIP entry name.
|
|
125
|
+
function sanitizeZipEntryName(path) {
|
|
126
|
+
const name = path.startsWith("/") ? path.slice(1) : path;
|
|
127
|
+
if (!name || name.includes("\\") || name.split("/").some((seg) => seg === "..")) return null;
|
|
128
|
+
return name;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Create a ZIP blob from an array of WFNFile entries (stored, no compression).
|
|
132
|
+
export async function toMMCZipAsync(data) {
|
|
133
|
+
const files = [];
|
|
134
|
+
for (const entry of data) {
|
|
135
|
+
if (entry.path.startsWith("§")) continue; // skip unlinked files
|
|
136
|
+
const name = sanitizeZipEntryName(entry.path);
|
|
137
|
+
if (!name) continue;
|
|
138
|
+
files.push({ name, data: entry.data });
|
|
139
|
+
}
|
|
140
|
+
return createZipBlob(files);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Extract all files from a ZIP archive into WFNFile entries.
|
|
144
|
+
export async function extractSDFiles(data) {
|
|
145
|
+
const files = await unzip(new Uint8Array(data));
|
|
146
|
+
const result = [];
|
|
147
|
+
for (const [name, fileData] of Object.entries(files)) {
|
|
148
|
+
if (!/^[a-z0-9./_ -]+$/i.test(name)) continue;
|
|
149
|
+
if (name.endsWith("/")) continue; // skip directory entries
|
|
150
|
+
if (name.split("/").some((seg) => seg === "..")) continue;
|
|
151
|
+
result.push(new WFNFile("/" + name, fileData));
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function LoadSD(file) {
|
|
157
|
+
const data = await loadData(file);
|
|
158
|
+
return extractSDFiles(data);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
class WFN {
|
|
162
|
+
/**
|
|
163
|
+
* Create a new WFN instance.
|
|
164
|
+
* @param {Object} cpu - The CPU instance.
|
|
165
|
+
*/
|
|
166
|
+
constructor(mmc, cpu) {
|
|
167
|
+
this.mmc = mmc;
|
|
168
|
+
this.cpu = cpu;
|
|
169
|
+
|
|
170
|
+
this.globalData = new Uint8Array(256);
|
|
171
|
+
this.globalIndex = 0;
|
|
172
|
+
this.globalDataPresent = 0;
|
|
173
|
+
this.globalAmount = 0;
|
|
174
|
+
|
|
175
|
+
this.fildata = new Array(4).fill(null);
|
|
176
|
+
this.fildataIndex = 0;
|
|
177
|
+
|
|
178
|
+
this.openeddir = ""; // Directory requested when reading directories
|
|
179
|
+
this.foldersSeen = []; // Folders seen when reading directories
|
|
180
|
+
this.dfn = 0; // Directory file number, used to track the current file in the zip file when reading directories
|
|
181
|
+
|
|
182
|
+
this.CWD = "";
|
|
183
|
+
|
|
184
|
+
this.filenum = -1;
|
|
185
|
+
this.seekpos = 0;
|
|
186
|
+
this.WildPattern = ".*"; // Default wildcard pattern (for regex not ATOM)
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @type {WFNFile[]}
|
|
190
|
+
*/
|
|
191
|
+
this.allfiles = [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
WFN_WorkerTest() {}
|
|
195
|
+
|
|
196
|
+
WFN_FileOpenRead() {
|
|
197
|
+
const res = this.fileOpen(FA_OPEN_EXISTING | FA_READ);
|
|
198
|
+
// if (filenum < 4) {
|
|
199
|
+
// FILINFO *filinfo = &filinfodata[filenum];
|
|
200
|
+
// get_fileinfo_special(filinfo);
|
|
201
|
+
// }
|
|
202
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | res);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
WFN_FileOpenWrite() {
|
|
206
|
+
const res = this.fileOpen(FA_CREATE_NEW | FA_WRITE);
|
|
207
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | res);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
WFN_FileClose() {
|
|
211
|
+
this.seekpos = 0;
|
|
212
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Convert a glob pattern (with * and ?) to an anchored regex string.
|
|
216
|
+
static wildcardToRegex(pattern) {
|
|
217
|
+
// Escape regex metacharacters except * and ?, then convert glob wildcards.
|
|
218
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
219
|
+
return "^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
GetWildcard() {
|
|
223
|
+
// Extract the path from this.globalData (Uint8Array) until the first null byte
|
|
224
|
+
const path = this.trimToFilename(this.globalData).replace(/\\/g, "/");
|
|
225
|
+
const hasWildcard = path.includes("*") || path.includes("?");
|
|
226
|
+
|
|
227
|
+
if (hasWildcard) {
|
|
228
|
+
const lastSlashIndex = path.lastIndexOf("/");
|
|
229
|
+
if (lastSlashIndex !== -1) {
|
|
230
|
+
this.globalData = new TextEncoder().encode(path.slice(0, lastSlashIndex + 1) + "\0");
|
|
231
|
+
this.WildPattern = WFN.wildcardToRegex(path.slice(lastSlashIndex + 1));
|
|
232
|
+
} else {
|
|
233
|
+
this.WildPattern = WFN.wildcardToRegex(path);
|
|
234
|
+
this.globalData = new TextEncoder().encode("\0");
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
this.WildPattern = ".*";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Strip trailing slash from a path string
|
|
242
|
+
stripTrailingSlash(path) {
|
|
243
|
+
if (path.endsWith("/")) {
|
|
244
|
+
return path.slice(0, -1);
|
|
245
|
+
}
|
|
246
|
+
return path;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/*
|
|
250
|
+
MMCData file contains only files with paths, and no directories.
|
|
251
|
+
To simulate directories, a basepath with multiple entries is a directory
|
|
252
|
+
For example, if the MMCData file contains:
|
|
253
|
+
/dir1/file1.txt
|
|
254
|
+
/dir1/file2.txt
|
|
255
|
+
/dir2/file3.txt
|
|
256
|
+
then /dir1/ is a directory, and /dir2/ is a directory.
|
|
257
|
+
The findfirst and findnext functions will simulate directory traversal by checking the paths in allfiles.
|
|
258
|
+
The findfirst function will set this.dfn to the index of the first file in the directory, and this.openeddir to the directory path.
|
|
259
|
+
The findnext function will iterate through the files in the directory, checking if the file path starts with this.openeddir.
|
|
260
|
+
If a file path starts with this.openeddir, it is considered to be in the directory.
|
|
261
|
+
If a file path does not start with this.openeddir, it is considered to be outside the directory.
|
|
262
|
+
If a file path has a subdirectory (i.e., it contains a slash after the directory path), it is considered to be in a subdirectory and is returned
|
|
263
|
+
once and subsequent calls to findnext will skip it.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
*/
|
|
268
|
+
|
|
269
|
+
// Simulate findfirst: check if a directory exists in allfiles
|
|
270
|
+
findfirst(path) {
|
|
271
|
+
const dirPrefix = this.stripTrailingSlash(path) + "/"; // Ensure path ends with a slash
|
|
272
|
+
|
|
273
|
+
// Simulate directory open: check if any file starts with path + "/"
|
|
274
|
+
const f_index = this.allfiles.findIndex((file) => file.path.startsWith(dirPrefix));
|
|
275
|
+
|
|
276
|
+
// if index is found, set dfn to the index of the first file in the directory
|
|
277
|
+
if (f_index !== -1) {
|
|
278
|
+
this.dfn = f_index + 1; // Set dfn to the next entry after the first one in this directory
|
|
279
|
+
this.openeddir = dirPrefix; // record the opened directory
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
this.foldersSeen = [];
|
|
283
|
+
|
|
284
|
+
return f_index !== -1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// this.dfn is the index of the next file in the zip file
|
|
288
|
+
|
|
289
|
+
// Simulate findnext: iterate through directory entries in allfiles[].path
|
|
290
|
+
findnext() {
|
|
291
|
+
let foundDirEntry = "";
|
|
292
|
+
do {
|
|
293
|
+
// entry = readdir()
|
|
294
|
+
|
|
295
|
+
if (this.dfn >= this.allfiles.length) break;
|
|
296
|
+
|
|
297
|
+
// Check if the next file starts with the opened directory
|
|
298
|
+
const nextFullName = this.allfiles[this.dfn].path;
|
|
299
|
+
//
|
|
300
|
+
|
|
301
|
+
if (!nextFullName.startsWith(this.openeddir)) {
|
|
302
|
+
this.dfn += 1;
|
|
303
|
+
continue; // Skip to the next entry if it doesn't match the opened directory
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let relativeName = nextFullName.slice(this.openeddir.length);
|
|
307
|
+
|
|
308
|
+
// Skip empty or root entries
|
|
309
|
+
if (relativeName === "" && relativeName !== "/") {
|
|
310
|
+
this.dfn += 1;
|
|
311
|
+
continue; // Skip to the next entry if it doesn't match the opened directory
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
//
|
|
315
|
+
let folders = relativeName.split("/");
|
|
316
|
+
|
|
317
|
+
relativeName = folders[0];
|
|
318
|
+
|
|
319
|
+
// skip files in subdirectories if the subdirecty has been seen before
|
|
320
|
+
if (folders.length > 1) {
|
|
321
|
+
if (this.foldersSeen.includes(folders[0])) {
|
|
322
|
+
// If this is seen subfolder, skip this entry
|
|
323
|
+
this.dfn += 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
this.foldersSeen.push(folders[0]);
|
|
327
|
+
relativeName = folders[0] + "/"; // relative name is the folder + slash
|
|
328
|
+
}
|
|
329
|
+
foundDirEntry = relativeName;
|
|
330
|
+
} while (foundDirEntry == "");
|
|
331
|
+
|
|
332
|
+
if (foundDirEntry) {
|
|
333
|
+
// Optionally, check for valid 8.3 filename here if needed
|
|
334
|
+
// For now, just return the entry
|
|
335
|
+
this.dfn += 1;
|
|
336
|
+
return foundDirEntry;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// No more entries
|
|
340
|
+
return "";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Simulate f_opendir: open a directory
|
|
344
|
+
f_opendir(path) {
|
|
345
|
+
// Build absolute path and check validity
|
|
346
|
+
const xpath = this.trimToFilename(path);
|
|
347
|
+
const absResult = this.buildAbsolutePath(xpath, false);
|
|
348
|
+
if (absResult.error) {
|
|
349
|
+
return absResult.error;
|
|
350
|
+
}
|
|
351
|
+
const newpath = absResult.path;
|
|
352
|
+
|
|
353
|
+
if (this.findfirst(newpath)) {
|
|
354
|
+
return FR_OK;
|
|
355
|
+
} else {
|
|
356
|
+
return FR_NO_PATH;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
WFN_DirectoryOpen() {
|
|
361
|
+
// Separate wildcard and path
|
|
362
|
+
this.GetWildcard();
|
|
363
|
+
|
|
364
|
+
let res = this.f_opendir(this.globalData);
|
|
365
|
+
this.dfn = 0;
|
|
366
|
+
if (res !== 0) {
|
|
367
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | res);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this.mmc.WriteDataPort(STATUS_OK);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
f_readdir() {
|
|
374
|
+
let fno = { fname: "", fsize: 0, fattrib: 0 };
|
|
375
|
+
// If a file found copy it's details, else set size to 0 and filename to ''
|
|
376
|
+
const nextentry = this.findnext();
|
|
377
|
+
fno.fname = nextentry;
|
|
378
|
+
|
|
379
|
+
return { error: FR_OK, fno: fno };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
WFN_DirectoryRead() {
|
|
383
|
+
while (true) {
|
|
384
|
+
let result = this.f_readdir();
|
|
385
|
+
let res = result.error;
|
|
386
|
+
|
|
387
|
+
if (res !== FR_OK || result.fno.fname === "") {
|
|
388
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | res);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const fname = result.fno.fname;
|
|
393
|
+
|
|
394
|
+
// Check to see if filename matches current wildcard
|
|
395
|
+
// const Match = wildcmp(WildPattern, longname);
|
|
396
|
+
const Match = fname.match(new RegExp(this.WildPattern));
|
|
397
|
+
|
|
398
|
+
if (Match) {
|
|
399
|
+
// if is a directory, str will be <fname>
|
|
400
|
+
|
|
401
|
+
const isdir = fname.endsWith("/");
|
|
402
|
+
let str = fname;
|
|
403
|
+
if (isdir) {
|
|
404
|
+
str = str.slice(0, -1); // Remove trailing slash for directory names
|
|
405
|
+
str = `<${str}>`; // Enclose directory names in <>
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Convert the string to a Uint8Array
|
|
409
|
+
this.globalData = new TextEncoder().encode(str + "\0");
|
|
410
|
+
|
|
411
|
+
this.mmc.WriteDataPort(STATUS_OK);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
dir_exists(path) {
|
|
418
|
+
// ensure path ends in slash
|
|
419
|
+
if (!path.endsWith("/")) path += "/";
|
|
420
|
+
|
|
421
|
+
// Check if the path exists in the names array
|
|
422
|
+
if (this.allfiles.some((file) => file.path === path || file.path.startsWith(path))) {
|
|
423
|
+
// Check if the path is a directory (ends with '/')
|
|
424
|
+
return FR_OK;
|
|
425
|
+
}
|
|
426
|
+
return FR_NO_PATH;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
realpath(path) {
|
|
430
|
+
// In C, realpath resolves all symbolic links, relative paths, and returns the absolute path.
|
|
431
|
+
// In JS, we just normalize the path (remove redundant slashes, resolve '.' and '..').
|
|
432
|
+
// This is a simplified version and does not handle symlinks.
|
|
433
|
+
const parts = [];
|
|
434
|
+
for (const part of path.split("/")) {
|
|
435
|
+
if (part === "" || part === ".") continue;
|
|
436
|
+
if (part === "..") {
|
|
437
|
+
if (parts.length > 0) parts.pop();
|
|
438
|
+
} else {
|
|
439
|
+
parts.push(part);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
path = "/" + parts.join("/");
|
|
443
|
+
|
|
444
|
+
while (path.length > 0 && path.endsWith("/")) {
|
|
445
|
+
path = path.slice(0, -1);
|
|
446
|
+
}
|
|
447
|
+
return path;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
f_chdir(path) {
|
|
451
|
+
// ensure newpath is an absolute path (relative paths appended to CWD,
|
|
452
|
+
// absolute paths are used as is)
|
|
453
|
+
const newpath = this.buildAbsolutePath(path, false);
|
|
454
|
+
if (newpath.error) {
|
|
455
|
+
return newpath.error; // Return error if path is invalid
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Resolve the newpath
|
|
459
|
+
const fullpath = this.realpath(newpath.path);
|
|
460
|
+
if (fullpath !== undefined) {
|
|
461
|
+
// Path exists and is a directory
|
|
462
|
+
if (this.dir_exists(fullpath) == FR_OK) {
|
|
463
|
+
this.CWD = fullpath; // Update the base path
|
|
464
|
+
return FR_OK; // Success
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return FR_NO_PATH;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
f_unlink(path) {
|
|
471
|
+
// ensure newpath is an absolute path (relative paths appended to CWD,
|
|
472
|
+
// absolute paths are used as is)
|
|
473
|
+
const newpath = this.buildAbsolutePath(path, true);
|
|
474
|
+
if (newpath.error) {
|
|
475
|
+
return newpath.error; // Return error if path is invalid
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// remove all in a folder, but this doesn't seem to be
|
|
479
|
+
// used for *DELETE as files are deleted one by one
|
|
480
|
+
if (this.dir_exists(newpath.path) == FR_OK) {
|
|
481
|
+
// delete all the files within the folder
|
|
482
|
+
const dirPrefix = newpath.path.endsWith("/") ? newpath.path : newpath.path + "/";
|
|
483
|
+
this.allfiles
|
|
484
|
+
.filter((file) => file.path.startsWith(dirPrefix))
|
|
485
|
+
.forEach((file) => (file.path = "§" + file.path));
|
|
486
|
+
|
|
487
|
+
return FR_OK;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (this.file_exists(newpath.path) == FR_OK) {
|
|
491
|
+
// remove the file from the list
|
|
492
|
+
this.allfiles.filter((file) => file.path == newpath.path).forEach((file) => (file.path = "§" + file.path));
|
|
493
|
+
|
|
494
|
+
return FR_OK;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return FR_NO_PATH;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
trimToFilename(globaldata) {
|
|
501
|
+
let path = String.fromCharCode(...globaldata.slice(0, -1)).split("\0")[0];
|
|
502
|
+
// when deleting folders, the pathname is echoed
|
|
503
|
+
// back by the ATOM which means they have <...> around the
|
|
504
|
+
// name
|
|
505
|
+
if (path.startsWith("<") && path.endsWith(">")) path = path.slice(1, -1);
|
|
506
|
+
return path;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
WFN_SetCWDirectory() {
|
|
510
|
+
const dirname = this.trimToFilename(this.globalData);
|
|
511
|
+
let ret = this.f_chdir(dirname);
|
|
512
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | ret);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
WFN_FileSeek() {
|
|
516
|
+
this.seekpos =
|
|
517
|
+
this.globalData[0] | (this.globalData[1] << 8) | (this.globalData[2] << 16) | (this.globalData[3] << 24);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
WFN_FileRead() {
|
|
521
|
+
if (this.globalAmount === 0) {
|
|
522
|
+
this.globalAmount = 256;
|
|
523
|
+
}
|
|
524
|
+
const read = Math.min(this.fildata[0].data.length, this.globalAmount);
|
|
525
|
+
const fildataEnd = this.fildataIndex + read;
|
|
526
|
+
const data = this.fildata[0].data.slice(this.fildataIndex, fildataEnd);
|
|
527
|
+
let ret = 0;
|
|
528
|
+
this.globalData = data;
|
|
529
|
+
this.fildataIndex = fildataEnd;
|
|
530
|
+
if (this.filenum > 0 && ret === 0 && this.globalAmount !== read) {
|
|
531
|
+
this.mmc.WriteDataPort(STATUS_EOF);
|
|
532
|
+
} else {
|
|
533
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | ret);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
WFN_FileWrite() {
|
|
538
|
+
if (this.globalAmount === 0) {
|
|
539
|
+
this.globalAmount = 256;
|
|
540
|
+
}
|
|
541
|
+
const wrote = this.globalAmount;
|
|
542
|
+
const fildataEnd = this.fildataIndex + wrote;
|
|
543
|
+
|
|
544
|
+
// append this.globalData to this.fildata[0] at this.fildataIndex
|
|
545
|
+
let oldData = this.fildata[0].data;
|
|
546
|
+
let newLength = Math.max(oldData.length, fildataEnd);
|
|
547
|
+
let newData = new Uint8Array(newLength);
|
|
548
|
+
newData.set(oldData, 0);
|
|
549
|
+
newData.set(this.globalData.slice(0, wrote), this.fildataIndex);
|
|
550
|
+
this.fildata[0].data = newData;
|
|
551
|
+
|
|
552
|
+
this.fildataIndex = fildataEnd;
|
|
553
|
+
|
|
554
|
+
// const res = this.f_write(fil, (void*)globalData, globalAmount, &written);
|
|
555
|
+
|
|
556
|
+
let res = 0; // always works !
|
|
557
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | res);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
WFN_ExecuteArbitrary() {}
|
|
561
|
+
|
|
562
|
+
WFN_FileOpenRAF() {}
|
|
563
|
+
WFN_FileDelete() {
|
|
564
|
+
const pathname = this.trimToFilename(this.globalData);
|
|
565
|
+
const ret = this.f_unlink(pathname);
|
|
566
|
+
this.mmc.WriteDataPort(STATUS_COMPLETE | ret);
|
|
567
|
+
}
|
|
568
|
+
WFN_FileGetInfo() {
|
|
569
|
+
this.mmc.WriteDataPort(STATUS_EOF);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
WFN_DirectoryCreate() {
|
|
573
|
+
// Not implemented — return EOF to signal failure to the Atom
|
|
574
|
+
this.mmc.WriteDataPort(STATUS_EOF);
|
|
575
|
+
}
|
|
576
|
+
WFN_DirectoryDelete() {
|
|
577
|
+
this.mmc.WriteDataPort(STATUS_EOF);
|
|
578
|
+
}
|
|
579
|
+
WFN_Rename() {
|
|
580
|
+
this.mmc.WriteDataPort(STATUS_EOF);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
reset() {
|
|
584
|
+
this.CWD = "";
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
clearData() {
|
|
588
|
+
this.globalData = new Uint8Array(256);
|
|
589
|
+
this.globalIndex = 0;
|
|
590
|
+
this.globalDataPresent = 0;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
addData(data) {
|
|
594
|
+
this.globalData[this.globalIndex] = data;
|
|
595
|
+
++this.globalIndex;
|
|
596
|
+
this.globalDataPresent = 1;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
getData(restart = false) {
|
|
600
|
+
if (restart) {
|
|
601
|
+
this.globalIndex = 0;
|
|
602
|
+
this.globalDataPresent = 0;
|
|
603
|
+
}
|
|
604
|
+
let val = 0;
|
|
605
|
+
if (this.globalIndex < this.globalData.length) val = this.globalData[this.globalIndex] | 0;
|
|
606
|
+
this.globalIndex++;
|
|
607
|
+
return val;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
setTransferLength(length) {
|
|
611
|
+
this.globalAmount = length;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
fileOpen(mode) {
|
|
615
|
+
if (!this.allfiles) return 4; // no file
|
|
616
|
+
let ret = 0;
|
|
617
|
+
let fname = this.trimToFilename(this.globalData);
|
|
618
|
+
|
|
619
|
+
if (this.filenum === 0) {
|
|
620
|
+
this.fildataIndex = 0; // FIXME : should be one for each fildata if going down this line!
|
|
621
|
+
// The scratch file is fixed, so we are backwards compatible with 2.9 firmware
|
|
622
|
+
let fopen = this.f_open(fname, mode);
|
|
623
|
+
if (fopen.error == FR_OK) this.fildata[0] = this.allfiles[fopen.fp];
|
|
624
|
+
ret = fopen.error;
|
|
625
|
+
} else {
|
|
626
|
+
this.filenum = 0;
|
|
627
|
+
if (this.fildata[1] == null) {
|
|
628
|
+
this.filenum = 1;
|
|
629
|
+
} else if (this.fildata[2] == null) {
|
|
630
|
+
this.filenum = 2;
|
|
631
|
+
} else if (this.fildata[3] == null) {
|
|
632
|
+
this.filenum = 3;
|
|
633
|
+
}
|
|
634
|
+
if (this.filenum > 0) {
|
|
635
|
+
let fopen = this.f_open(fname, mode);
|
|
636
|
+
if (fopen.error == FR_OK) {
|
|
637
|
+
this.fildata[this.filenum] = this.allfiles[fopen.fp];
|
|
638
|
+
// No error, so update the return value to indicate the file num
|
|
639
|
+
ret = FILENUM_OFFSET | this.filenum;
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
// All files are open, return too many open files
|
|
643
|
+
ret = ERROR_TOO_MANY_OPEN;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return STATUS_COMPLETE | ret;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Convert a path to an absolute, normalized path and optionally validate 8.3 filename
|
|
650
|
+
buildAbsolutePath(xpath, validateName = true) {
|
|
651
|
+
// Normalize path separators
|
|
652
|
+
let path = String(xpath).replace(/\\/g, "/");
|
|
653
|
+
|
|
654
|
+
// Optionally validate 8.3 filename rules
|
|
655
|
+
if (validateName) {
|
|
656
|
+
// Find the last element of the path
|
|
657
|
+
let nameIdx = path.lastIndexOf("/");
|
|
658
|
+
let name = nameIdx === -1 ? path : path.slice(nameIdx + 1);
|
|
659
|
+
|
|
660
|
+
// Find the suffix
|
|
661
|
+
let suffixIdx = name.indexOf(".");
|
|
662
|
+
let namePart = suffixIdx !== -1 ? name.slice(0, suffixIdx) : name;
|
|
663
|
+
let suffixPart = suffixIdx !== -1 ? name.slice(suffixIdx + 1) : "";
|
|
664
|
+
|
|
665
|
+
// Validate the name part
|
|
666
|
+
if (namePart.length < 1 || namePart.length > 8) {
|
|
667
|
+
// Name not between 1 and 8 characters
|
|
668
|
+
return { error: FR_INVALID_NAME };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Validate the optional suffix part
|
|
672
|
+
if (suffixIdx !== -1) {
|
|
673
|
+
// Reject multiple suffixes
|
|
674
|
+
if (suffixPart.includes(".")) {
|
|
675
|
+
return { error: FR_INVALID_NAME };
|
|
676
|
+
}
|
|
677
|
+
if (suffixPart.length === 0) {
|
|
678
|
+
// Remove a dangling suffix
|
|
679
|
+
path = path.slice(0, path.length - 1);
|
|
680
|
+
} else if (suffixPart.length > 3) {
|
|
681
|
+
// Suffix too long
|
|
682
|
+
return { error: FR_INVALID_NAME };
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Make the path absolute
|
|
688
|
+
let absPath;
|
|
689
|
+
if (path.startsWith("/")) {
|
|
690
|
+
// absolute: append the path to the root directory path
|
|
691
|
+
absPath = path;
|
|
692
|
+
} else {
|
|
693
|
+
// relative: append the path to current directory path
|
|
694
|
+
absPath = this.CWD + "/" + path; // CWD should be MMCPath
|
|
695
|
+
}
|
|
696
|
+
return { path: absPath };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Check if a file exists in allfiles by name (returns FR_OK if exists, FR_NO_PATH if not)
|
|
700
|
+
file_exists(name) {
|
|
701
|
+
// if (!this.allfiles || !Array.isArray(this.allfiles)) return FR_NO_PATH;
|
|
702
|
+
// Normalize name to absolute path
|
|
703
|
+
const absResult = this.buildAbsolutePath(name, false);
|
|
704
|
+
if (absResult.error) return FR_NO_PATH;
|
|
705
|
+
const absName = absResult.path;
|
|
706
|
+
// Search for a WFNFile with matching path (not a directory)
|
|
707
|
+
const file = this.allfiles.find((f) => f.path === absName && !f.path.startsWith("§"));
|
|
708
|
+
return file ? FR_OK : FR_NO_PATH;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
open(path, mode) {
|
|
712
|
+
let fileIndex = -1;
|
|
713
|
+
if (this.allfiles) {
|
|
714
|
+
fileIndex = this.allfiles.findIndex((f) => f.path === path);
|
|
715
|
+
|
|
716
|
+
if (fileIndex === -1 && mode & O_CREAT) {
|
|
717
|
+
// Create new file
|
|
718
|
+
this.allfiles.push(new WFNFile(path, new Uint8Array()));
|
|
719
|
+
fileIndex = this.allfiles.length - 1;
|
|
720
|
+
} else if (fileIndex !== -1) {
|
|
721
|
+
//cannot find file and not creating it.
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return fileIndex;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Simulate f_open in JavaScript
|
|
729
|
+
// Returns FR_OK (0) on success, or error code
|
|
730
|
+
// fp: file object (JS object), path: string, mode: integer flags
|
|
731
|
+
f_open(path, mode) {
|
|
732
|
+
// Build absolute path and check validity
|
|
733
|
+
const absResult = this.buildAbsolutePath(path, true);
|
|
734
|
+
if (absResult.error) {
|
|
735
|
+
return { error: FR_INVALID_NAME };
|
|
736
|
+
}
|
|
737
|
+
const open_path = absResult.path;
|
|
738
|
+
|
|
739
|
+
// Check if file exists
|
|
740
|
+
let exists = this.file_exists(open_path);
|
|
741
|
+
|
|
742
|
+
// Mask mode flags
|
|
743
|
+
mode &= FA_READ | FA_WRITE | FA_CREATE_ALWAYS | FA_OPEN_ALWAYS | FA_CREATE_NEW;
|
|
744
|
+
|
|
745
|
+
let open_mode = 0;
|
|
746
|
+
if (exists === FR_OK) {
|
|
747
|
+
if (mode & FA_CREATE_NEW) return { error: FR_EXIST };
|
|
748
|
+
if (mode & FA_CREATE_ALWAYS) open_mode = O_CREAT;
|
|
749
|
+
if (mode & (FA_READ | FA_WRITE)) {
|
|
750
|
+
if (mode & FA_WRITE) open_mode |= O_RDWR;
|
|
751
|
+
else open_mode |= O_RDONLY;
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
if (mode & (FA_OPEN_ALWAYS | FA_CREATE_NEW | FA_CREATE_ALWAYS)) open_mode = O_CREAT | O_RDWR;
|
|
755
|
+
else return { error: FR_NO_FILE };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Simulate file open/create
|
|
759
|
+
let fileIndex = this.open(open_path, open_mode | O_BINARY);
|
|
760
|
+
|
|
761
|
+
if (fileIndex >= 0) {
|
|
762
|
+
return { fp: fileIndex, error: FR_OK };
|
|
763
|
+
} else {
|
|
764
|
+
return { error: FR_INVALID_NAME };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* AtomMMC2 emulates the AtoMMC2 device for the Acorn Atom.
|
|
771
|
+
* Handles SD/MMC file operations and communication with the Atom.
|
|
772
|
+
*/
|
|
773
|
+
export class AtomMMC2 {
|
|
774
|
+
/**
|
|
775
|
+
* Attach a gamepad object for joystick support.
|
|
776
|
+
* @param {Object} gamepad
|
|
777
|
+
*/
|
|
778
|
+
attachGamepad(gamepad) {
|
|
779
|
+
this.gamepad = gamepad;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Reset the MMC state.
|
|
784
|
+
* @param {boolean} hard - If true, perform a hard reset.
|
|
785
|
+
*/
|
|
786
|
+
reset(hard) {
|
|
787
|
+
if (hard) {
|
|
788
|
+
this.configByte = 0xff;
|
|
789
|
+
}
|
|
790
|
+
this.WFN.reset();
|
|
791
|
+
this.seekpos = 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Set the MMC data (unzipped files).
|
|
796
|
+
* @param {Object} data
|
|
797
|
+
*/
|
|
798
|
+
SetMMCData(data) {
|
|
799
|
+
this.WFN.allfiles = data;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Get the MMC data.
|
|
804
|
+
* @returns {Object}
|
|
805
|
+
*/
|
|
806
|
+
GetMMCData() {
|
|
807
|
+
return this.WFN.allfiles;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* clear
|
|
812
|
+
* @returns
|
|
813
|
+
*/
|
|
814
|
+
ClearMMCData() {
|
|
815
|
+
const fname = "README".padEnd(16, "\0");
|
|
816
|
+
const loadaddr = 0x2900;
|
|
817
|
+
const basicstart = 0xb2c2;
|
|
818
|
+
const flen = 0x003e;
|
|
819
|
+
const basicfile = "\r\0\n REM created by jsatom\r\0\x14 REM \x19commandercoder.com\r\0\x1E END\r";
|
|
820
|
+
const fend = 0xc3;
|
|
821
|
+
|
|
822
|
+
// Build the byte array for the README file
|
|
823
|
+
// Format: [fname (16 bytes), loadaddr (2 bytes LE), basicstart (2 bytes LE), flen (2 bytes LE), basicfile (flen bytes), fend (1 byte)]
|
|
824
|
+
const readmeBytes = new Uint8Array(16 + 2 + 2 + 2 + basicfile.length + 1);
|
|
825
|
+
let offset = 0;
|
|
826
|
+
// fname (16 bytes)
|
|
827
|
+
for (let i = 0; i < 16; i++) {
|
|
828
|
+
readmeBytes[offset++] = fname.charCodeAt(i);
|
|
829
|
+
}
|
|
830
|
+
// loadaddr (2 bytes, little endian)
|
|
831
|
+
readmeBytes[offset++] = loadaddr & 0xff;
|
|
832
|
+
readmeBytes[offset++] = (loadaddr >> 8) & 0xff;
|
|
833
|
+
// basicstart (2 bytes, little endian)
|
|
834
|
+
readmeBytes[offset++] = basicstart & 0xff;
|
|
835
|
+
readmeBytes[offset++] = (basicstart >> 8) & 0xff;
|
|
836
|
+
// flen (2 bytes, little endian)
|
|
837
|
+
readmeBytes[offset++] = flen & 0xff;
|
|
838
|
+
readmeBytes[offset++] = (flen >> 8) & 0xff;
|
|
839
|
+
// basicfile (flen bytes)
|
|
840
|
+
for (let i = 0; i < basicfile.length; i++) {
|
|
841
|
+
readmeBytes[offset++] = basicfile.charCodeAt(i);
|
|
842
|
+
}
|
|
843
|
+
// fend (1 byte)
|
|
844
|
+
readmeBytes[offset] = fend;
|
|
845
|
+
|
|
846
|
+
this.WFN.allfiles = [new WFNFile("/README", readmeBytes)];
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* @param {Object} cpu - The CPU instance.
|
|
851
|
+
*/
|
|
852
|
+
constructor(cpu) {
|
|
853
|
+
this.cpu = cpu;
|
|
854
|
+
this.gamepad = null;
|
|
855
|
+
this.MMCtoAtom = STATUS_BUSY;
|
|
856
|
+
this.heartbeat = 0x55;
|
|
857
|
+
this.MCUStatus = MMC_MCU_BUSY;
|
|
858
|
+
this.configByte = 0;
|
|
859
|
+
this.lastaddr = CMD_REG; // address latch for reads
|
|
860
|
+
this.byteValueLatch = 0;
|
|
861
|
+
this.worker = null;
|
|
862
|
+
this.seekpos = 0;
|
|
863
|
+
this.WildPattern = ".*";
|
|
864
|
+
this.foldersSeen = [];
|
|
865
|
+
this.dfn = 0;
|
|
866
|
+
this.TRISB = 0;
|
|
867
|
+
this.PORTB = 0;
|
|
868
|
+
this.LATB = 0;
|
|
869
|
+
|
|
870
|
+
this.WFN = new WFN(this, cpu);
|
|
871
|
+
this.reset(true);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// MMCtoAtom and MCUStatus are used to transfer data
|
|
875
|
+
// from the MMC to the Atom and vice versa.
|
|
876
|
+
|
|
877
|
+
// Set the WROTE bit and write data to the Atom
|
|
878
|
+
WriteDataPort(b) {
|
|
879
|
+
this.MMCtoAtom = b;
|
|
880
|
+
this.MCUStatus &= ~MMC_MCU_BUSY;
|
|
881
|
+
this.MCUStatus |= MMC_MCU_WROTE;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Set the WROTE bit and return the data from the Atom
|
|
885
|
+
ReadDataPort() {
|
|
886
|
+
this.MCUStatus &= ~MMC_MCU_READ;
|
|
887
|
+
this.MCUStatus |= MMC_MCU_WROTE;
|
|
888
|
+
|
|
889
|
+
return this.MMCtoAtom;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// CPU is writing to 0xb400-0xb40c
|
|
893
|
+
write(addr, val) {
|
|
894
|
+
this.lastaddr = addr;
|
|
895
|
+
this.at_process(addr, val, true);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// CPU is reading from 0xb400-0xb40c
|
|
899
|
+
read(addr) {
|
|
900
|
+
const Current = this.MMCtoAtom;
|
|
901
|
+
const val = Current & 0xff;
|
|
902
|
+
const reg = addr & 0x0f;
|
|
903
|
+
const stat = this.MCUStatus;
|
|
904
|
+
this.MCUStatus &= ~MMC_MCU_READ;
|
|
905
|
+
addr = this.lastaddr;
|
|
906
|
+
if (reg === STATUS_REG) {
|
|
907
|
+
return stat;
|
|
908
|
+
}
|
|
909
|
+
this.at_process(addr, val, false);
|
|
910
|
+
return Current;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Process Atom MMC register access.
|
|
914
|
+
* @param {number} addr
|
|
915
|
+
* @param {number} val
|
|
916
|
+
* @param {boolean} write
|
|
917
|
+
*/
|
|
918
|
+
at_process(addr, val, write) {
|
|
919
|
+
const LatchedAddress = addr & 0x0f;
|
|
920
|
+
this.latchedAddress = LatchedAddress;
|
|
921
|
+
const ADDRESS_MASK = 0x07;
|
|
922
|
+
this.worker = null;
|
|
923
|
+
this.MCUStatus |= MMC_MCU_READ;
|
|
924
|
+
if (write === false) {
|
|
925
|
+
this.MCUStatus &= ~MMC_MCU_READ;
|
|
926
|
+
switch (LatchedAddress) {
|
|
927
|
+
case READ_DATA_REG: {
|
|
928
|
+
let data = this.WFN.getData();
|
|
929
|
+
this.WriteDataPort(data);
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
switch (LatchedAddress & ADDRESS_MASK) {
|
|
935
|
+
case CMD_REG: {
|
|
936
|
+
let received = val & 0xff;
|
|
937
|
+
if ((received & 0x98) === 0x10) {
|
|
938
|
+
this.WFN.filenum = (received >> 5) & 3;
|
|
939
|
+
received &= 0x9f;
|
|
940
|
+
}
|
|
941
|
+
if ((received & 0xf0) === 0x20) {
|
|
942
|
+
this.WFN.filenum = (received >> 2) & 3;
|
|
943
|
+
received &= 0xf3;
|
|
944
|
+
}
|
|
945
|
+
this.WriteDataPort(STATUS_BUSY);
|
|
946
|
+
this.MCUStatus |= MMC_MCU_BUSY;
|
|
947
|
+
if (received === CMD_DIR_OPEN) {
|
|
948
|
+
this.worker = () => this.WFN.WFN_DirectoryOpen();
|
|
949
|
+
} else if (received === CMD_DIR_READ) {
|
|
950
|
+
this.worker = () => this.WFN.WFN_DirectoryRead();
|
|
951
|
+
} else if (received === CMD_DIR_CWD) {
|
|
952
|
+
this.worker = () => this.WFN.WFN_SetCWDirectory();
|
|
953
|
+
} else if (received == CMD_DIR_MKDIR) {
|
|
954
|
+
// create directory
|
|
955
|
+
this.worker = () => this.WFN.WFN_DirectoryCreate();
|
|
956
|
+
} else if (received == CMD_DIR_RMDIR) {
|
|
957
|
+
// delete directory
|
|
958
|
+
this.worker = () => this.WFN.WFN_DirectoryDelete();
|
|
959
|
+
} else if (received == CMD_RENAME) {
|
|
960
|
+
// rename
|
|
961
|
+
this.worker = () => this.WFN.WFN_Rename();
|
|
962
|
+
} else if (received === CMD_FILE_CLOSE) {
|
|
963
|
+
this.worker = () => this.WFN.WFN_FileClose();
|
|
964
|
+
} else if (received === CMD_FILE_OPEN_READ) {
|
|
965
|
+
this.worker = () => this.WFN.WFN_FileOpenRead();
|
|
966
|
+
} else if (received === CMD_FILE_OPEN_WRITE) {
|
|
967
|
+
this.worker = () => this.WFN.WFN_FileOpenWrite();
|
|
968
|
+
} else if (received === CMD_FILE_OPEN_RAF) {
|
|
969
|
+
this.worker = () => this.WFN.WFN_FileOpenRAF();
|
|
970
|
+
} else if (received === CMD_FILE_DELETE) {
|
|
971
|
+
this.worker = () => this.WFN.WFN_FileDelete();
|
|
972
|
+
} else if (received === CMD_FILE_GETINFO) {
|
|
973
|
+
this.worker = () => this.WFN.WFN_FileGetInfo();
|
|
974
|
+
} else if (received === CMD_FILE_SEEK) {
|
|
975
|
+
this.worker = () => this.WFN.WFN_FileSeek();
|
|
976
|
+
} else if (received === CMD_INIT_READ) {
|
|
977
|
+
let data = this.WFN.getData(true);
|
|
978
|
+
this.WriteDataPort(data);
|
|
979
|
+
this.lastaddr = READ_DATA_REG;
|
|
980
|
+
} else if (received === CMD_INIT_WRITE) {
|
|
981
|
+
this.WFN.clearData();
|
|
982
|
+
} else if (received === CMD_READ_BYTES) {
|
|
983
|
+
this.WFN.setTransferLength(this.byteValueLatch);
|
|
984
|
+
this.worker = () => this.WFN.WFN_FileRead();
|
|
985
|
+
} else if (received === CMD_WRITE_BYTES) {
|
|
986
|
+
this.WFN.setTransferLength(this.byteValueLatch);
|
|
987
|
+
this.worker = () => this.WFN.WFN_FileWrite();
|
|
988
|
+
} else if (received === CMD_EXEC_PACKET) {
|
|
989
|
+
this.worker = () => this.WFN.WFN_ExecuteArbitrary();
|
|
990
|
+
} else if (received === CMD_GET_FW_VER) {
|
|
991
|
+
this.WriteDataPort((VSN_MAJ << 4) | VSN_MIN);
|
|
992
|
+
} else if (received === CMD_GET_BL_VER) {
|
|
993
|
+
this.WriteDataPort(1);
|
|
994
|
+
} else if (received === CMD_GET_CFG_BYTE) {
|
|
995
|
+
this.WriteDataPort(this.configByte);
|
|
996
|
+
} else if (received === CMD_SET_CFG_BYTE) {
|
|
997
|
+
this.configByte = this.byteValueLatch;
|
|
998
|
+
this.WriteDataPort(STATUS_OK);
|
|
999
|
+
} else if (received === CMD_READ_AUX) {
|
|
1000
|
+
this.WriteDataPort(this.latchedAddress);
|
|
1001
|
+
} else if (received === CMD_GET_HEARTBEAT) {
|
|
1002
|
+
this.WriteDataPort(this.heartbeat);
|
|
1003
|
+
this.heartbeat ^= 0xff;
|
|
1004
|
+
} else if (received === CMD_GET_CARD_TYPE) {
|
|
1005
|
+
this.WriteDataPort(0x01);
|
|
1006
|
+
} else if (received === CMD_GET_PORT_DDR) {
|
|
1007
|
+
this.WriteDataPort(this.TRISB);
|
|
1008
|
+
} else if (received === CMD_SET_PORT_DDR) {
|
|
1009
|
+
this.TRISB = this.byteValueLatch;
|
|
1010
|
+
this.WriteDataPort(STATUS_OK);
|
|
1011
|
+
} else if (received === CMD_READ_PORT) {
|
|
1012
|
+
let JOYSTICK = 0xff;
|
|
1013
|
+
const joyst = true;
|
|
1014
|
+
if (joyst && this.gamepad && this.gamepad.gamepadButtons !== undefined) {
|
|
1015
|
+
if (this.gamepad.gamepadButtons[15]) JOYSTICK ^= 1;
|
|
1016
|
+
if (this.gamepad.gamepadButtons[14]) JOYSTICK ^= 2;
|
|
1017
|
+
if (this.gamepad.gamepadButtons[13]) JOYSTICK ^= 4;
|
|
1018
|
+
if (this.gamepad.gamepadButtons[12]) JOYSTICK ^= 8;
|
|
1019
|
+
if (this.gamepad.gamepadButtons[0]) JOYSTICK ^= 0x10;
|
|
1020
|
+
this.WriteDataPort(JOYSTICK);
|
|
1021
|
+
} else {
|
|
1022
|
+
this.WriteDataPort(this.PORTB);
|
|
1023
|
+
}
|
|
1024
|
+
} else if (received === CMD_WRITE_PORT) {
|
|
1025
|
+
this.LATB = this.byteValueLatch;
|
|
1026
|
+
this.WriteDataPort(STATUS_OK);
|
|
1027
|
+
} else {
|
|
1028
|
+
console.warn(`MMC: unrecognised CMD: 0x${received.toString(16)}`);
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case WRITE_DATA_REG: {
|
|
1033
|
+
const received = val & 0xff;
|
|
1034
|
+
this.WFN.addData(received);
|
|
1035
|
+
break;
|
|
1036
|
+
}
|
|
1037
|
+
case LATCH_REG: {
|
|
1038
|
+
const received = val & 0xff;
|
|
1039
|
+
this.byteValueLatch = received;
|
|
1040
|
+
this.WriteDataPort(this.byteValueLatch);
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case STATUS_REG: {
|
|
1044
|
+
// does nothing
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (this.worker) {
|
|
1049
|
+
this.worker();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|