web-a2e 1.0.0

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 (295) hide show
  1. package/.clangd +5 -0
  2. package/.mcp.json +12 -0
  3. package/CLAUDE.md +362 -0
  4. package/CMakeLists.txt +774 -0
  5. package/LICENSE +21 -0
  6. package/README.md +392 -0
  7. package/build-wasm/generated/roms.cpp +2447 -0
  8. package/docker-compose.staging.yml +9 -0
  9. package/docs/basic-rom-disassembly.md +6663 -0
  10. package/docs/softswitch-comparison.md +273 -0
  11. package/docs/thunderclock-debug.md +89 -0
  12. package/examples/cube.bas +72 -0
  13. package/examples/hello.s +55 -0
  14. package/examples/scroll.s +140 -0
  15. package/package.json +18 -0
  16. package/public/assets/apple-logo-old.png +0 -0
  17. package/public/assets/apple-logo.png +0 -0
  18. package/public/assets/drive-closed-light-on.png +0 -0
  19. package/public/assets/drive-closed.png +0 -0
  20. package/public/assets/drive-open-light-on.png +0 -0
  21. package/public/assets/drive-open.png +0 -0
  22. package/public/audio-worklet.js +82 -0
  23. package/public/disks/Apple DOS 3.3 January 1983.dsk +0 -0
  24. package/public/disks/ProDOS 2.4.3.po +0 -0
  25. package/public/disks/h32mb.2mg +0 -0
  26. package/public/disks/library.json +26 -0
  27. package/public/docs/llms/llm-assembler.txt +90 -0
  28. package/public/docs/llms/llm-basic-program.txt +256 -0
  29. package/public/docs/llms/llm-disk-drives.txt +72 -0
  30. package/public/docs/llms/llm-file-explorer.txt +50 -0
  31. package/public/docs/llms/llm-hard-drives.txt +80 -0
  32. package/public/docs/llms/llm-main.txt +51 -0
  33. package/public/docs/llms/llm-slot-configuration.txt +66 -0
  34. package/public/icons/icon-192.svg +4 -0
  35. package/public/icons/icon-512.svg +4 -0
  36. package/public/index.html +661 -0
  37. package/public/llms.txt +49 -0
  38. package/public/manifest.json +29 -0
  39. package/public/shaders/burnin.glsl +22 -0
  40. package/public/shaders/crt.glsl +706 -0
  41. package/public/shaders/edge.glsl +109 -0
  42. package/public/shaders/vertex.glsl +8 -0
  43. package/public/sw.js +186 -0
  44. package/roms/341-0027.bin +0 -0
  45. package/roms/341-0160-A-US-UK.bin +0 -0
  46. package/roms/341-0160-A.bin +0 -0
  47. package/roms/342-0273-A-US-UK.bin +0 -0
  48. package/roms/342-0349-B-C0-FF.bin +0 -0
  49. package/roms/Apple Mouse Interface Card ROM - 342-0270-C.bin +0 -0
  50. package/roms/Thunderclock Plus ROM.bin +0 -0
  51. package/scripts/generate_roms.sh +69 -0
  52. package/src/bindings/wasm_interface.cpp +1940 -0
  53. package/src/core/assembler/assembler.cpp +1239 -0
  54. package/src/core/assembler/assembler.hpp +115 -0
  55. package/src/core/audio/audio.cpp +160 -0
  56. package/src/core/audio/audio.hpp +81 -0
  57. package/src/core/basic/basic_detokenizer.cpp +436 -0
  58. package/src/core/basic/basic_detokenizer.hpp +41 -0
  59. package/src/core/basic/basic_tokenizer.cpp +286 -0
  60. package/src/core/basic/basic_tokenizer.hpp +26 -0
  61. package/src/core/basic/basic_tokens.hpp +295 -0
  62. package/src/core/cards/disk2_card.cpp +568 -0
  63. package/src/core/cards/disk2_card.hpp +316 -0
  64. package/src/core/cards/expansion_card.hpp +185 -0
  65. package/src/core/cards/mockingboard/ay8910.cpp +616 -0
  66. package/src/core/cards/mockingboard/ay8910.hpp +159 -0
  67. package/src/core/cards/mockingboard/via6522.cpp +530 -0
  68. package/src/core/cards/mockingboard/via6522.hpp +163 -0
  69. package/src/core/cards/mockingboard_card.cpp +312 -0
  70. package/src/core/cards/mockingboard_card.hpp +159 -0
  71. package/src/core/cards/mouse_card.cpp +654 -0
  72. package/src/core/cards/mouse_card.hpp +190 -0
  73. package/src/core/cards/smartport/block_device.cpp +202 -0
  74. package/src/core/cards/smartport/block_device.hpp +60 -0
  75. package/src/core/cards/smartport/smartport_card.cpp +603 -0
  76. package/src/core/cards/smartport/smartport_card.hpp +120 -0
  77. package/src/core/cards/thunderclock_card.cpp +237 -0
  78. package/src/core/cards/thunderclock_card.hpp +122 -0
  79. package/src/core/cpu/cpu6502.cpp +1609 -0
  80. package/src/core/cpu/cpu6502.hpp +203 -0
  81. package/src/core/debug/condition_evaluator.cpp +470 -0
  82. package/src/core/debug/condition_evaluator.hpp +87 -0
  83. package/src/core/disassembler/disassembler.cpp +552 -0
  84. package/src/core/disassembler/disassembler.hpp +171 -0
  85. package/src/core/disk-image/disk_image.hpp +267 -0
  86. package/src/core/disk-image/dsk_disk_image.cpp +827 -0
  87. package/src/core/disk-image/dsk_disk_image.hpp +204 -0
  88. package/src/core/disk-image/gcr_encoding.cpp +147 -0
  89. package/src/core/disk-image/gcr_encoding.hpp +78 -0
  90. package/src/core/disk-image/woz_disk_image.cpp +1049 -0
  91. package/src/core/disk-image/woz_disk_image.hpp +343 -0
  92. package/src/core/emulator.cpp +2126 -0
  93. package/src/core/emulator.hpp +434 -0
  94. package/src/core/filesystem/dos33.cpp +178 -0
  95. package/src/core/filesystem/dos33.hpp +66 -0
  96. package/src/core/filesystem/pascal.cpp +262 -0
  97. package/src/core/filesystem/pascal.hpp +87 -0
  98. package/src/core/filesystem/prodos.cpp +369 -0
  99. package/src/core/filesystem/prodos.hpp +119 -0
  100. package/src/core/input/keyboard.cpp +227 -0
  101. package/src/core/input/keyboard.hpp +111 -0
  102. package/src/core/mmu/mmu.cpp +1387 -0
  103. package/src/core/mmu/mmu.hpp +236 -0
  104. package/src/core/types.hpp +196 -0
  105. package/src/core/video/video.cpp +680 -0
  106. package/src/core/video/video.hpp +156 -0
  107. package/src/css/assembler-editor.css +1617 -0
  108. package/src/css/base.css +470 -0
  109. package/src/css/basic-debugger.css +791 -0
  110. package/src/css/basic-editor.css +792 -0
  111. package/src/css/controls.css +783 -0
  112. package/src/css/cpu-debugger.css +1413 -0
  113. package/src/css/debug-base.css +160 -0
  114. package/src/css/debug-windows.css +6455 -0
  115. package/src/css/disk-drives.css +406 -0
  116. package/src/css/documentation.css +392 -0
  117. package/src/css/file-explorer.css +867 -0
  118. package/src/css/hard-drive.css +180 -0
  119. package/src/css/layout.css +217 -0
  120. package/src/css/memory-windows.css +798 -0
  121. package/src/css/modals.css +510 -0
  122. package/src/css/monitor.css +425 -0
  123. package/src/css/release-notes.css +101 -0
  124. package/src/css/responsive.css +400 -0
  125. package/src/css/rule-builder.css +340 -0
  126. package/src/css/save-states.css +201 -0
  127. package/src/css/settings-windows.css +1231 -0
  128. package/src/css/window-switcher.css +150 -0
  129. package/src/js/agent/agent-manager.js +643 -0
  130. package/src/js/agent/agent-tools.js +293 -0
  131. package/src/js/agent/agent-version-tools.js +131 -0
  132. package/src/js/agent/assembler-tools.js +357 -0
  133. package/src/js/agent/basic-program-tools.js +894 -0
  134. package/src/js/agent/disk-tools.js +417 -0
  135. package/src/js/agent/file-explorer-tools.js +269 -0
  136. package/src/js/agent/index.js +13 -0
  137. package/src/js/agent/main-tools.js +222 -0
  138. package/src/js/agent/slot-tools.js +303 -0
  139. package/src/js/agent/smartport-tools.js +257 -0
  140. package/src/js/agent/window-tools.js +80 -0
  141. package/src/js/audio/audio-driver.js +417 -0
  142. package/src/js/audio/audio-worklet.js +85 -0
  143. package/src/js/audio/index.js +8 -0
  144. package/src/js/config/default-layout.js +34 -0
  145. package/src/js/config/version.js +8 -0
  146. package/src/js/data/apple2-rom-routines.js +577 -0
  147. package/src/js/debug/assembler-editor-window.js +2993 -0
  148. package/src/js/debug/basic-breakpoint-manager.js +529 -0
  149. package/src/js/debug/basic-program-parser.js +436 -0
  150. package/src/js/debug/basic-program-window.js +2594 -0
  151. package/src/js/debug/basic-variable-inspector.js +447 -0
  152. package/src/js/debug/breakpoint-manager.js +472 -0
  153. package/src/js/debug/cpu-debugger-window.js +2396 -0
  154. package/src/js/debug/index.js +22 -0
  155. package/src/js/debug/label-manager.js +238 -0
  156. package/src/js/debug/memory-browser-window.js +416 -0
  157. package/src/js/debug/memory-heat-map-window.js +481 -0
  158. package/src/js/debug/memory-map-window.js +206 -0
  159. package/src/js/debug/mockingboard-window.js +882 -0
  160. package/src/js/debug/mouse-card-window.js +355 -0
  161. package/src/js/debug/rule-builder-window.js +648 -0
  162. package/src/js/debug/soft-switch-window.js +458 -0
  163. package/src/js/debug/stack-viewer-window.js +221 -0
  164. package/src/js/debug/symbols.js +416 -0
  165. package/src/js/debug/trace-panel.js +291 -0
  166. package/src/js/debug/zero-page-watch-window.js +297 -0
  167. package/src/js/disk-manager/disk-drives-window.js +212 -0
  168. package/src/js/disk-manager/disk-operations.js +284 -0
  169. package/src/js/disk-manager/disk-persistence.js +301 -0
  170. package/src/js/disk-manager/disk-surface-renderer.js +388 -0
  171. package/src/js/disk-manager/drive-sounds.js +139 -0
  172. package/src/js/disk-manager/hard-drive-manager.js +481 -0
  173. package/src/js/disk-manager/hard-drive-persistence.js +187 -0
  174. package/src/js/disk-manager/hard-drive-window.js +57 -0
  175. package/src/js/disk-manager/index.js +890 -0
  176. package/src/js/display/display-settings-window.js +383 -0
  177. package/src/js/display/index.js +10 -0
  178. package/src/js/display/screen-window.js +342 -0
  179. package/src/js/display/webgl-renderer.js +705 -0
  180. package/src/js/file-explorer/disassembler.js +574 -0
  181. package/src/js/file-explorer/dos33.js +266 -0
  182. package/src/js/file-explorer/file-viewer.js +359 -0
  183. package/src/js/file-explorer/index.js +1261 -0
  184. package/src/js/file-explorer/prodos.js +549 -0
  185. package/src/js/file-explorer/utils.js +67 -0
  186. package/src/js/help/documentation-window.js +1096 -0
  187. package/src/js/help/index.js +10 -0
  188. package/src/js/help/release-notes-window.js +85 -0
  189. package/src/js/help/release-notes.js +612 -0
  190. package/src/js/input/gamepad-handler.js +176 -0
  191. package/src/js/input/index.js +12 -0
  192. package/src/js/input/input-handler.js +396 -0
  193. package/src/js/input/joystick-window.js +404 -0
  194. package/src/js/input/mouse-handler.js +99 -0
  195. package/src/js/input/text-selection.js +462 -0
  196. package/src/js/main.js +653 -0
  197. package/src/js/state/index.js +15 -0
  198. package/src/js/state/save-states-window.js +393 -0
  199. package/src/js/state/state-manager.js +409 -0
  200. package/src/js/state/state-persistence.js +218 -0
  201. package/src/js/ui/confirm.js +43 -0
  202. package/src/js/ui/disk-drive-positioner.js +347 -0
  203. package/src/js/ui/reminder-controller.js +129 -0
  204. package/src/js/ui/slot-configuration-window.js +560 -0
  205. package/src/js/ui/theme-manager.js +61 -0
  206. package/src/js/ui/toast.js +44 -0
  207. package/src/js/ui/ui-controller.js +897 -0
  208. package/src/js/ui/window-switcher.js +275 -0
  209. package/src/js/utils/basic-autocomplete.js +832 -0
  210. package/src/js/utils/basic-highlighting.js +473 -0
  211. package/src/js/utils/basic-tokenizer.js +153 -0
  212. package/src/js/utils/basic-tokens.js +117 -0
  213. package/src/js/utils/constants.js +28 -0
  214. package/src/js/utils/indexeddb-helper.js +225 -0
  215. package/src/js/utils/merlin-editor-support.js +905 -0
  216. package/src/js/utils/merlin-highlighting.js +551 -0
  217. package/src/js/utils/storage.js +125 -0
  218. package/src/js/utils/string-utils.js +19 -0
  219. package/src/js/utils/wasm-memory.js +54 -0
  220. package/src/js/windows/base-window.js +690 -0
  221. package/src/js/windows/index.js +9 -0
  222. package/src/js/windows/window-manager.js +375 -0
  223. package/tests/catch2/catch.hpp +17976 -0
  224. package/tests/common/basic_program_builder.cpp +119 -0
  225. package/tests/common/basic_program_builder.hpp +209 -0
  226. package/tests/common/disk_image_builder.cpp +444 -0
  227. package/tests/common/disk_image_builder.hpp +141 -0
  228. package/tests/common/test_helpers.hpp +118 -0
  229. package/tests/gcr/gcr-test.cpp +142 -0
  230. package/tests/integration/check-rom.js +70 -0
  231. package/tests/integration/compare-boot.js +239 -0
  232. package/tests/integration/crash-trace.js +102 -0
  233. package/tests/integration/disk-boot-test.js +264 -0
  234. package/tests/integration/memory-crash.js +108 -0
  235. package/tests/integration/nibble-read-test.js +249 -0
  236. package/tests/integration/phase-test.js +159 -0
  237. package/tests/integration/test_emulator.cpp +291 -0
  238. package/tests/integration/test_emulator_basic.cpp +91 -0
  239. package/tests/integration/test_emulator_debug.cpp +344 -0
  240. package/tests/integration/test_emulator_disk.cpp +153 -0
  241. package/tests/integration/test_emulator_state.cpp +163 -0
  242. package/tests/klaus/6502_functional_test.bin +0 -0
  243. package/tests/klaus/65C02_extended_opcodes_test.bin +0 -0
  244. package/tests/klaus/klaus_6502_test.cpp +184 -0
  245. package/tests/klaus/klaus_65c02_test.cpp +197 -0
  246. package/tests/thunderclock/thunderclock_mmu_test.cpp +304 -0
  247. package/tests/thunderclock/thunderclock_test.cpp +550 -0
  248. package/tests/unit/test_assembler.cpp +521 -0
  249. package/tests/unit/test_audio.cpp +196 -0
  250. package/tests/unit/test_ay8910.cpp +311 -0
  251. package/tests/unit/test_basic_detokenizer.cpp +265 -0
  252. package/tests/unit/test_basic_tokenizer.cpp +382 -0
  253. package/tests/unit/test_block_device.cpp +259 -0
  254. package/tests/unit/test_condition_evaluator.cpp +219 -0
  255. package/tests/unit/test_cpu6502.cpp +1301 -0
  256. package/tests/unit/test_cpu_addressing.cpp +361 -0
  257. package/tests/unit/test_cpu_cycle_counts.cpp +409 -0
  258. package/tests/unit/test_cpu_decimal.cpp +166 -0
  259. package/tests/unit/test_cpu_interrupts.cpp +285 -0
  260. package/tests/unit/test_disassembler.cpp +323 -0
  261. package/tests/unit/test_disk2_card.cpp +330 -0
  262. package/tests/unit/test_dos33.cpp +273 -0
  263. package/tests/unit/test_dsk_disk_image.cpp +315 -0
  264. package/tests/unit/test_expansion_card.cpp +178 -0
  265. package/tests/unit/test_gcr_encoding.cpp +232 -0
  266. package/tests/unit/test_keyboard.cpp +262 -0
  267. package/tests/unit/test_mmu.cpp +555 -0
  268. package/tests/unit/test_mmu_slots.cpp +323 -0
  269. package/tests/unit/test_mockingboard.cpp +352 -0
  270. package/tests/unit/test_mouse_card.cpp +386 -0
  271. package/tests/unit/test_pascal.cpp +248 -0
  272. package/tests/unit/test_prodos.cpp +259 -0
  273. package/tests/unit/test_smartport_card.cpp +321 -0
  274. package/tests/unit/test_thunderclock.cpp +354 -0
  275. package/tests/unit/test_via6522.cpp +323 -0
  276. package/tests/unit/test_video.cpp +319 -0
  277. package/tests/unit/test_woz_disk_image.cpp +257 -0
  278. package/vite.config.js +96 -0
  279. package/wiki/AI-Agent.md +372 -0
  280. package/wiki/Architecture-Overview.md +303 -0
  281. package/wiki/Audio-System.md +449 -0
  282. package/wiki/CPU-Emulation.md +477 -0
  283. package/wiki/Debugger.md +516 -0
  284. package/wiki/Disk-Drives.md +161 -0
  285. package/wiki/Disk-System-Internals.md +547 -0
  286. package/wiki/Display-Settings.md +88 -0
  287. package/wiki/Expansion-Slots.md +187 -0
  288. package/wiki/File-Explorer.md +259 -0
  289. package/wiki/Getting-Started.md +156 -0
  290. package/wiki/Home.md +69 -0
  291. package/wiki/Input-Devices.md +183 -0
  292. package/wiki/Keyboard-Shortcuts.md +158 -0
  293. package/wiki/Memory-System.md +364 -0
  294. package/wiki/Save-States.md +172 -0
  295. package/wiki/Video-Rendering.md +658 -0
