rk86 2.0.16 → 2.0.17

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.
Files changed (3) hide show
  1. package/README.md +71 -13
  2. package/package.json +1 -1
  3. package/rk86.js +95 -17
package/README.md CHANGED
@@ -5,24 +5,33 @@
5
5
  ## Запуск
6
6
 
7
7
  ```bash
8
- bunx rk86
8
+ bunx rk86 # встроенный монитор mon32
9
+ bunx rk86 CHESS.GAM # загрузить и запустить файл из текущей директории
10
+ bunx rk86 -p CHESS.GAM # загрузить без запуска
11
+ bunx rk86 -g 0x100 prog.bin # запуск с адреса 0x100
12
+ bunx rk86 -m mon16.bin # другой монитор
13
+ bunx rk86 prog.asm # собрать и запустить .asm (i8080)
14
+ bunx rk86 -l # список файлов встроенного каталога
9
15
  ```
10
16
 
11
- ## С программой
12
-
13
- ```bash
14
- bunx rk86 CHESS.GAM
15
- ```
16
-
17
- Файл программы загружается из текущей директории.
18
-
19
17
  ## Опции
20
18
 
21
19
  ```
22
- -h справка
23
- -l список файлов из каталога
24
- -m <файл> монитор (по умолчанию: встроенный mon32.bin)
25
- -p загрузить файл без запуска
20
+ -v версия
21
+ -h справка
22
+ -l список файлов из каталога
23
+ -m <файл> монитор (по умолчанию: встроенный mon32.bin)
24
+ -p загрузить файл без запуска
25
+ -g <адрес> адрес запуска (несовместим с -p)
26
+ --exit-halt выход при выполнении HLT
27
+ --exit-address [адрес] выход при переходе на адрес (по умолчанию: 0xFFFE)
28
+ --headless без отображения экрана (для автотестов)
29
+ --timeout <сек> выход по таймауту
30
+ --memory <файл> сохранить память в файл при выходе
31
+ --memory-from <адрес> начало области дампа памяти (по умолчанию: 0x0000)
32
+ --memory-to <адрес> конец области дампа памяти включительно (по умолчанию: 0xFFFF)
33
+ --screen <файл> сохранить экран 78x30 как текст при выходе
34
+ --input <seq> инъекция клавиш (через запятую): KeyA,Digit1,Enter,...
26
35
  ```
27
36
 
28
37
  ## Управление
@@ -30,6 +39,55 @@ bunx rk86 CHESS.GAM
30
39
  - Клавиатура работает через stdin (raw mode)
31
40
  - `Ctrl+C` — выход
32
41
 
42
+ ## Безголовый режим (headless) и автотесты
43
+
44
+ Флаг `--headless` отключает вывод на терминал и настройку stdin — удобно для
45
+ автоматизированных e2e-проверок. Обычно комбинируется с `--input`, `--timeout`,
46
+ `--exit-halt` и `--screen`/`--memory` для анализа результата.
47
+
48
+ Формат `--input`: через запятую указываются WebKit-коды клавиш
49
+ (`KeyA`..`KeyZ`, `Digit0`..`Digit9`, `Enter`, `Space`, `Comma`, `Period`,
50
+ `Backspace`, `ArrowLeft`, `F1`..`F10`, и т.д.). Инъекция начинается после того,
51
+ как эмулятор инициализируется (как при подгрузке файла — с небольшой задержкой),
52
+ клавиши нажимаются по одной с минимальной паузой.
53
+
54
+ Формат файла `--screen`: 30 строк по 78 символов, разделитель `\r\n`.
55
+ Байты `\0`, `\t`, `\n`, `\r` заменяются на `.`, остальные символы с кодами <0x20
56
+ рисуются псевдографикой (как на реальном РК-86).
57
+
58
+ ### Пример 1. Дамп памяти через команду монитора `D`
59
+
60
+ Монитор `mon32` печатает дамп памяти с адреса 0 до FFh, через 10 секунд
61
+ эмулятор завершается по таймауту и содержимое экрана сохраняется в файл:
62
+
63
+ ```bash
64
+ bunx rk86 --headless \
65
+ --input "KeyD,Digit0,Comma,KeyF,KeyF,Enter" \
66
+ --timeout 10 \
67
+ --screen out.txt
68
+ ```
69
+
70
+ После выхода `out.txt` содержит приглашение `-->D0,FF` и 16 строк шестнадцатеричного
71
+ дампа `0000..00F0`.
72
+
73
+ ### Пример 2. Запись HLT и запуск через команды `M` / `G`
74
+
75
+ Командой `M` мы помещаем байт `76h` (опкод HLT) в ячейку `0000`, выходим из режима
76
+ редактирования (`.`), затем командой `G 0` запускаем с адреса 0. На первой же
77
+ инструкции срабатывает HLT и `--exit-halt` завершает эмулятор:
78
+
79
+ ```bash
80
+ bunx rk86 --headless \
81
+ --exit-halt \
82
+ --input "KeyM,Enter,Digit7,Digit6,Enter,Period,KeyG,Digit0,Enter" \
83
+ --timeout 15 \
84
+ --screen out.txt \
85
+ --memory mem.bin --memory-from 0x0000 --memory-to 0x0000
86
+ ```
87
+
88
+ После выхода `mem.bin` содержит один байт `76h`, а `out.txt` — скриншот с
89
+ приглашениями `-->M` и `-->G0`.
90
+
33
91
  ## Требования
