robchang commited on
Commit
40020e7
·
1 Parent(s): d140af7

Deploy KLH10 TOPS-20 emulator

Browse files
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ .git/
2
+ .claude/
3
+ Panda-doc/
4
+ node_modules/
5
+ *.tap
6
+ build/wasm/bld-*/RH20.*
7
+ build/wasm/bld-*/*-dbd9
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.sav filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Hugging Face Spaces Dockerfile for KLH10 TOPS-20 WebAssembly Emulator
2
+ #
3
+ # Deploy to HF Spaces using deploy-huggingface.sh, or manually:
4
+ # 1. Create a Docker Space on huggingface.co
5
+ # 2. Copy this file as "Dockerfile" into the Space repo
6
+ # 3. Push to HF
7
+
8
+ FROM node:22-slim
9
+
10
+ RUN apt-get update && \
11
+ apt-get install -y --no-install-recommends curl bzip2 ca-certificates && \
12
+ rm -rf /var/lib/apt/lists/*
13
+
14
+ RUN useradd -m -u 1000 user
15
+ WORKDIR /app
16
+ COPY . .
17
+ RUN ./download-images.sh && chown -R user:user /app
18
+
19
+ USER user
20
+ ENV PORT=7860
21
+ EXPOSE 7860
22
+ CMD ["node", "build/wasm/bld-kl/serve.js"]
README.md CHANGED
@@ -1,12 +1,24 @@
1
  ---
2
- title: Tops20
3
- emoji: 🌖
4
- colorFrom: pink
5
- colorTo: yellow
6
  sdk: docker
7
- pinned: false
8
- license: other
9
- short_description: Standalone in-browser PDP 10 Emulator running TOPS-20
 
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TOPS-20 PDP-10 Emulator
3
+ emoji: "\U0001F5A5"
4
+ colorFrom: blue
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
+ custom_headers:
9
+ cross-origin-embedder-policy: require-corp
10
+ cross-origin-opener-policy: same-origin
11
+ cross-origin-resource-policy: cross-origin
12
  ---
13
 
14
+ # TOPS-20 PDP-10 Emulator
15
+
16
+ A PDP-10 mainframe emulator running DEC TOPS-20 V7.0 in your browser via WebAssembly.
17
+
18
+ Click **Boot TOPS-20** and wait ~30 seconds for the boot sequence to complete.
19
+
20
+ WebAssembly port via agentic coding by Rob Chang.
21
+ KLH10 emulator by Kenneth L. Harrenstien.
22
+ Panda TOPS-20 distribution by Mark Crispin.
23
+
24
+ Source: [github.com/robchang/tops20](https://github.com/robchang/tops20)
build/wasm/bld-kl/boot.sav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2e0c3d1ea76b81fa1c5aaa8b3922acacc7e87f168befa35f4867939389f764f
3
+ size 23280
build/wasm/bld-kl/emulator-worker.js ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Web Worker for KLH10 PDP-10 Emulator
2
+ console.log('Worker script loaded successfully');
3
+
4
+ // Ring Buffer Manager for shared memory communication
5
+ class RingBufferManager {
6
+ constructor(sharedBuffer, baseOffset = 0) {
7
+ this.buffer = new Uint8Array(sharedBuffer);
8
+ this.view = new DataView(sharedBuffer);
9
+
10
+ // Buffer layout:
11
+ // Input ring buffer: offset 0, size 4112 (4096 data + 16 metadata)
12
+ // Output ring buffer: offset 4112, size 4112 (4096 data + 16 metadata)
13
+ // Control: offset 8224, size 16 (mode + flush_request + padding)
14
+
15
+ this.baseOffset = baseOffset;
16
+ this.inputOffset = baseOffset;
17
+ this.outputOffset = baseOffset + 4112;
18
+ this.controlOffset = baseOffset + 8224;
19
+ }
20
+
21
+ // Input ring buffer (JavaScript writes, WASM reads)
22
+ writeInputChar(char) {
23
+ const writePos = this.view.getUint32(this.inputOffset, true);
24
+ const readPos = this.view.getUint32(this.inputOffset + 4, true);
25
+ const size = 4096;
26
+
27
+ const nextWritePos = (writePos + 1) % size;
28
+ if (nextWritePos === readPos) {
29
+ return false; // Buffer full
30
+ }
31
+
32
+ this.buffer[this.inputOffset + 16 + writePos] = char.charCodeAt(0);
33
+ this.view.setUint32(this.inputOffset, nextWritePos, true);
34
+ return true;
35
+ }
36
+
37
+ writeInputLine(line) {
38
+ // Write each character followed by newline
39
+ for (let i = 0; i < line.length; i++) {
40
+ if (!this.writeInputChar(line[i])) {
41
+ return false; // Buffer full
42
+ }
43
+ }
44
+ // Add newline to complete the line
45
+ return this.writeInputChar('\n');
46
+ }
47
+
48
+ // Output ring buffer (WASM writes, JavaScript reads)
49
+ readOutputChar() {
50
+ const writePos = this.view.getUint32(this.outputOffset, true);
51
+ const readPos = this.view.getUint32(this.outputOffset + 4, true);
52
+
53
+ if (readPos === writePos) {
54
+ return null; // Buffer empty
55
+ }
56
+
57
+ const char = this.buffer[this.outputOffset + 16 + readPos];
58
+ const nextReadPos = (readPos + 1) % 4096;
59
+ this.view.setUint32(this.outputOffset + 4, nextReadPos, true);
60
+
61
+ return String.fromCharCode(char);
62
+ }
63
+
64
+ hasOutputData() {
65
+ const writePos = this.view.getUint32(this.outputOffset, true);
66
+ const readPos = this.view.getUint32(this.outputOffset + 4, true);
67
+ return readPos !== writePos;
68
+ }
69
+
70
+ // Output ring buffer (WASM writes, JavaScript reads) - Worker side writing
71
+ writeOutputChar(char) {
72
+ const writePos = this.view.getUint32(this.outputOffset, true);
73
+ const readPos = this.view.getUint32(this.outputOffset + 4, true);
74
+ const size = 4096;
75
+
76
+ const nextWritePos = (writePos + 1) % size;
77
+ if (nextWritePos === readPos) {
78
+ return false; // Buffer full
79
+ }
80
+
81
+ this.buffer[this.outputOffset + 16 + writePos] = char.charCodeAt(0);
82
+ this.view.setUint32(this.outputOffset, nextWritePos, true);
83
+ return true;
84
+ }
85
+
86
+ // Control functions
87
+ getCurrentMode() {
88
+ return this.view.getUint32(this.controlOffset, true);
89
+ }
90
+
91
+ getFlushRequest() {
92
+ return this.view.getUint32(this.controlOffset + 4, true);
93
+ }
94
+
95
+ clearFlushRequest() {
96
+ this.view.setUint32(this.controlOffset + 4, 0, true);
97
+ }
98
+ }
99
+
100
+ class EmulatorWorker {
101
+ constructor() {
102
+ this.Module = null;
103
+ this.isRunning = false;
104
+
105
+ // Shared WebAssembly memory - set from main thread
106
+ this.wasmMemory = null;
107
+ this.ringBufferBase = null;
108
+ }
109
+
110
+ async initialize() {
111
+ return new Promise((resolve, reject) => {
112
+ console.log('Worker: Starting initialization...');
113
+ try {
114
+ // Configure Module to use shared WebAssembly memory
115
+ console.log('Worker: Configuring Module with shared memory:', this.wasmMemory);
116
+ console.log('Worker: Shared memory buffer size:', this.wasmMemory.buffer.byteLength);
117
+
118
+ self.Module = {
119
+ // Use the shared memory passed from main thread
120
+ wasmMemory: this.wasmMemory,
121
+ // Capture stdout/stderr and write directly to shared ring buffer
122
+ print: (text) => {
123
+ // Clean up any invalid UTF-8 characters
124
+ const cleanText = text.replace(/[\u00A9]/g, '(C)').replace(/\uFFFD/g, '');
125
+
126
+ // Check if this is a prompt (KLH10# or KLH10> or KLH10>>)
127
+ const isPrompt = /^KLH10[#>]+\s*$/.test(cleanText.trim());
128
+
129
+ if (isPrompt) {
130
+ // Prompts should not have newlines added
131
+ this.writeToWasmOutputRing(cleanText);
132
+ } else {
133
+ // Regular output needs newlines added
134
+ this.writeToWasmOutputRing(cleanText + '\n');
135
+ }
136
+ },
137
+ printErr: (text) => {
138
+ // Clean up any invalid UTF-8 characters
139
+ const cleanText = text.replace(/[\u00A9]/g, '(C)').replace(/\uFFFD/g, '');
140
+ // Add newline since Module.print() is line-based and Emscripten strips \n
141
+ this.writeToWasmOutputRing(cleanText + '\n');
142
+ },
143
+
144
+ // Handle module ready
145
+ onRuntimeInitialized: async () => {
146
+ this.Module = self.Module;
147
+
148
+ // Set up ring buffers in WASM memory IMMEDIATELY before anything else
149
+ this.setupWasmRingBuffers();
150
+
151
+ // Create empty config file in virtual file system
152
+ // KLH10 processes each line as a command, so we want an empty file
153
+ const configContent = ``;
154
+
155
+ try {
156
+ // Create config file with same name as program (KLH10 default behavior)
157
+ this.Module.FS.writeFile('/kn10-kl', configContent);
158
+
159
+ // Preload all available .tap files
160
+ const potentialTapes = [
161
+ ];
162
+
163
+ for (const tapeFile of potentialTapes) {
164
+ try {
165
+ console.log('Worker: Attempting to preload tape:', tapeFile);
166
+
167
+ // Fetch the tape file
168
+ const response = await fetch('/' + tapeFile);
169
+ if (response.ok) {
170
+ const arrayBuffer = await response.arrayBuffer();
171
+ const uint8Array = new Uint8Array(arrayBuffer);
172
+
173
+ // Write to the virtual filesystem
174
+ this.Module.FS.writeFile(tapeFile, uint8Array);
175
+ console.log('Worker: Tape loaded successfully:', tapeFile, uint8Array.length, 'bytes');
176
+ } else {
177
+ console.log('Worker: Tape file not found (skipping):', tapeFile);
178
+ }
179
+ } catch (e) {
180
+ console.log('Worker: Tape file not available (skipping):', tapeFile);
181
+ }
182
+ }
183
+
184
+ // Preload bootstrap files if they exist (KL10 variants)
185
+ const bootFiles = ['boot.sav', 'mtboot.sav'];
186
+ for (const bootFile of bootFiles) {
187
+ try {
188
+ const response = await fetch('/' + bootFile);
189
+ if (response.ok) {
190
+ const arrayBuffer = await response.arrayBuffer();
191
+ const uint8Array = new Uint8Array(arrayBuffer);
192
+ this.Module.FS.writeFile(bootFile, uint8Array);
193
+ console.log('Bootstrap file loaded:', bootFile, uint8Array.length, 'bytes');
194
+ }
195
+ } catch (e) {
196
+ // Ignore if bootstrap files don't exist
197
+ }
198
+ }
199
+
200
+ // Preload Panda disk image
201
+ try {
202
+ console.log('Worker: Loading Panda disk image: RH20.RP07.1');
203
+ const response = await fetch('/RH20.RP07.1');
204
+ if (response.ok) {
205
+ const arrayBuffer = await response.arrayBuffer();
206
+ const uint8Array = new Uint8Array(arrayBuffer);
207
+ this.Module.FS.writeFile('RH20.RP07.1', uint8Array);
208
+ console.log('Worker: Panda disk image loaded:', uint8Array.length, 'bytes');
209
+ } else {
210
+ console.error('Worker: Could not load Panda disk image RH20.RP07.1');
211
+ }
212
+ } catch (e) {
213
+ console.error('Worker: Error loading Panda disk image:', e);
214
+ }
215
+
216
+ // List all files in the virtual filesystem for debugging
217
+ try {
218
+ var files = this.Module.FS.readdir('/');
219
+ console.log('Files in virtual filesystem:', files);
220
+ } catch (e) {
221
+ console.warn('Could not list files:', e);
222
+ }
223
+ } catch (err) {
224
+ console.warn('Could not create config file:', err);
225
+ }
226
+
227
+ this.sendMessage('ready');
228
+ resolve();
229
+ },
230
+
231
+ // Handle exit
232
+ onExit: (code) => {
233
+ this.isRunning = false;
234
+ this.sendMessage('exit', code);
235
+ },
236
+
237
+ // Don't quit on main exit
238
+ quit: (status, toThrow) => {
239
+ this.isRunning = false;
240
+ this.sendMessage('exit', status);
241
+ },
242
+
243
+ // Disable automatic main execution
244
+ noInitialRun: true,
245
+
246
+ // Capture errors
247
+ onAbort: (reason) => {
248
+ this.sendMessage('error', `Emulator aborted: ${reason}`);
249
+ reject(new Error(`Emulator aborted: ${reason}`));
250
+ }
251
+ };
252
+
253
+ // Import the Emscripten-generated JavaScript (use new filename to bypass cache)
254
+ console.log('Worker: About to import kn10-kl.js...');
255
+ importScripts('kn10-kl.js');
256
+ console.log('Worker: Successfully imported kn10-kl.js');
257
+
258
+ } catch (error) {
259
+ console.error('Failed to load Emscripten script:', error);
260
+ console.error('Error stack:', error.stack);
261
+ this.sendMessage('error', `Failed to load emulator: ${error.message} | Stack: ${error.stack}`);
262
+ reject(error);
263
+ }
264
+ });
265
+ }
266
+
267
+ startEmulator() {
268
+ try {
269
+ if (!this.Module || !this.Module.callMain) {
270
+ throw new Error('Module not properly initialized');
271
+ }
272
+
273
+ this.isRunning = true;
274
+
275
+
276
+ // Run the main function with config file argument
277
+ // argv[0] = program name, argv[1] = config file
278
+ setTimeout(() => {
279
+ try {
280
+ // Run without config file argument - will use default "kn10-kl" file
281
+ this.Module.callMain(['kn10-kl']);
282
+ } catch (e) {
283
+ console.error('callMain error:', e);
284
+ this.sendMessage('error', `Main execution failed: ${e.message}`);
285
+ }
286
+ }, 100);
287
+
288
+ } catch (error) {
289
+ this.sendMessage('error', `Failed to start emulator: ${error.message}`);
290
+ }
291
+ }
292
+
293
+ setupWasmRingBuffers() {
294
+ if (!this.wasmMemory || !this.ringBufferBase) {
295
+ console.error('❌ No shared WASM memory or ring buffer base provided');
296
+ return;
297
+ }
298
+
299
+ console.log('Worker: Setting up ring buffers in shared WASM memory');
300
+ console.log('Worker: WASM memory buffer:', this.wasmMemory.buffer.byteLength, 'bytes');
301
+ console.log('Worker: Ring buffer base offset:', this.ringBufferBase.toString(16));
302
+
303
+ if (this.Module && this.Module._klh10_set_ring_buffer_offset) {
304
+ // Tell WASM where the ring buffers are located in its own memory
305
+ this.Module._klh10_set_ring_buffer_offset(this.ringBufferBase);
306
+
307
+ // Create output ring buffer manager for JavaScript to write to
308
+ // RingBufferManager calculates output offset internally from base
309
+ this.outputRingBuffer = new RingBufferManager(this.wasmMemory.buffer, this.ringBufferBase);
310
+
311
+ console.log('Worker: Ring buffers initialized at WASM memory offset:', this.ringBufferBase.toString(16));
312
+ } else {
313
+ console.error('Worker: klh10_set_ring_buffer_offset function not found');
314
+ }
315
+ }
316
+
317
+ writeToWasmOutputRing(text) {
318
+ if (!this.outputRingBuffer) {
319
+ console.warn('Worker: Cannot write to output ring - not initialized');
320
+ return;
321
+ }
322
+
323
+ // Write each character directly to WASM memory output ring buffer
324
+ for (let i = 0; i < text.length; i++) {
325
+ const success = this.outputRingBuffer.writeOutputChar(text[i]);
326
+ if (!success) {
327
+ console.warn('Worker: Output ring buffer full, character dropped');
328
+ break;
329
+ }
330
+ }
331
+ }
332
+
333
+
334
+
335
+ sendMessage(type, data = null) {
336
+ self.postMessage({ type, data });
337
+ }
338
+ }
339
+
340
+ // Create worker instance
341
+ console.log('Creating EmulatorWorker instance...');
342
+ const worker = new EmulatorWorker();
343
+ console.log('EmulatorWorker instance created');
344
+
345
+ // Handle messages from main thread
346
+ self.onmessage = async (event) => {
347
+ const { type, wasmMemory, ringBufferBase } = event.data;
348
+
349
+ switch (type) {
350
+ case 'start':
351
+ console.log('Worker: Starting emulator with shared WASM memory...');
352
+ if (wasmMemory) {
353
+ worker.wasmMemory = wasmMemory;
354
+ worker.ringBufferBase = ringBufferBase;
355
+ console.log('Worker: Received shared WASM memory:', wasmMemory.buffer.byteLength, 'bytes');
356
+ console.log('Worker: Ring buffer base:', ringBufferBase.toString(16));
357
+ } else {
358
+ console.error('Worker: No shared WASM memory provided!');
359
+ }
360
+ try {
361
+ await worker.initialize();
362
+ // After successful initialization, start the emulator
363
+ worker.startEmulator();
364
+ } catch (error) {
365
+ worker.sendMessage('error', `Initialization failed: ${error.message}`);
366
+ }
367
+ break;
368
+
369
+ case 'mode_change':
370
+ worker.sendMessage('mode_change', data);
371
+ break;
372
+
373
+ default:
374
+ worker.sendMessage('error', `Unknown message type: ${type}`);
375
+ }
376
+ };
build/wasm/bld-kl/index.html ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>KLH10 PDP-10 Emulator - WebAssembly</title>
7
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ padding: 20px;
12
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
13
+ background-color: #1e1e1e;
14
+ color: #ffffff;
15
+ }
16
+ .container {
17
+ max-width: 1200px;
18
+ margin: 0 auto;
19
+ }
20
+ h1 {
21
+ text-align: center;
22
+ color: #4fc3f7;
23
+ margin-bottom: 30px;
24
+ }
25
+ .terminal-container {
26
+ background-color: #000;
27
+ border: 2px solid #333;
28
+ border-radius: 8px;
29
+ padding: 10px;
30
+ margin-bottom: 20px;
31
+ }
32
+ .controls {
33
+ text-align: center;
34
+ margin-bottom: 20px;
35
+ }
36
+ button {
37
+ background-color: #4fc3f7;
38
+ color: #000;
39
+ border: none;
40
+ padding: 10px 15px;
41
+ border-radius: 4px;
42
+ cursor: pointer;
43
+ font-size: 14px;
44
+ margin: 5px;
45
+ min-width: 120px;
46
+ }
47
+ button:hover {
48
+ background-color: #29b6f6;
49
+ }
50
+ button:disabled {
51
+ background-color: #666;
52
+ cursor: not-allowed;
53
+ }
54
+ .boot-btn {
55
+ font-size: 18px;
56
+ padding: 14px 32px;
57
+ min-width: 200px;
58
+ background-color: #4fc3f7;
59
+ }
60
+ .boot-btn:hover {
61
+ background-color: #29b6f6;
62
+ }
63
+ .mode-toggle {
64
+ margin-top: 8px;
65
+ font-size: 12px;
66
+ }
67
+ .mode-toggle a {
68
+ color: #888;
69
+ text-decoration: none;
70
+ }
71
+ .mode-toggle a:hover {
72
+ color: #4fc3f7;
73
+ text-decoration: underline;
74
+ }
75
+ .status {
76
+ text-align: center;
77
+ margin: 10px 0;
78
+ font-size: 14px;
79
+ }
80
+ .status.loading {
81
+ color: #ffb74d;
82
+ }
83
+ .status.ready {
84
+ color: #81c784;
85
+ }
86
+ .status.error {
87
+ color: #e57373;
88
+ }
89
+ </style>
90
+ </head>
91
+ <body>
92
+ <div class="container">
93
+ <h1>KLH10 PDP-10 Emulator</h1>
94
+
95
+ <div class="status" id="status">Initializing...</div>
96
+
97
+ <div class="controls" id="simpleControls">
98
+ <button id="autoBootBtn" class="boot-btn">Boot TOPS-20</button>
99
+ <div class="mode-toggle">
100
+ <a href="#" id="showAdvanced">Advanced Mode</a>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="controls" id="advancedControls" style="display: none;">
105
+ <button id="startBtn" disabled>Start Emulator</button>
106
+ <button id="resetBtn" disabled>Reset</button>
107
+ <button id="loadConfigBtn" disabled>Load TOPS-20 Config</button>
108
+ <button id="bootTops20Btn" disabled>BOOT TOPS-20</button>
109
+ <div class="mode-toggle">
110
+ <a href="#" id="showSimple">Simple Mode</a>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="terminal-container">
115
+ <div id="terminal"></div>
116
+ </div>
117
+
118
+ <div style="text-align: center; font-size: 12px; color: #888; margin-top: 20px;">
119
+ WebAssembly port via agentic coding by Rob Chang<br>
120
+ KLH10 PDP-10 Emulator by Kenneth L. Harrenstien<br>
121
+ Panda TOPS-20 distribution by Mark Crispin<br>
122
+ <span style="color: #666;">Networking is disabled in this build</span>
123
+ </div>
124
+ </div>
125
+
126
+ <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
127
+ <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
128
+ <script src="main.js"></script>
129
+ </body>
130
+ </html>
build/wasm/bld-kl/kn10-kl.js ADDED
The diff for this file is too large to render. See raw diff
 
build/wasm/bld-kl/kn10-kl.wasm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:65b7d723f5cb03cecf2303ee732493c9bb45142562fb686d101aa8d2a589509d
3
+ size 399870
build/wasm/bld-kl/main.js ADDED
@@ -0,0 +1,785 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Main web interface for KLH10 PDP-10 Emulator
2
+
3
+ // Ring Buffer Manager for shared memory communication (shared with worker)
4
+ class RingBufferManager {
5
+ constructor(sharedBuffer, baseOffset = 0) {
6
+ this.buffer = new Uint8Array(sharedBuffer);
7
+ this.view = new DataView(sharedBuffer);
8
+
9
+ // Buffer layout:
10
+ // Input ring buffer: offset 0, size 4112 (4096 data + 16 metadata)
11
+ // Output ring buffer: offset 4112, size 4112 (4096 data + 16 metadata)
12
+ // Control: offset 8224, size 16 (mode + flush_request + padding)
13
+
14
+ this.baseOffset = baseOffset;
15
+ this.inputOffset = baseOffset;
16
+ this.outputOffset = baseOffset + 4112;
17
+ this.controlOffset = baseOffset + 8224;
18
+ }
19
+
20
+ // Input ring buffer (JavaScript writes, WASM reads)
21
+ writeInputChar(char) {
22
+ const writePos = this.view.getUint32(this.inputOffset, true);
23
+ const readPos = this.view.getUint32(this.inputOffset + 4, true);
24
+ const size = 4096;
25
+
26
+ const nextWritePos = (writePos + 1) % size;
27
+ if (nextWritePos === readPos) {
28
+ return false; // Buffer full
29
+ }
30
+
31
+ this.buffer[this.inputOffset + 16 + writePos] = char.charCodeAt(0);
32
+ this.view.setUint32(this.inputOffset, nextWritePos, true);
33
+ return true;
34
+ }
35
+
36
+ // Output ring buffer (WASM writes, JavaScript reads)
37
+ readOutputChar() {
38
+ const writePos = this.view.getUint32(this.outputOffset, true);
39
+ const readPos = this.view.getUint32(this.outputOffset + 4, true);
40
+
41
+ if (readPos === writePos) {
42
+ return null; // Buffer empty
43
+ }
44
+
45
+ const char = this.buffer[this.outputOffset + 16 + readPos];
46
+ const nextReadPos = (readPos + 1) % 4096;
47
+ this.view.setUint32(this.outputOffset + 4, nextReadPos, true);
48
+
49
+ return String.fromCharCode(char);
50
+ }
51
+
52
+ hasOutputData() {
53
+ const writePos = this.view.getUint32(this.outputOffset, true);
54
+ const readPos = this.view.getUint32(this.outputOffset + 4, true);
55
+ return readPos !== writePos;
56
+ }
57
+ }
58
+
59
+ class KLH10WebInterface {
60
+ constructor() {
61
+ this.terminal = null;
62
+ this.fitAddon = null;
63
+ this.worker = null;
64
+ this.emulatorReady = false;
65
+ this.inRuncmdMode = true; // KLH10 starts in command mode by default
66
+ this.terminalEchoEnabled = true; // Terminal echo enabled by default
67
+ this.autoBootInProgress = false; // True during one-click boot sequence
68
+ this._readyResolve = null; // Promise resolve for waitForReady()
69
+
70
+ // Shared WebAssembly memory (proper approach)
71
+ this.wasmMemory = null;
72
+ this.inputRingBuffer = null;
73
+ this.outputRingBuffer = null;
74
+
75
+ this.initializeTerminal();
76
+ this.setupEventListeners();
77
+ this.updateStatus('Ready to start emulator', 'ready');
78
+ }
79
+
80
+ initializeTerminal() {
81
+ // Create xterm.js terminal
82
+ this.terminal = new Terminal({
83
+ theme: {
84
+ background: '#000000',
85
+ foreground: '#ffffff',
86
+ cursor: '#ffffff',
87
+ cursorAccent: '#000000',
88
+ selection: '#44475a',
89
+ },
90
+ fontFamily: 'Consolas, "Courier New", monospace',
91
+ fontSize: 14,
92
+ cursorBlink: true,
93
+ cols: 80,
94
+ rows: 24,
95
+ convertEol: true, // Convert \n to \r\n for proper line breaks
96
+ windowsMode: false // Keep Unix-style line endings in input
97
+ });
98
+
99
+ // Add fit addon for responsive terminal
100
+ this.fitAddon = new FitAddon.FitAddon();
101
+ this.terminal.loadAddon(this.fitAddon);
102
+
103
+ // Open terminal in DOM
104
+ this.terminal.open(document.getElementById('terminal'));
105
+ this.fitAddon.fit();
106
+ this.terminal.resize(this.terminal.cols, 24);
107
+
108
+ // Handle terminal input
109
+ this.terminal.onData((data) => {
110
+ if (this.autoBootInProgress) return; // Block input during auto-boot
111
+ if (this.worker && this.emulatorReady && this.inputRingBuffer) {
112
+ // Echo input based on both mode and echo setting
113
+ if (this.inRuncmdMode && this.terminalEchoEnabled) {
114
+ // Handle special characters
115
+ if (data === '\r' || data === '\n') {
116
+ this.terminal.write('\r\n');
117
+ } else if (data === '\b' || data === '\x7f') {
118
+ // Backspace handling
119
+ this.terminal.write('\b \b');
120
+ } else if (data >= ' ' && data <= '~') {
121
+ // Printable characters
122
+ this.terminal.write(data);
123
+ }
124
+ }
125
+
126
+ // Write directly to WASM memory ring buffer - true zero-copy!
127
+
128
+
129
+ for (let i = 0; i < data.length; i++) {
130
+ const char = data[i];
131
+ const success = this.inputRingBuffer.writeInputChar(char);
132
+
133
+
134
+ if (!success) {
135
+ console.warn('Main: Input ring buffer full, character dropped');
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ });
141
+
142
+ // Handle window resize (fit width, keep 24 rows)
143
+ window.addEventListener('resize', () => {
144
+ this.fitAddon.fit();
145
+ this.terminal.resize(this.terminal.cols, 24);
146
+ });
147
+
148
+ // Initial welcome message
149
+ this.terminal.writeln('\x1b[36mKLH10 PDP-10 Emulator - WebAssembly Port\x1b[0m');
150
+ this.terminal.writeln('Click "Boot TOPS-20" to begin...');
151
+ this.terminal.writeln('');
152
+
153
+ // Focus the terminal for immediate keyboard input
154
+ this.terminal.focus();
155
+ }
156
+
157
+ setupEventListeners() {
158
+ // Simple mode
159
+ document.getElementById('autoBootBtn').addEventListener('click', () => this.autoBoot());
160
+ document.getElementById('showAdvanced').addEventListener('click', (e) => {
161
+ e.preventDefault();
162
+ document.getElementById('simpleControls').style.display = 'none';
163
+ document.getElementById('advancedControls').style.display = '';
164
+ });
165
+ document.getElementById('showSimple').addEventListener('click', (e) => {
166
+ e.preventDefault();
167
+ document.getElementById('advancedControls').style.display = 'none';
168
+ document.getElementById('simpleControls').style.display = '';
169
+ });
170
+
171
+ // Advanced mode
172
+ const startBtn = document.getElementById('startBtn');
173
+ const resetBtn = document.getElementById('resetBtn');
174
+ const loadConfigBtn = document.getElementById('loadConfigBtn');
175
+ const bootTops20Btn = document.getElementById('bootTops20Btn');
176
+
177
+ startBtn.addEventListener('click', () => this.startEmulator());
178
+ resetBtn.addEventListener('click', () => this.resetEmulator());
179
+ loadConfigBtn.addEventListener('click', () => this.loadInstallationConfig());
180
+ bootTops20Btn.addEventListener('click', () => this.bootTops20());
181
+
182
+ // Enable start button
183
+ startBtn.disabled = false;
184
+ }
185
+
186
+ updateStatus(message, type = '') {
187
+ const statusEl = document.getElementById('status');
188
+ statusEl.textContent = message;
189
+ statusEl.className = `status ${type}`;
190
+ }
191
+
192
+ async startEmulator() {
193
+ try {
194
+ this.updateStatus('Loading emulator...', 'loading');
195
+ document.getElementById('startBtn').disabled = true;
196
+
197
+ this.terminal.writeln('Starting KLH10 emulator...');
198
+ this.terminal.writeln('');
199
+
200
+ // Create shared WebAssembly memory (proper architecture)
201
+ this.wasmMemory = new WebAssembly.Memory({
202
+ initial: 256, // 16MB initial
203
+ maximum: 512, // 32MB maximum
204
+ shared: true // CRITICAL: Shared between main thread and worker
205
+ });
206
+
207
+ // Ring buffers will be at known offsets in WASM memory
208
+ const RING_BUFFER_BASE = 0x10000; // 64KB offset
209
+
210
+ // Ring buffers will be at known offsets in WASM memory
211
+ // Create ONE ring buffer manager with the base offset
212
+ // It will handle both input (at base) and output (at base + 4112) internally
213
+ this.ringBufferManager = new RingBufferManager(this.wasmMemory.buffer, RING_BUFFER_BASE);
214
+
215
+ // For compatibility, create aliases that point to the same manager
216
+ this.inputRingBuffer = this.ringBufferManager;
217
+ this.outputRingBuffer = this.ringBufferManager;
218
+
219
+ // console.log('Main: Created shared WASM memory:', this.wasmMemory.buffer.byteLength, 'bytes');
220
+ // console.log('Main: Input ring at offset:', this.ringBufferManager.inputOffset.toString(16));
221
+ // console.log('Main: Output ring at offset:', this.ringBufferManager.outputOffset.toString(16));
222
+
223
+ // Create Web Worker for emulator with cache-busting
224
+ const timestamp = Date.now();
225
+ this.worker = new Worker(`emulator-worker.js?v=${timestamp}`);
226
+
227
+ // Handle messages from worker
228
+ this.worker.onmessage = (event) => {
229
+ const { type, data } = event.data;
230
+
231
+ switch (type) {
232
+ case 'ready':
233
+ this.emulatorReady = true;
234
+ this.updateStatus('Emulator ready', 'ready');
235
+ document.getElementById('resetBtn').disabled = false;
236
+ document.getElementById('loadConfigBtn').disabled = false;
237
+ document.getElementById('bootTops20Btn').disabled = true; // Only enable after config loaded
238
+
239
+ // Start polling output from shared ring buffer
240
+ this.startOutputPolling();
241
+
242
+ // Resolve waitForReady() promise if auto-booting
243
+ if (this._readyResolve) {
244
+ this._readyResolve();
245
+ this._readyResolve = null;
246
+ }
247
+
248
+ // Focus terminal after emulator starts
249
+ this.terminal.focus();
250
+ break;
251
+
252
+ case 'mode_change':
253
+ this.inRuncmdMode = (data === 'command');
254
+ // Echo is automatically disabled in RUN mode, enabled in CMDRUN mode
255
+ break;
256
+
257
+ case 'error':
258
+ this.updateStatus(`Error: ${data}`, 'error');
259
+ console.error('Emulator error:', data);
260
+ break;
261
+
262
+ case 'exit':
263
+ this.updateStatus('Emulator exited', '');
264
+ this.terminal.writeln(`\x1b[33mEmulator exited with code: ${data}\x1b[0m`);
265
+ this.emulatorReady = false;
266
+ document.getElementById('startBtn').disabled = false;
267
+ document.getElementById('resetBtn').disabled = true;
268
+ document.getElementById('loadConfigBtn').disabled = true;
269
+ document.getElementById('bootTops20Btn').disabled = true;
270
+
271
+ // Stop output polling
272
+ if (this.outputPollInterval) {
273
+ clearInterval(this.outputPollInterval);
274
+ this.outputPollInterval = null;
275
+ }
276
+ break;
277
+ }
278
+ };
279
+
280
+ this.worker.onerror = (error) => {
281
+ console.error('Worker error event:', error);
282
+ console.error('Worker error message:', error.message);
283
+ console.error('Worker error filename:', error.filename);
284
+ console.error('Worker error lineno:', error.lineno);
285
+ console.error('Worker error colno:', error.colno);
286
+ console.error('Worker error object:', error.error);
287
+
288
+ this.updateStatus('Worker error', 'error');
289
+ console.error('Worker error:', error.message || 'undefined');
290
+ if (error.filename) {
291
+ console.error(`Worker error location: ${error.filename}:${error.lineno}:${error.colno}`);
292
+ }
293
+ };
294
+
295
+ // Start the emulator with shared WASM memory
296
+ this.worker.postMessage({
297
+ type: 'start',
298
+ wasmMemory: this.wasmMemory,
299
+ ringBufferBase: 0x10000 // Tell worker where ring buffers are located
300
+ });
301
+
302
+ } catch (error) {
303
+ this.updateStatus(`Failed to start: ${error.message}`, 'error');
304
+ console.error('Failed to start emulator:', error.message);
305
+ document.getElementById('startBtn').disabled = false;
306
+ }
307
+ }
308
+
309
+ async loadInstallationConfig() {
310
+ if (!this.worker || !this.emulatorReady || !this.inputRingBuffer) {
311
+ console.warn('Load config attempted but emulator not ready');
312
+ return;
313
+ }
314
+
315
+ try {
316
+ // Load the config commands from the text file
317
+ const response = await fetch('tops20-config-commands.txt');
318
+ if (!response.ok) {
319
+ throw new Error('Could not load config commands file');
320
+ }
321
+
322
+ const commandsText = await response.text();
323
+ const lines = commandsText.split('\n');
324
+
325
+ // Process commands and filter out comments/empty lines
326
+ const configCommands = [];
327
+ for (const line of lines) {
328
+ const trimmed = line.trim();
329
+ if (trimmed && !trimmed.startsWith('#')) {
330
+ configCommands.push(trimmed);
331
+ }
332
+ }
333
+
334
+ this.terminal.writeln('\x1b[36mLoading TOPS-20 Installation Configuration...\x1b[0m');
335
+ this.terminal.writeln('\x1b[33mCommands loaded from: tops20-config-commands.txt\x1b[0m');
336
+
337
+ // Send each command with a delay to simulate typing
338
+ let delay = 0;
339
+ configCommands.forEach((command, index) => {
340
+ setTimeout(() => {
341
+ // Write command directly to WASM memory ring buffer
342
+ const fullCommand = command + '\r';
343
+ for (let i = 0; i < fullCommand.length; i++) {
344
+ this.inputRingBuffer.writeInputChar(fullCommand[i]);
345
+ }
346
+
347
+ // Show what we're sending if in command mode
348
+ if (this.inRuncmdMode) {
349
+ this.terminal.write(command + '\r\n');
350
+ }
351
+
352
+ // After all commands, show completion message and update button states
353
+ if (index === configCommands.length - 1) {
354
+ setTimeout(() => {
355
+ this.terminal.writeln('\x1b[32mConfiguration loaded! Ready to boot TOPS-20.\x1b[0m');
356
+
357
+ // Disable Load Config button and enable BOOT TOPS-20 button
358
+ document.getElementById('loadConfigBtn').disabled = true;
359
+ document.getElementById('bootTops20Btn').disabled = false;
360
+
361
+ // Focus terminal after config loads
362
+ this.terminal.focus();
363
+ }, 100);
364
+ }
365
+ }, delay);
366
+ delay += 500; // 500ms delay between commands
367
+ });
368
+
369
+ } catch (error) {
370
+ this.terminal.writeln(`\x1b[31mError loading config commands: ${error.message}\x1b[0m`);
371
+ console.error('Error loading config commands:', error);
372
+ }
373
+ }
374
+
375
+ async bootTops20() {
376
+ if (!this.worker || !this.emulatorReady || !this.inputRingBuffer) {
377
+ console.warn('Boot TOPS-20 attempted but emulator not ready');
378
+ return;
379
+ }
380
+
381
+ try {
382
+ // Load the boot commands from the text file
383
+ const response = await fetch('tops20-boot-commands.txt');
384
+ if (!response.ok) {
385
+ throw new Error('Could not load boot commands file');
386
+ }
387
+
388
+ const commandsText = await response.text();
389
+ const lines = commandsText.split('\n');
390
+
391
+ // Process commands and filter out comments/empty lines
392
+ const bootCommands = [];
393
+ for (const line of lines) {
394
+ const trimmed = line.trim();
395
+ if (trimmed && !trimmed.startsWith('#')) {
396
+ // Check if line starts with any { } command
397
+ if (trimmed.startsWith('{')) {
398
+ // Lines starting with { } commands - handle specially
399
+ if (trimmed === '{date-time}') {
400
+ // Generate current date/time in TOPS-20 format: DD-MMM-YYYY HHMM
401
+ const now = new Date();
402
+ const day = now.getDate().toString().padStart(2, '0');
403
+ const months = ['JAN','FEB','MAR','APR','MAY','JUN',
404
+ 'JUL','AUG','SEP','OCT','NOV','DEC'];
405
+ const month = months[now.getMonth()];
406
+ const year = now.getFullYear();
407
+ const hours = now.getHours().toString().padStart(2, '0');
408
+ const minutes = now.getMinutes().toString().padStart(2, '0');
409
+ const dateTime = `${day}-${month}-${year} ${hours}${minutes}`;
410
+ bootCommands.push({ command: dateTime, noCr: true }); // No CR for { } command lines
411
+ } else if (trimmed === '{ctrl-c}') {
412
+ // Send CTRL-C (ASCII 3)
413
+ bootCommands.push({ command: '\x03', noCr: true }); // No CR for { } command lines
414
+ } else if (trimmed.match(/^\{wait \d+\}$/)) {
415
+ // Extract wait duration from {wait #} format
416
+ const waitMatch = trimmed.match(/^\{wait (\d+)\}$/);
417
+ const waitSeconds = parseInt(waitMatch[1], 10);
418
+ bootCommands.push({
419
+ command: null,
420
+ noCr: true,
421
+ waitSeconds: waitSeconds
422
+ });
423
+ } else if (trimmed === '{echo on}') {
424
+ // Turn on terminal echo
425
+ bootCommands.push({
426
+ command: null,
427
+ noCr: true,
428
+ echoControl: 'on'
429
+ });
430
+ } else if (trimmed === '{echo off}') {
431
+ // Turn off terminal echo
432
+ bootCommands.push({
433
+ command: null,
434
+ noCr: true,
435
+ echoControl: 'off'
436
+ });
437
+ } else if (trimmed === '{cr}') {
438
+ // Send carriage return
439
+ bootCommands.push({
440
+ command: '\r',
441
+ noCr: true // Already includes CR, don't add another
442
+ });
443
+ }
444
+ // Note: Lines starting with { } that we don't recognize are ignored
445
+ } else {
446
+ // Process {esc} and {nocr} inline within commands
447
+ let processedCommand = trimmed;
448
+ console.log(`[BOOT SCRIPT PARSE] Processing line: "${trimmed}"`);
449
+ processedCommand = processedCommand.replace(/\{esc\}/g, '\x1b'); // ESC character (ASCII 27)
450
+
451
+ // Handle {nocr} - mark command to not add \r
452
+ const hasNoCr = processedCommand.includes('{nocr}');
453
+ processedCommand = processedCommand.replace(/\{nocr\}/g, ''); // Remove {nocr} markers
454
+
455
+ console.log(`[BOOT SCRIPT PARSE] Final processed command: "${processedCommand}", noCr: ${hasNoCr}`);
456
+
457
+ // Store command with nocr flag
458
+ bootCommands.push({
459
+ command: processedCommand,
460
+ noCr: hasNoCr
461
+ });
462
+ }
463
+ }
464
+ }
465
+
466
+ this.terminal.writeln('\x1b[36mStarting TOPS-20 boot sequence...\x1b[0m');
467
+ this.terminal.writeln('\x1b[33mCommands loaded from: tops20-boot-commands.txt\x1b[0m');
468
+
469
+ // Disable the boot button immediately when starting
470
+ document.getElementById('bootTops20Btn').disabled = true;
471
+
472
+ // Execute commands sequentially with non-blocking delays
473
+ this.executeCommandsSequentially(bootCommands, 0);
474
+ } catch (error) {
475
+ console.error('Error executing boot script:', error);
476
+ this.terminal.writeln('\x1b[31mError executing boot script: ' + error.message + '\x1b[0m');
477
+ }
478
+ }
479
+
480
+ executeCommandsSequentially(bootCommands, index, onComplete) {
481
+ if (index >= bootCommands.length) {
482
+ // All commands completed
483
+ if (!this.autoBootInProgress) {
484
+ this.terminal.writeln('\x1b[32mBOOT TOPS-20 sequence completed!\x1b[0m');
485
+ this.terminal.writeln('\x1b[33mContinue interacting with TOPS-20 system manually.\x1b[0m');
486
+ }
487
+ this.terminal.focus();
488
+ if (onComplete) onComplete();
489
+ return;
490
+ }
491
+
492
+ const commandEntry = bootCommands[index];
493
+ // Handle both string commands and object commands
494
+ let command, noCr, waitSeconds, echoControl;
495
+ if (typeof commandEntry === 'string') {
496
+ command = commandEntry;
497
+ noCr = false;
498
+ waitSeconds = 0;
499
+ echoControl = null;
500
+ } else {
501
+ command = commandEntry.command;
502
+ noCr = commandEntry.noCr;
503
+ waitSeconds = commandEntry.waitSeconds || 0;
504
+ echoControl = commandEntry.echoControl || null;
505
+ }
506
+
507
+ // Handle echo control commands
508
+ if (echoControl) {
509
+ if (echoControl === 'on') {
510
+ this.terminalEchoEnabled = true;
511
+ this.terminal.writeln('\x1b[32mTerminal echo enabled\x1b[0m');
512
+ } else if (echoControl === 'off') {
513
+ this.terminalEchoEnabled = false;
514
+ this.terminal.writeln('\x1b[33mTerminal echo disabled\x1b[0m');
515
+ }
516
+ // Continue with next command immediately
517
+ this.executeCommandsSequentially(bootCommands, index + 1, onComplete);
518
+ return;
519
+ }
520
+
521
+ // Handle wait commands
522
+ if (waitSeconds > 0) {
523
+ this.terminal.writeln(`\x1b[33mWaiting ${waitSeconds} seconds...\x1b[0m`);
524
+ // Wait and then continue with next command - this allows event loop to run
525
+ setTimeout(() => {
526
+ this.executeCommandsSequentially(bootCommands, index + 1, onComplete);
527
+ }, waitSeconds * 1000);
528
+ return;
529
+ }
530
+
531
+ // Skip null commands
532
+ if (command === null || command === undefined) {
533
+ this.executeCommandsSequentially(bootCommands, index + 1, onComplete);
534
+ return;
535
+ }
536
+
537
+ // Debug: Log what we're about to send
538
+ console.log(`[BOOT SCRIPT] Sending command: "${command}" (length: ${command.length}), noCr: ${noCr}`);
539
+ console.log(`[BOOT SCRIPT] Command char codes:`, Array.from(command).map(c => `${c}(${c.charCodeAt(0)})`).join(' '));
540
+
541
+ // Write command directly to WASM memory ring buffer
542
+ const fullCommand = noCr ? command : command + '\r';
543
+ console.log(`[BOOT SCRIPT] Full command: "${fullCommand}" (length: ${fullCommand.length})`);
544
+ for (let i = 0; i < fullCommand.length; i++) {
545
+ this.inputRingBuffer.writeInputChar(fullCommand[i]);
546
+ }
547
+
548
+ // Small delay before next command to allow processing
549
+ setTimeout(() => {
550
+ this.executeCommandsSequentially(bootCommands, index + 1, onComplete);
551
+ }, 500); // Small delay but allows event loop to run
552
+ }
553
+
554
+
555
+ startOutputPolling() {
556
+ // Poll output ring buffer at 60fps
557
+ this.outputPollInterval = setInterval(() => {
558
+ this.drainOutputBuffer();
559
+ }, 16); // ~60fps
560
+ }
561
+
562
+ drainOutputBuffer() {
563
+ if (!this.outputRingBuffer) return;
564
+
565
+ // Check for flush request from WASM side
566
+ const flushRequest = this.outputRingBuffer.view.getUint32(this.outputRingBuffer.controlOffset + 4, true);
567
+ if (flushRequest) {
568
+ // console.log('[JS_FLUSH] Detected flush request from WASM side');
569
+ }
570
+
571
+ let output = '';
572
+ let charCount = 0;
573
+ while (this.outputRingBuffer.hasOutputData()) {
574
+ const char = this.outputRingBuffer.readOutputChar();
575
+ if (char) {
576
+ output += char;
577
+ charCount++;
578
+ }
579
+ }
580
+
581
+ if (output.length > 0) {
582
+ this.terminal.write(output);
583
+ }
584
+
585
+ // If there was a flush request, force terminal to flush and clear the request
586
+ if (flushRequest) {
587
+ // console.log('[JS_FLUSH] Processing flush request, output.length=' + output.length);
588
+ // Force immediate terminal update - this ensures characters appear right away
589
+ if (output.length > 0) {
590
+ // Terminal write is already called above, just ensure it's processed
591
+ setTimeout(() => {}, 0); // Allow browser to process the write immediately
592
+ }
593
+ // Clear the flush request
594
+ this.outputRingBuffer.view.setUint32(this.outputRingBuffer.controlOffset + 4, 0, true);
595
+ // console.log('[JS_FLUSH] Cleared flush request');
596
+ }
597
+ }
598
+
599
+ resetEmulator() {
600
+ if (this.worker) {
601
+ this.worker.terminate();
602
+ this.worker = null;
603
+ }
604
+
605
+ // Stop output polling
606
+ if (this.outputPollInterval) {
607
+ clearInterval(this.outputPollInterval);
608
+ this.outputPollInterval = null;
609
+ }
610
+
611
+ // Clean up shared resources
612
+ this.wasmMemory = null;
613
+ this.inputRingBuffer = null;
614
+ this.outputRingBuffer = null;
615
+ this.emulatorReady = false;
616
+ this.autoBootInProgress = false;
617
+ this._readyResolve = null;
618
+
619
+ this.terminal.clear();
620
+ this.terminal.writeln('\x1b[36mKLH10 PDP-10 Emulator - WebAssembly Port\x1b[0m');
621
+ this.terminal.writeln('Click "Boot TOPS-20" to begin...');
622
+ this.terminal.writeln('');
623
+
624
+ this.updateStatus('Ready to start emulator', 'ready');
625
+ document.getElementById('startBtn').disabled = false;
626
+ document.getElementById('resetBtn').disabled = true;
627
+ document.getElementById('loadConfigBtn').disabled = true;
628
+ document.getElementById('bootTops20Btn').disabled = true;
629
+ document.getElementById('autoBootBtn').disabled = false;
630
+
631
+ // Focus terminal after reset
632
+ this.terminal.focus();
633
+ }
634
+
635
+ // --- Simple Mode (one-click boot) ---
636
+
637
+ waitForReady() {
638
+ if (this.emulatorReady) return Promise.resolve();
639
+ return new Promise((resolve) => {
640
+ this._readyResolve = resolve;
641
+ });
642
+ }
643
+
644
+ loadInstallationConfigAsync() {
645
+ return new Promise((resolve, reject) => {
646
+ if (!this.worker || !this.emulatorReady || !this.inputRingBuffer) {
647
+ reject(new Error('Emulator not ready'));
648
+ return;
649
+ }
650
+
651
+ fetch('tops20-config-commands.txt')
652
+ .then(response => {
653
+ if (!response.ok) throw new Error('Could not load config commands file');
654
+ return response.text();
655
+ })
656
+ .then(commandsText => {
657
+ const lines = commandsText.split('\n');
658
+ const configCommands = [];
659
+ for (const line of lines) {
660
+ const trimmed = line.trim();
661
+ if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith(';')) {
662
+ configCommands.push(trimmed);
663
+ }
664
+ }
665
+
666
+ let delay = 0;
667
+ configCommands.forEach((command, index) => {
668
+ setTimeout(() => {
669
+ const fullCommand = command + '\r';
670
+ for (let i = 0; i < fullCommand.length; i++) {
671
+ this.inputRingBuffer.writeInputChar(fullCommand[i]);
672
+ }
673
+ if (this.inRuncmdMode) {
674
+ this.terminal.write(command + '\r\n');
675
+ }
676
+ if (index === configCommands.length - 1) {
677
+ setTimeout(() => {
678
+ resolve();
679
+ }, 100);
680
+ }
681
+ }, delay);
682
+ delay += 500;
683
+ });
684
+ })
685
+ .catch(reject);
686
+ });
687
+ }
688
+
689
+ bootTops20Async() {
690
+ return new Promise((resolve, reject) => {
691
+ if (!this.worker || !this.emulatorReady || !this.inputRingBuffer) {
692
+ reject(new Error('Emulator not ready'));
693
+ return;
694
+ }
695
+
696
+ fetch('tops20-boot-commands.txt')
697
+ .then(response => {
698
+ if (!response.ok) throw new Error('Could not load boot commands file');
699
+ return response.text();
700
+ })
701
+ .then(commandsText => {
702
+ const lines = commandsText.split('\n');
703
+ const bootCommands = [];
704
+ for (const line of lines) {
705
+ const trimmed = line.trim();
706
+ if (trimmed && !trimmed.startsWith('#')) {
707
+ if (trimmed.startsWith('{')) {
708
+ if (trimmed === '{date-time}') {
709
+ const now = new Date();
710
+ const day = now.getDate().toString().padStart(2, '0');
711
+ const months = ['JAN','FEB','MAR','APR','MAY','JUN',
712
+ 'JUL','AUG','SEP','OCT','NOV','DEC'];
713
+ const month = months[now.getMonth()];
714
+ const year = now.getFullYear();
715
+ const hours = now.getHours().toString().padStart(2, '0');
716
+ const minutes = now.getMinutes().toString().padStart(2, '0');
717
+ const dateTime = `${day}-${month}-${year} ${hours}${minutes}`;
718
+ bootCommands.push({ command: dateTime, noCr: true });
719
+ } else if (trimmed === '{ctrl-c}') {
720
+ bootCommands.push({ command: '\x03', noCr: true });
721
+ } else if (trimmed.match(/^\{wait \d+\}$/)) {
722
+ const waitMatch = trimmed.match(/^\{wait (\d+)\}$/);
723
+ bootCommands.push({ command: null, noCr: true, waitSeconds: parseInt(waitMatch[1], 10) });
724
+ } else if (trimmed === '{echo on}') {
725
+ bootCommands.push({ command: null, noCr: true, echoControl: 'on' });
726
+ } else if (trimmed === '{echo off}') {
727
+ bootCommands.push({ command: null, noCr: true, echoControl: 'off' });
728
+ } else if (trimmed === '{cr}') {
729
+ bootCommands.push({ command: '\r', noCr: true });
730
+ }
731
+ } else {
732
+ let processedCommand = trimmed;
733
+ processedCommand = processedCommand.replace(/\{esc\}/g, '\x1b');
734
+ const hasNoCr = processedCommand.includes('{nocr}');
735
+ processedCommand = processedCommand.replace(/\{nocr\}/g, '');
736
+ bootCommands.push({ command: processedCommand, noCr: hasNoCr });
737
+ }
738
+ }
739
+ }
740
+
741
+ this.executeCommandsSequentially(bootCommands, 0, resolve);
742
+ })
743
+ .catch(reject);
744
+ });
745
+ }
746
+
747
+ async autoBoot() {
748
+ this.autoBootInProgress = true;
749
+ document.getElementById('autoBootBtn').disabled = true;
750
+
751
+ try {
752
+ // Step 1: Start emulator
753
+ this.updateStatus('Initializing emulator... please wait', 'loading');
754
+ await this.startEmulator();
755
+
756
+ // Step 2: Wait for worker to be ready (loads WASM + disk image)
757
+ this.terminal.writeln('\x1b[33mPlease wait — loading disk image (~476 MB)...\x1b[0m');
758
+ await this.waitForReady();
759
+
760
+ // Step 3: Load config
761
+ this.updateStatus('Configuring virtual hardware... please wait', 'loading');
762
+ await this.loadInstallationConfigAsync();
763
+
764
+ // Step 4: Boot
765
+ this.updateStatus('Booting TOPS-20... please wait', 'loading');
766
+ this.terminal.writeln('\x1b[33mPlease wait — TOPS-20 is booting...\x1b[0m');
767
+ await this.bootTops20Async();
768
+
769
+ // Done
770
+ this.autoBootInProgress = false;
771
+ this.updateStatus('TOPS-20 ready', 'ready');
772
+ this.terminal.focus();
773
+ } catch (error) {
774
+ this.autoBootInProgress = false;
775
+ this.updateStatus(`Boot failed: ${error.message}`, 'error');
776
+ document.getElementById('autoBootBtn').disabled = false;
777
+ console.error('Auto-boot failed:', error);
778
+ }
779
+ }
780
+ }
781
+
782
+ // Initialize the interface when DOM is loaded
783
+ document.addEventListener('DOMContentLoaded', () => {
784
+ new KLH10WebInterface();
785
+ });
build/wasm/bld-kl/mtboot.sav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a4a495622d642b926b87aa1ac63a7fd719cb1b27a877de4b1f8353a54735251e
3
+ size 20460
build/wasm/bld-kl/serve.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ const http = require('http');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const PORT = process.env.PORT || 8080;
8
+ const HOSTNAME = '0.0.0.0'; // Allow external access
9
+
10
+ // MIME types for web assets
11
+ const mimeTypes = {
12
+ '.html': 'text/html',
13
+ '.js': 'application/javascript',
14
+ '.wasm': 'application/wasm',
15
+ '.css': 'text/css',
16
+ '.ico': 'image/x-icon',
17
+ '.tap': 'application/octet-stream',
18
+ '.sav': 'application/octet-stream'
19
+ };
20
+
21
+ // Special handling for Emscripten generated files
22
+ const getContentType = (filePath) => {
23
+ const extname = path.extname(filePath);
24
+ const basename = path.basename(filePath);
25
+
26
+ // Handle kn10-ks (Emscripten JS file without extension)
27
+ if (basename === 'kn10-ks' || basename.startsWith('kn10-') && !extname) {
28
+ return 'application/javascript';
29
+ }
30
+
31
+ return mimeTypes[extname] || 'text/plain';
32
+ };
33
+
34
+ const server = http.createServer((req, res) => {
35
+ // Remove query parameters for file lookup
36
+ const urlPath = req.url.split('?')[0];
37
+ let filePath = path.join(__dirname, urlPath === '/' ? 'index.html' : urlPath);
38
+
39
+ // Security: prevent directory traversal
40
+ if (!filePath.startsWith(__dirname)) {
41
+ res.writeHead(403);
42
+ res.end('Forbidden');
43
+ return;
44
+ }
45
+
46
+ const contentType = getContentType(filePath);
47
+
48
+ fs.readFile(filePath, (error, content) => {
49
+ if (error) {
50
+ if (error.code === 'ENOENT') {
51
+ res.writeHead(404);
52
+ res.end('File not found');
53
+ } else {
54
+ res.writeHead(500);
55
+ res.end('Server error: ' + error.code);
56
+ }
57
+ } else {
58
+ // Set CORS headers for SharedArrayBuffer support
59
+ res.setHeader('Access-Control-Allow-Origin', '*');
60
+ res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
61
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
62
+
63
+ res.writeHead(200, { 'Content-Type': contentType });
64
+ res.end(content);
65
+ }
66
+ });
67
+ });
68
+
69
+ server.listen(PORT, HOSTNAME, () => {
70
+ console.log(`🚀 KLH10 WebAssembly Emulator Server`);
71
+ console.log(`📡 Serving at http://${HOSTNAME}:${PORT}/`);
72
+ console.log(`🌐 Open in browser to start the PDP-10 emulator`);
73
+ console.log(`📁 Serving files from: ${__dirname}`);
74
+ console.log('');
75
+ console.log('Press Ctrl+C to stop the server');
76
+ });
77
+
78
+ process.on('SIGTERM', () => {
79
+ console.log('\\n👋 Server shutting down...');
80
+ server.close(() => {
81
+ console.log('✅ Server stopped');
82
+ process.exit(0);
83
+ });
84
+ });
build/wasm/bld-kl/tops20-boot-commands.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TOPS-20 Boot Commands for KLH10 WebAssembly
2
+ # Based on doc/kst20.txt installation sequence
3
+ #
4
+ # Available script commands:
5
+ # {date-time} - Insert current date/time in TOPS-20 format (DD-MMM-YYYY HHMM)
6
+ # {ctrl-c} - Send CTRL-C character (ASCII 3)
7
+ # {wait #} - Wait for # seconds before continuing (e.g. {wait 3})
8
+ # {esc} - Insert ESC character (ASCII 27) - can be used inline
9
+ # {nocr} - Don't add carriage return (\r) at end of command - can be used inline
10
+ # {echo on} - Enable terminal echo (keyboard input is displayed)
11
+ # {echo off} - Disable terminal echo (keyboard input is not displayed)
12
+ # {cr} - Send carriage return (\r) character
13
+ #
14
+ # Notes:
15
+ # - Lines starting with # are comments and ignored
16
+ # - Empty lines are ignored
17
+ # - {esc} and {nocr} can be used anywhere within a command line; others must be at start of line
18
+ # - {nocr} prevents the automatic \r that is normally added to each command
19
+ # - Terminal echo is enabled by default; use {echo off} to hide typed input
20
+ # - {echo on/off} only affect keyboard input visibility, not script commands
21
+ # 0. GO
22
+ go
23
+ {echo off}
24
+ {cr}
25
+ {wait 5}
26
+ new
27
+ {wait 1}
28
+ no
29
+ {wait 10}
30
+ {ctrl-c}
31
+ log operator dec-20
32
+ enable
33
+ terminal vt100
build/wasm/bld-kl/tops20-config-commands.txt ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ; Define basic device config - one DTE, one disk, one tape
2
+ devdef dte0 200 dte master
3
+ devdef rh0 540 rh20
4
+ devdef rh1 544 rh20
5
+ devdef dsk0 rh0.0 rp type=rp07 format=dbd9
6
+ devdef mta0 rh1.0 tm03 type=tu45
7
+
8
+ ; Ethernet interface disabled for WebAssembly build
9
+ ; devdef ni0 564 ni20 ipaddr=127.0.0.1 debug=1
10
+
11
+ ; Define the idle device on 700
12
+ devdef idler 700 host
13
+
14
+ ; Set switch register
15
+ set sw=254200,,147
16
+
17
+ ; Set console lights I/O base register
18
+ ;Uncomment the following line if you have a Panda display.
19
+ ;lights 378
20
+
21
+ ; Mount disk image file to disk drive
22
+ devmount dsk0 RH20.RP07.1
23
+
24
+ ; Load disk bootstrap directly
25
+ load boot.sav
26
+
27
+ ; Ready to GO
download-images.sh ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # download-images.sh — Downloads TOPS-20 disk and tape images for KLH10 WASM builds
3
+ #
4
+ # These are historical DEC and community-maintained software distribution images.
5
+ # All files are freely available from public PDP-10 preservation archives.
6
+ #
7
+ # Sources:
8
+ # - DEC TOPS-20 tapes: http://pdp-10.trailing-edge.com/tapes/
9
+ # - Panda TOPS-20 distribution: https://panda.trailing-edge.com/
10
+ # - Panda tape image: https://github.com/PDP-10/panda
11
+ #
12
+ # Requirements: curl, bunzip2, tar
13
+ #
14
+ # Verified checksums (MD5):
15
+ # RH20.RP07.1 1cee5bd59bfcf0a0876360c195293998
16
+ # bb-h137f-bm.tap a82ad1f21d28dd5bd905fe24b789eb5c
17
+ # panda.tap c543517319a2b37025531f7d798ebb74
18
+
19
+ set -e
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
22
+ BLD_KL="$SCRIPT_DIR/build/wasm/bld-kl"
23
+ BLD_KS="$SCRIPT_DIR/build/wasm/bld-ks"
24
+ TAPES_DIR="$SCRIPT_DIR/tapes"
25
+ TMPDIR="${TMPDIR:-/tmp}/klh10-download-$$"
26
+
27
+ # Expected MD5 checksums
28
+ MD5_RH20="1cee5bd59bfcf0a0876360c195293998"
29
+ MD5_BB_H137F="a82ad1f21d28dd5bd905fe24b789eb5c"
30
+ MD5_PANDA_TAP="c543517319a2b37025531f7d798ebb74"
31
+
32
+ cleanup() {
33
+ rm -rf "$TMPDIR"
34
+ }
35
+ trap cleanup EXIT
36
+
37
+ check_md5() {
38
+ local file="$1"
39
+ local expected="$2"
40
+ local actual
41
+ actual=$(md5sum "$file" | cut -d' ' -f1)
42
+ if [ "$actual" = "$expected" ]; then
43
+ echo " checksum OK: $actual"
44
+ return 0
45
+ else
46
+ echo " checksum MISMATCH: expected $expected, got $actual"
47
+ return 1
48
+ fi
49
+ }
50
+
51
+ echo "=============================================="
52
+ echo "KLH10 TOPS-20 Image Downloader"
53
+ echo "=============================================="
54
+ echo ""
55
+
56
+ # Check prerequisites
57
+ for cmd in curl bunzip2 tar md5sum; do
58
+ if ! command -v "$cmd" >/dev/null 2>&1; then
59
+ echo "ERROR: Required command '$cmd' not found. Please install it."
60
+ exit 1
61
+ fi
62
+ done
63
+
64
+ mkdir -p "$TMPDIR"
65
+ mkdir -p "$BLD_KL"
66
+
67
+ # -----------------------------------------------
68
+ # 1. Download RH20.RP07.1 (Panda TOPS-20 pre-installed disk image)
69
+ # Source: Mark Crispin's Panda distribution
70
+ # From: https://panda.trailing-edge.com/panda-dist.tar.gz
71
+ # -----------------------------------------------
72
+ if [ -f "$BLD_KL/RH20.RP07.1" ]; then
73
+ echo "[1/3] RH20.RP07.1 already exists in $BLD_KL/, verifying..."
74
+ check_md5 "$BLD_KL/RH20.RP07.1" "$MD5_RH20" || {
75
+ echo " Existing file has wrong checksum, re-downloading..."
76
+ rm -f "$BLD_KL/RH20.RP07.1"
77
+ }
78
+ fi
79
+
80
+ if [ ! -f "$BLD_KL/RH20.RP07.1" ]; then
81
+ echo "[1/3] Downloading Panda distribution (221 MB compressed)..."
82
+ echo " Source: https://panda.trailing-edge.com/panda-dist.tar.gz"
83
+ echo " This is Mark Crispin's enhanced TOPS-20 with pre-installed system on RP07 disk."
84
+ curl -L --progress-bar -o "$TMPDIR/panda-dist.tar.gz" \
85
+ "https://panda.trailing-edge.com/panda-dist.tar.gz"
86
+
87
+ echo " Extracting RH20.RP07.1 (476 MB)..."
88
+ tar xzf "$TMPDIR/panda-dist.tar.gz" -C "$TMPDIR" panda-dist/RH20.RP07.1
89
+ mv "$TMPDIR/panda-dist/RH20.RP07.1" "$BLD_KL/RH20.RP07.1"
90
+ rm -f "$TMPDIR/panda-dist.tar.gz"
91
+ rm -rf "$TMPDIR/panda-dist"
92
+
93
+ echo " Verifying RH20.RP07.1..."
94
+ check_md5 "$BLD_KL/RH20.RP07.1" "$MD5_RH20"
95
+ fi
96
+ echo ""
97
+
98
+ # -----------------------------------------------
99
+ # 2. Copy boot.sav and mtboot.sav from source tree
100
+ # These are small files already in the repository.
101
+ # boot.sav = disk bootstrap loader (23 KB)
102
+ # mtboot.sav = tape bootstrap loader (20 KB)
103
+ # -----------------------------------------------
104
+ echo "[2/3] Copying bootstrap loaders from run/klt20/..."
105
+ if [ -f "$SCRIPT_DIR/run/klt20/boot.sav" ]; then
106
+ cp "$SCRIPT_DIR/run/klt20/boot.sav" "$BLD_KL/boot.sav"
107
+ echo " boot.sav copied ($(du -h "$BLD_KL/boot.sav" | cut -f1))"
108
+ else
109
+ echo " WARNING: run/klt20/boot.sav not found"
110
+ fi
111
+ if [ -f "$SCRIPT_DIR/run/klt20/mtboot.sav" ]; then
112
+ cp "$SCRIPT_DIR/run/klt20/mtboot.sav" "$BLD_KL/mtboot.sav"
113
+ echo " mtboot.sav copied ($(du -h "$BLD_KL/mtboot.sav" | cut -f1))"
114
+ else
115
+ echo " WARNING: run/klt20/mtboot.sav not found"
116
+ fi
117
+ echo ""
118
+
119
+ # -----------------------------------------------
120
+ # 3. Download bb-h137f-bm.tap (DEC TOPS-20 V7.0 installation tape)
121
+ # Source: DEC distribution BB-H137F-BM, "TOPS-20 V7.0 INSTL 16MT9"
122
+ # From: http://pdp-10.trailing-edge.com/tapes/
123
+ # Note: Not required for normal boot (we boot from disk), but useful
124
+ # for fresh TOPS-20 installations from tape.
125
+ # -----------------------------------------------
126
+ if [ -f "$BLD_KL/bb-h137f-bm.tap" ]; then
127
+ echo "[3/3] bb-h137f-bm.tap already exists, verifying..."
128
+ check_md5 "$BLD_KL/bb-h137f-bm.tap" "$MD5_BB_H137F" || {
129
+ echo " Existing file has wrong checksum, re-downloading..."
130
+ rm -f "$BLD_KL/bb-h137f-bm.tap"
131
+ }
132
+ fi
133
+
134
+ if [ ! -f "$BLD_KL/bb-h137f-bm.tap" ]; then
135
+ echo "[3/3] Downloading DEC TOPS-20 V7.0 install tape (8.5 MB compressed)..."
136
+ echo " Source: http://pdp-10.trailing-edge.com/tapes/bb-h137f-bm.tap.bz2"
137
+ echo " DEC part number BB-H137F-BM, 'TOPS-20 V7.0 INSTL 16MT9', copyright 1988 DEC."
138
+ curl -L --progress-bar -o "$TMPDIR/bb-h137f-bm.tap.bz2" \
139
+ "http://pdp-10.trailing-edge.com/tapes/bb-h137f-bm.tap.bz2"
140
+ bunzip2 "$TMPDIR/bb-h137f-bm.tap.bz2"
141
+ mv "$TMPDIR/bb-h137f-bm.tap" "$BLD_KL/bb-h137f-bm.tap"
142
+
143
+ echo " Verifying bb-h137f-bm.tap..."
144
+ check_md5 "$BLD_KL/bb-h137f-bm.tap" "$MD5_BB_H137F"
145
+ fi
146
+ echo ""
147
+
148
+ # -----------------------------------------------
149
+ # Summary
150
+ # -----------------------------------------------
151
+ echo "=============================================="
152
+ echo "Download complete! Files in $BLD_KL/:"
153
+ echo "=============================================="
154
+ ls -lh "$BLD_KL/RH20.RP07.1" "$BLD_KL/boot.sav" "$BLD_KL/mtboot.sav" "$BLD_KL/bb-h137f-bm.tap" 2>/dev/null
155
+ echo ""
156
+ echo "To run the emulator:"
157
+ echo " cd build/wasm/bld-kl"
158
+ echo " node serve.js"
159
+ echo " # Open http://localhost:8080"
160
+ echo " # Click 'Boot TOPS-20' and wait ~30 seconds"
run/klt20/boot.sav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2e0c3d1ea76b81fa1c5aaa8b3922acacc7e87f168befa35f4867939389f764f
3
+ size 23280
run/klt20/mtboot.sav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a4a495622d642b926b87aa1ac63a7fd719cb1b27a877de4b1f8353a54735251e
3
+ size 20460