@@ -0,0 +1,550 @@
1
+ /**
2
+ * Thunderclock Plus Card Tests
3
+ *
4
+ * Tests the Thunderclock Plus clock card implementation to verify:
5
+ * 1. ROM signature bytes for ProDOS detection
6
+ * 2. I/O register behavior
7
+ * 3. Time data serialization
8
+ * 4. Clock/strobe state machine
9
+ *
10
+ * ProDOS scans slots looking for specific ROM signature bytes:
11
+ * - $Cn00: $08 (PHP instruction)
12
+ * - $Cn02: $28 (signature byte)
13
+ * - $Cn04: $58 (signature byte)
14
+ * - $Cn06: $70 (signature byte)
15
+ */
16
+
17
+ #include <iostream>
18
+ #include <iomanip>
19
+ #include <cstdint>
20
+ #include <vector>
21
+ #include <string>
22
+ #include <sstream>
23
+ #include <ctime>
24
+ #include <stdexcept>
25
+
26
+ // Include the Thunderclock card implementation
27
+ #include "cards/thunderclock_card.hpp"
28
+
29
+ // Test result tracking
30
+ static int testsRun = 0;
31
+ static int testsPassed = 0;
32
+ static int testsFailed = 0;
33
+
34
+ #define TEST(name) \
35
+ void test_##name(); \
36
+ struct TestRunner_##name { \
37
+ TestRunner_##name() { \
38
+ std::cout << "Running: " << #name << "... "; \
39
+ testsRun++; \
40
+ try { \
41
+ test_##name(); \
42
+ testsPassed++; \
43
+ std::cout << "\033[32mPASSED\033[0m\n"; \
44
+ } catch (const std::exception& e) { \
45
+ testsFailed++; \
46
+ std::cout << "\033[31mFAILED: " << e.what() << "\033[0m\n"; \
47
+ } \
48
+ } \
49
+ } testRunner_##name; \
50
+ void test_##name()
51
+
52
+ #define ASSERT_EQ(expected, actual) \
53
+ if ((expected) != (actual)) { \
54
+ throw std::runtime_error( \
55
+ "Expected " + std::to_string(expected) + \
56
+ " but got " + std::to_string(actual)); \
57
+ }
58
+
59
+ #define ASSERT_EQ_HEX(expected, actual) \
60
+ if ((expected) != (actual)) { \
61
+ std::stringstream ss; \
62
+ ss << "Expected $" << std::hex << std::uppercase << (int)(expected) \
63
+ << " but got $" << (int)(actual); \
64
+ throw std::runtime_error(ss.str()); \
65
+ }
66
+
67
+ #define ASSERT_TRUE(condition) \
68
+ if (!(condition)) { \
69
+ throw std::runtime_error("Assertion failed: " #condition); \
70
+ }
71
+
72
+ // Control register flags
73
+ static constexpr uint8_t FLAG_CLOCK = 0x02;
74
+ static constexpr uint8_t FLAG_STROBE = 0x04;
75
+ static constexpr uint8_t CMD_REGHOLD = 0x00;
76
+ static constexpr uint8_t CMD_REGSHIFT = 0x20;
77
+ static constexpr uint8_t CMD_TIMED = 0xA0;
78
+
79
+ // ============================================================================
80
+ // ROM Signature Tests
81
+ // ============================================================================
82
+
83
+ TEST(rom_signature_prodos_detection) {
84
+ // ProDOS looks for these bytes at specific offsets to detect a clock card
85
+ a2e::ThunderclockCard card;
86
+
87
+ // $Cn00: $08 (PHP instruction)
88
+ ASSERT_EQ_HEX(0x08, card.readROM(0x00));
89
+
90
+ // $Cn02: $28 (PLP instruction - signature byte)
91
+ ASSERT_EQ_HEX(0x28, card.readROM(0x02));
92
+
93
+ // $Cn04: $58 (CLI instruction - signature byte)
94
+ ASSERT_EQ_HEX(0x58, card.readROM(0x04));
95
+
96
+ // $Cn06: $70 (BVS instruction - signature byte)
97
+ ASSERT_EQ_HEX(0x70, card.readROM(0x06));
98
+ }
99
+
100
+ TEST(rom_first_instruction_sequence) {
101
+ // Verify the first few bytes form valid 6502 code
102
+ a2e::ThunderclockCard card;
103
+
104
+ // Dump first 16 bytes for inspection
105
+ std::cout << "\n ROM bytes: ";
106
+ for (int i = 0; i < 16; i++) {
107
+ std::cout << std::hex << std::uppercase << std::setw(2) << std::setfill('0')
108
+ << (int)card.readROM(i) << " ";
109
+ }
110
+ std::cout << std::dec << "\n ";
111
+
112
+ // The ROM should start with PHP ($08) to save processor status
113
+ ASSERT_EQ_HEX(0x08, card.readROM(0x00));
114
+ }
115
+
116
+ TEST(rom_size_and_expansion) {
117
+ a2e::ThunderclockCard card;
118
+
119
+ // Card should have ROM
120
+ ASSERT_TRUE(card.hasROM());
121
+
122
+ // Card should have expansion ROM
123
+ ASSERT_TRUE(card.hasExpansionROM());
124
+
125
+ // Read expansion ROM - should return valid data, not 0xFF
126
+ uint8_t expByte = card.readExpansionROM(0x00);
127
+ std::cout << "\n Expansion ROM[0] = $" << std::hex << (int)expByte << std::dec << "\n ";
128
+ }
129
+
130
+ // ============================================================================
131
+ // I/O Register Tests
132
+ // ============================================================================
133
+
134
+ TEST(io_read_initial_state) {
135
+ a2e::ThunderclockCard card;
136
+
137
+ // Initial register state should be 0
138
+ ASSERT_EQ_HEX(0x00, card.readIO(0x00));
139
+
140
+ // All I/O addresses should return the same register
141
+ for (int offset = 0; offset < 16; offset++) {
142
+ ASSERT_EQ_HEX(0x00, card.readIO(offset));
143
+ }
144
+ }
145
+
146
+ TEST(io_peek_matches_read) {
147
+ a2e::ThunderclockCard card;
148
+
149
+ // Peek should match read
150
+ ASSERT_EQ(card.readIO(0x00), card.peekIO(0x00));
151
+ }
152
+
153
+ TEST(io_write_does_not_directly_change_register) {
154
+ a2e::ThunderclockCard card;
155
+
156
+ // Writing to I/O should not directly change the read value
157
+ // (it controls the state machine, not the data register)
158
+ card.writeIO(0x00, 0x55);
159
+ // Register value depends on state machine, not direct write
160
+ }
161
+
162
+ // ============================================================================
163
+ // Time Data Command Tests
164
+ // ============================================================================
165
+
166
+ TEST(time_command_loads_data) {
167
+ a2e::ThunderclockCard card;
168
+
169
+ // Issue CMD_TIMED with strobe rising edge
170
+ card.writeIO(0x00, 0x00); // Ensure strobe is low
171
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE); // Strobe rising edge with time command
172
+
173
+ // Now clock out the first bit
174
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE); // Clock low
175
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK); // Clock rising edge
176
+
177
+ // Read should now have bit 7 set to the first data bit
178
+ uint8_t value = card.readIO(0x00);
179
+ std::cout << "\n After first clock: register = $" << std::hex << (int)value << std::dec << "\n ";
180
+ }
181
+
182
+ TEST(time_data_40_bits) {
183
+ a2e::ThunderclockCard card;
184
+
185
+ // Issue CMD_TIMED with strobe rising edge
186
+ card.writeIO(0x00, 0x00);
187
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
188
+
189
+ // Clock out all 40 bits
190
+ std::vector<int> bits;
191
+ for (int i = 0; i < 40; i++) {
192
+ // Clock rising edge
193
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE); // Clock low
194
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK); // Clock high
195
+
196
+ uint8_t value = card.readIO(0x00);
197
+ bits.push_back((value & 0x80) ? 1 : 0);
198
+ }
199
+
200
+ // Print the bits
201
+ std::cout << "\n 40 bits: ";
202
+ for (int i = 0; i < 40; i++) {
203
+ std::cout << bits[i];
204
+ if ((i + 1) % 4 == 0) std::cout << " ";
205
+ }
206
+ std::cout << "\n ";
207
+
208
+ // Decode the bits
209
+ // Month (4 bits), weekday (4 bits), day (8 bits BCD), hour (8 bits BCD), min (8 bits BCD), sec (8 bits BCD)
210
+ int month = (bits[0] << 3) | (bits[1] << 2) | (bits[2] << 1) | bits[3];
211
+ int weekday = (bits[4] << 3) | (bits[5] << 2) | (bits[6] << 1) | bits[7];
212
+ int day_tens = (bits[8] << 3) | (bits[9] << 2) | (bits[10] << 1) | bits[11];
213
+ int day_ones = (bits[12] << 3) | (bits[13] << 2) | (bits[14] << 1) | bits[15];
214
+ int day = day_tens * 10 + day_ones;
215
+ int hour_tens = (bits[16] << 3) | (bits[17] << 2) | (bits[18] << 1) | bits[19];
216
+ int hour_ones = (bits[20] << 3) | (bits[21] << 2) | (bits[22] << 1) | bits[23];
217
+ int hour = hour_tens * 10 + hour_ones;
218
+ int min_tens = (bits[24] << 3) | (bits[25] << 2) | (bits[26] << 1) | bits[27];
219
+ int min_ones = (bits[28] << 3) | (bits[29] << 2) | (bits[30] << 1) | bits[31];
220
+ int minute = min_tens * 10 + min_ones;
221
+ int sec_tens = (bits[32] << 3) | (bits[33] << 2) | (bits[34] << 1) | bits[35];
222
+ int sec_ones = (bits[36] << 3) | (bits[37] << 2) | (bits[38] << 1) | bits[39];
223
+ int second = sec_tens * 10 + sec_ones;
224
+
225
+ std::cout << " Decoded: month=" << month << " weekday=" << weekday
226
+ << " day=" << day << " hour=" << hour << ":" << minute << ":" << second << "\n ";
227
+
228
+ // Get current time to compare
229
+ std::time_t now = std::time(nullptr);
230
+ std::tm* tm = std::localtime(&now);
231
+
232
+ // Month should match (0-11)
233
+ ASSERT_EQ(tm->tm_mon, month);
234
+
235
+ // Weekday should match (0-6)
236
+ ASSERT_EQ(tm->tm_wday, weekday);
237
+
238
+ // Day should be close (might change during test)
239
+ ASSERT_TRUE(day >= 1 && day <= 31);
240
+
241
+ // Hour should match
242
+ ASSERT_EQ(tm->tm_hour, hour);
243
+
244
+ // Minute should be close (might change during test)
245
+ ASSERT_TRUE(minute >= 0 && minute <= 59);
246
+
247
+ // Second should be valid
248
+ ASSERT_TRUE(second >= 0 && second <= 59);
249
+ }
250
+
251
+ // ============================================================================
252
+ // State Machine Tests
253
+ // ============================================================================
254
+
255
+ TEST(strobe_edge_detection) {
256
+ a2e::ThunderclockCard card;
257
+
258
+ // Strobe should only trigger on rising edge
259
+ card.writeIO(0x00, 0x00); // Low
260
+ card.writeIO(0x00, FLAG_STROBE); // Rising edge - should trigger
261
+ card.writeIO(0x00, FLAG_STROBE); // Still high - should NOT trigger again
262
+ card.writeIO(0x00, 0x00); // Falling edge
263
+ card.writeIO(0x00, FLAG_STROBE); // Rising edge again - should trigger
264
+ }
265
+
266
+ TEST(clock_edge_detection) {
267
+ a2e::ThunderclockCard card;
268
+
269
+ // Setup: issue time command
270
+ card.writeIO(0x00, 0x00);
271
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
272
+
273
+ // Clock should only shift on rising edge
274
+ uint8_t val1 = card.readIO(0x00);
275
+
276
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK); // Rising edge
277
+ uint8_t val2 = card.readIO(0x00);
278
+
279
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK); // Still high - no shift
280
+ uint8_t val3 = card.readIO(0x00);
281
+
282
+ // val2 and val3 should be the same (no shift on holding high)
283
+ ASSERT_EQ(val2, val3);
284
+ }
285
+
286
+ TEST(reset_clears_state) {
287
+ a2e::ThunderclockCard card;
288
+
289
+ // Setup some state
290
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
291
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
292
+
293
+ // Reset
294
+ card.reset();
295
+
296
+ // Register should be 0 after reset
297
+ ASSERT_EQ_HEX(0x00, card.readIO(0x00));
298
+ }
299
+
300
+ // ============================================================================
301
+ // Card Information Tests
302
+ // ============================================================================
303
+
304
+ TEST(card_name) {
305
+ a2e::ThunderclockCard card;
306
+ std::string name = card.getName();
307
+ ASSERT_TRUE(name == "Thunderclock");
308
+ }
309
+
310
+ TEST(preferred_slot) {
311
+ a2e::ThunderclockCard card;
312
+ // Thunderclock is typically in slot 5 or 7
313
+ uint8_t slot = card.getPreferredSlot();
314
+ ASSERT_TRUE(slot == 5 || slot == 7);
315
+ }
316
+
317
+ // ============================================================================
318
+ // ProDOS Driver Interaction Simulation
319
+ // ============================================================================
320
+
321
+ TEST(prodos_driver_read_sequence) {
322
+ // Simulate how ProDOS reads time from the Thunderclock
323
+ // The driver in ROM does:
324
+ // 1. Write CMD_TIMED with strobe rising edge to start reading
325
+ // 2. Clock out 32 bits (ProDOS only needs date/time, not seconds)
326
+ // 3. Convert to ProDOS date/time format
327
+
328
+ a2e::ThunderclockCard card;
329
+
330
+ // Step 1: Issue time read command
331
+ card.writeIO(0x00, 0x00); // Clear
332
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE); // Strobe rising with time cmd
333
+
334
+ // Step 2: Clock out 32 bits (ProDOS format)
335
+ uint32_t rawTime = 0;
336
+ for (int i = 0; i < 32; i++) {
337
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE); // Clock low
338
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK); // Clock rising
339
+
340
+ uint8_t value = card.readIO(0x00);
341
+ rawTime = (rawTime << 1) | ((value & 0x80) ? 1 : 0);
342
+ }
343
+
344
+ std::cout << "\n Raw 32-bit time: $" << std::hex << rawTime << std::dec << "\n ";
345
+
346
+ // Decode: first 8 bits = month(4) + weekday(4), next 8 = day BCD, next 8 = hour BCD, next 8 = min BCD
347
+ int month = (rawTime >> 28) & 0x0F;
348
+ int weekday = (rawTime >> 24) & 0x0F;
349
+ int day = ((rawTime >> 20) & 0x0F) * 10 + ((rawTime >> 16) & 0x0F);
350
+ int hour = ((rawTime >> 12) & 0x0F) * 10 + ((rawTime >> 8) & 0x0F);
351
+ int minute = ((rawTime >> 4) & 0x0F) * 10 + (rawTime & 0x0F);
352
+
353
+ std::cout << " ProDOS format: " << (month + 1) << "/" << day << " " << hour << ":" << minute << "\n ";
354
+
355
+ // Verify values are in valid ranges
356
+ ASSERT_TRUE(month >= 0 && month <= 11);
357
+ ASSERT_TRUE(day >= 1 && day <= 31);
358
+ ASSERT_TRUE(hour >= 0 && hour <= 23);
359
+ ASSERT_TRUE(minute >= 0 && minute <= 59);
360
+ }
361
+
362
+ // ============================================================================
363
+ // ROM Disassembly Analysis
364
+ // ============================================================================
365
+
366
+ TEST(rom_entry_point_analysis) {
367
+ a2e::ThunderclockCard card;
368
+
369
+ std::cout << "\n ROM Entry Point Disassembly:\n";
370
+
371
+ // Disassemble first 32 bytes
372
+ const char* mnemonics[] = {
373
+ "PHP", "SEI", "PLP", "BIT $FF58", "", "", "BVS +5", "",
374
+ "SEC", "BCS +1", "", "CLC", "CLV", "PHP", "SEI", "PHA"
375
+ };
376
+
377
+ for (int i = 0; i < 16; i++) {
378
+ uint8_t byte = card.readROM(i);
379
+ std::cout << " $" << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << i
380
+ << ": " << std::setw(2) << (int)byte << std::dec;
381
+ if (i < 16 && mnemonics[i][0]) {
382
+ std::cout << " ; " << mnemonics[i];
383
+ }
384
+ std::cout << "\n";
385
+ }
386
+ std::cout << " ";
387
+ }
388
+
389
+ TEST(expansion_rom_entry_points) {
390
+ a2e::ThunderclockCard card;
391
+
392
+ // The expansion ROM at $C800 should have valid code
393
+ std::cout << "\n Expansion ROM ($C800) first 16 bytes:\n ";
394
+ for (int i = 0; i < 16; i++) {
395
+ std::cout << std::hex << std::uppercase << std::setw(2) << std::setfill('0')
396
+ << (int)card.readExpansionROM(i) << " ";
397
+ }
398
+ std::cout << std::dec << "\n ";
399
+
400
+ // Check that expansion ROM isn't all zeros or 0xFF
401
+ bool allZero = true;
402
+ bool allFF = true;
403
+ for (int i = 0; i < 256; i++) {
404
+ uint8_t byte = card.readExpansionROM(i);
405
+ if (byte != 0x00) allZero = false;
406
+ if (byte != 0xFF) allFF = false;
407
+ }
408
+ ASSERT_TRUE(!allZero);
409
+ ASSERT_TRUE(!allFF);
410
+ }
411
+
412
+ // ============================================================================
413
+ // Verify ProDOS-specific behavior
414
+ // ============================================================================
415
+
416
+ TEST(prodos_reads_rom_signature_correctly) {
417
+ // ProDOS reads specific offsets to detect clock card
418
+ a2e::ThunderclockCard card;
419
+
420
+ // These must match exactly for ProDOS to recognize the card
421
+ std::cout << "\n ProDOS signature check:\n";
422
+ std::cout << " $Cn00 = $" << std::hex << (int)card.readROM(0x00) << " (expected $08)\n";
423
+ std::cout << " $Cn02 = $" << std::hex << (int)card.readROM(0x02) << " (expected $28)\n";
424
+ std::cout << " $Cn04 = $" << std::hex << (int)card.readROM(0x04) << " (expected $58)\n";
425
+ std::cout << " $Cn06 = $" << std::hex << (int)card.readROM(0x06) << " (expected $70)\n";
426
+ std::cout << std::dec << " ";
427
+
428
+ ASSERT_EQ_HEX(0x08, card.readROM(0x00));
429
+ ASSERT_EQ_HEX(0x28, card.readROM(0x02));
430
+ ASSERT_EQ_HEX(0x58, card.readROM(0x04));
431
+ ASSERT_EQ_HEX(0x70, card.readROM(0x06));
432
+ }
433
+
434
+ TEST(io_address_all_offsets_return_same_register) {
435
+ // The Thunderclock should return the same register from all I/O offsets
436
+ a2e::ThunderclockCard card;
437
+
438
+ // Set up some state
439
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
440
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
441
+
442
+ uint8_t expected = card.readIO(0x00);
443
+
444
+ // All 16 I/O addresses should return the same value
445
+ for (int offset = 0; offset < 16; offset++) {
446
+ ASSERT_EQ(expected, card.readIO(offset));
447
+ }
448
+ }
449
+
450
+ TEST(multiple_time_reads_without_reset) {
451
+ // Verify that multiple time read commands work correctly
452
+ a2e::ThunderclockCard card;
453
+
454
+ for (int iteration = 0; iteration < 3; iteration++) {
455
+ // Issue time read command
456
+ card.writeIO(0x00, 0x00);
457
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
458
+
459
+ // Read 8 bits
460
+ uint8_t byte = 0;
461
+ for (int bit = 0; bit < 8; bit++) {
462
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
463
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
464
+ uint8_t value = card.readIO(0x00);
465
+ byte = (byte << 1) | ((value & 0x80) ? 1 : 0);
466
+ }
467
+
468
+ std::cout << "\n Iteration " << iteration << ": first byte = $"
469
+ << std::hex << (int)byte << std::dec;
470
+ }
471
+ std::cout << "\n ";
472
+ }
473
+
474
+ TEST(read_before_any_clocking) {
475
+ // Test what happens when you read the register before any clocking
476
+ // This is important because ProDOS may read to check status
477
+ a2e::ThunderclockCard card;
478
+
479
+ // Read immediately after construction (after reset)
480
+ uint8_t value = card.readIO(0x00);
481
+ std::cout << "\n After construction (no commands): $" << std::hex << (int)value << std::dec << "\n";
482
+
483
+ // Issue strobe only (no clock yet)
484
+ card.writeIO(0x00, 0x00);
485
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
486
+ value = card.readIO(0x00);
487
+ std::cout << " After strobe (time command, no clock): $" << std::hex << (int)value << std::dec << "\n";
488
+
489
+ // Now clock one bit
490
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
491
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
492
+ value = card.readIO(0x00);
493
+ std::cout << " After first clock: $" << std::hex << (int)value << std::dec << "\n ";
494
+ }
495
+
496
+ TEST(verify_bcd_encoding) {
497
+ // Verify BCD encoding is correct for various values
498
+ a2e::ThunderclockCard card;
499
+
500
+ // Issue time read command
501
+ card.writeIO(0x00, 0x00);
502
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
503
+
504
+ // Skip first 8 bits (month + weekday)
505
+ for (int i = 0; i < 8; i++) {
506
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
507
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
508
+ }
509
+
510
+ // Read day (8 bits BCD)
511
+ uint8_t dayBCD = 0;
512
+ for (int i = 0; i < 8; i++) {
513
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE);
514
+ card.writeIO(0x00, CMD_TIMED | FLAG_STROBE | FLAG_CLOCK);
515
+ dayBCD = (dayBCD << 1) | ((card.readIO(0x00) & 0x80) ? 1 : 0);
516
+ }
517
+
518
+ int dayTens = (dayBCD >> 4) & 0x0F;
519
+ int dayOnes = dayBCD & 0x0F;
520
+ int day = dayTens * 10 + dayOnes;
521
+
522
+ std::cout << "\n Day BCD: $" << std::hex << (int)dayBCD << std::dec
523
+ << " = " << day << "\n ";
524
+
525
+ // Day should be valid (1-31)
526
+ ASSERT_TRUE(day >= 1 && day <= 31);
527
+ // BCD digits should be 0-9
528
+ ASSERT_TRUE(dayTens <= 3);
529
+ ASSERT_TRUE(dayOnes <= 9);
530
+ }
531
+
532
+ // ============================================================================
533
+ // Main
534
+ // ============================================================================
535
+
536
+ int main() {
537
+ std::cout << "Thunderclock Plus Card Tests\n";
538
+ std::cout << "============================\n\n";
539
+
540
+ // Tests run automatically via static initializers
541
+
542
+ std::cout << "\n============================\n";
543
+ std::cout << "Results: " << testsPassed << "/" << testsRun << " passed";
544
+ if (testsFailed > 0) {
545
+ std::cout << " (" << testsFailed << " failed)";
546
+ }
547
+ std::cout << "\n";
548
+
549
+ return testsFailed > 0 ? 1 : 0;
550
+ }