34
92
 
35
93
  - [Bun](https://bun.sh) runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rk86",
3
- "version": "2.0.16",
3
+ "version": "2.0.17",
4
4
  "description": "Эмулятор Радио-86РК (Intel 8080) для терминала",
5
5
  "bin": {
6
6
  "rk86": "rk86.js"
package/rk86.js CHANGED
@@ -2273,11 +2273,11 @@ if (false) {}
2273
2273
 
2274
2274
  // src/lib/terminal/rk86_terminal.ts
2275
2275
  import { existsSync } from "fs";
2276
- import { readFile } from "fs/promises";
2276
+ import { readFile, writeFile } from "fs/promises";
2277
2277
  // packages/rk86/package.json
2278
2278
  var package_default = {
2279
2279
  name: "rk86",
2280
- version: "2.0.15",
2280
+ version: "2.0.16",
2281
2281
  description: "\u042D\u043C\u0443\u043B\u044F\u0442\u043E\u0440 \u0420\u0430\u0434\u0438\u043E-86\u0420\u041A (Intel 8080) \u0434\u043B\u044F \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0430",
2282
2282
  bin: {
2283
2283
  rk86: "rk86.js"
@@ -4220,6 +4220,11 @@ class TerminalRenderer {
4220
4220
  process.stdout.write(output);
4221
4221
  }
4222
4222
  }
4223
+
4224
+ class HeadlessRenderer {
4225
+ connect(_machine) {}
4226
+ update() {}
4227
+ }
4223
4228
  var KEY_MAP = {
4224
4229
  a: "KeyA",
4225
4230
  b: "KeyB",
@@ -4341,6 +4346,13 @@ function printHelp() {
4341
4346
  -g <\u0430\u0434\u0440\u0435\u0441> \u0430\u0434\u0440\u0435\u0441 \u0437\u0430\u043F\u0443\u0441\u043A\u0430 (\u043D\u0435\u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C \u0441 -p)
4342
4347
  --exit-halt \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0438 HLT
4343
4348
  --exit-address [\u0430\u0434\u0440\u0435\u0441] \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0435 \u043D\u0430 \u0430\u0434\u0440\u0435\u0441 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0xFFFE)
4349
+ --headless \u0431\u0435\u0437 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u044D\u043A\u0440\u0430\u043D\u0430 (\u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0442\u0435\u0441\u0442\u043E\u0432)
4350
+ --timeout <\u0441\u0435\u043A> \u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443
4351
+ --memory <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u0432 \u0444\u0430\u0439\u043B \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4352
+ --memory-from <\u0430\u0434\u0440\u0435\u0441> \u043D\u0430\u0447\u0430\u043B\u043E \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0434\u0430\u043C\u043F\u0430 \u043F\u0430\u043C\u044F\u0442\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0x0000)
4353
+ --memory-to <\u0430\u0434\u0440\u0435\u0441> \u043A\u043E\u043D\u0435\u0446 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0434\u0430\u043C\u043F\u0430 \u043F\u0430\u043C\u044F\u0442\u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435\u043B\u044C\u043D\u043E (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0xFFFF)
4354
+ --screen <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u044D\u043A\u0440\u0430\u043D 78x30 \u043A\u0430\u043A \u0442\u0435\u043A\u0441\u0442 \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4355
+ --input <seq> \u0438\u043D\u044A\u0435\u043A\u0446\u0438\u044F \u043A\u043B\u0430\u0432\u0438\u0448 (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u044F\u0442\u0443\u044E): KeyA,Digit1,Enter,...
4344
4356
 
4345
4357
  \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
4346
4358
  bunx rk86 \u0437\u0430\u043F\u0443\u0441\u043A \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0430
@@ -4411,6 +4423,15 @@ async function main() {
4411
4423
  const exitAddrValue = arg(args, "--exit-address", "0xFFFE", /^0x[0-9a-fA-F]+$/i, (v) => parseInt(v, 16));
4412
4424
  const exitAddr = exitAddrValue !== undefined;
4413
4425
  const monitorFile_ = arg(args, "-m");
4426
+ const headless = flag(args, "--headless");
4427
+ const timeoutSec = arg(args, "--timeout", undefined, /^\d+(\.\d+)?$/, parseFloat);
4428
+ const memoryFile = arg(args, "--memory");
4429
+ const addrRe = /^(0x)?[0-9a-fA-F]+$/i;
4430
+ const parseAddr = (v) => parseInt(v.toLowerCase().startsWith("0x") ? v.slice(2) : v, 16) & 65535;
4431
+ const memoryFrom = arg(args, "--memory-from", undefined, addrRe, parseAddr) ?? 0;
4432
+ const memoryTo = arg(args, "--memory-to", undefined, addrRe, parseAddr) ?? 65535;
4433
+ const screenFile = arg(args, "--screen");
4434
+ const inputSeq = arg(args, "--input");
4414
4435
  const programFile = args[0];
4415
4436
  const keyboard = new Keyboard;
4416
4437
  const io = new IO;
@@ -4472,19 +4493,36 @@ async function main() {
4472
4493
  if (goAddr !== undefined)
4473
4494
  entryPoint = goAddr;
4474
4495
  }
4475
- process.stdout.write("\x1B[?25l");
4476
- process.stdout.write("\x1B[2J");
4477
- setupKeyboard(keyboard);
4478
- const renderer = new TerminalRenderer;
4479
- renderer.loadInfo = loadInfo;
4496
+ if (!headless) {
4497
+ process.stdout.write("\x1B[?25l");
4498
+ process.stdout.write("\x1B[2J");
4499
+ setupKeyboard(keyboard);
4500
+ } else {
4501
+ process.on("SIGINT", () => doExit(null));
4502
+ }
4503
+ const renderer = headless ? new HeadlessRenderer : Object.assign(new TerminalRenderer, { loadInfo });
4480
4504
  machine.screen.start(renderer);
4481
- const onTerminate = exitOnHalt || exitAddr ? () => {
4482
- renderer.update();
4483
- setTimeout(() => {
4505
+ let exiting = false;
4506
+ const doExit = async (message) => {
4507
+ if (exiting)
4508
+ return;
4509
+ exiting = true;
4510
+ if (screenFile)
4511
+ await writeFile(screenFile, dumpScreen(machine));
4512
+ if (memoryFile)
4513
+ await writeFile(memoryFile, new Uint8Array(machine.memory.buf.slice(memoryFrom, memoryTo + 1)));
4514
+ if (!headless)
4515
+ process.stdout.write("\x1B[?25h");
4516
+ if (message !== null && !headless) {
4484
4517
  console.log();
4485
- console.log("\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430", hex16(machine.cpu.pc));
4486
- process.exit(0);
4487
- }, 1000);
4518
+ console.log(message);
4519
+ }
4520
+ process.exit(0);
4521
+ };
4522
+ const onTerminate = exitOnHalt || exitAddr ? () => {
4523
+ if (!headless)
4524
+ renderer.update();
4525
+ setTimeout(() => doExit(`\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430 ${hex16(machine.cpu.pc)}`), headless ? 0 : 1000);
4488
4526
  } : undefined;
4489
4527
  const armed = { value: entryPoint === undefined };
4490
4528
  machine.runner.execute({
@@ -4493,14 +4531,54 @@ async function main() {
4493
4531
  on_terminate: onTerminate,
4494
4532
  armed
4495
4533
  });
4534
+ const armDelayMs = 500;
4496
4535
  if (entryPoint !== undefined && !loadOnly) {
4497
4536
  setTimeout(() => {
4498
4537
  machine.cpu.jump(entryPoint);
4499
4538
  armed.value = true;
4500
- }, 500);
4539
+ }, armDelayMs);
4501
4540
  }
4502
- process.on("exit", () => {
4503
- process.stdout.write("\x1B[?25h");
4504
- });
4541
+ if (inputSeq) {
4542
+ const keys = inputSeq.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4543
+ const settleMs = armDelayMs + 1000;
4544
+ const keyDownMs = 50;
4545
+ const keyGapMs = 50;
4546
+ setTimeout(() => {
4547
+ const pressNext = (i) => {
4548
+ if (i >= keys.length)
4549
+ return;
4550
+ const code = keys[i];
4551
+ keyboard.onkeydown(code);
4552
+ setTimeout(() => {
4553
+ keyboard.onkeyup(code);
4554
+ setTimeout(() => pressNext(i + 1), keyGapMs);
4555
+ }, keyDownMs);
4556
+ };
4557
+ pressNext(0);
4558
+ }, settleMs);
4559
+ }
4560
+ if (timeoutSec !== undefined) {
4561
+ setTimeout(() => doExit(`\u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443 ${timeoutSec}\u0441`), timeoutSec * 1000);
4562
+ }
4563
+ }
4564
+ function dumpScreen(machine) {
4565
+ const { memory, screen } = machine;
4566
+ const lines = [];
4567
+ let addr = screen.video_memory_base;
4568
+ for (let y = 0;y < screen.height; y++) {
4569
+ let line = "";
4570
+ for (let x = 0;x < screen.width; x++) {
4571
+ const byte = memory.read_raw(addr++) & 127;
4572
+ if (byte === 0 || byte === 9 || byte === 10 || byte === 13) {
4573
+ line += ".";
4574
+ } else {
4575
+ line += rk86char(byte);
4576
+ }
4577
+ }
4578
+ lines.push(line);
4579
+ }
4580
+ return lines.join(`\r
4581
+ `) + `\r
4582
+ `;
4505
4583
  }
4506
4584
  main();