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,120 @@
1
+ /*
2
+ * smartport_card.hpp - SmartPort expansion card for ProDOS block devices
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #pragma once
9
+
10
+ #include "../expansion_card.hpp"
11
+ #include "block_device.hpp"
12
+ #include <cstdint>
13
+ #include <functional>
14
+ #include <array>
15
+
16
+ namespace a2e {
17
+
18
+ class SmartPortCard : public ExpansionCard {
19
+ public:
20
+ static constexpr int MAX_DEVICES = 2;
21
+
22
+ using MemReadCallback = std::function<uint8_t(uint16_t)>;
23
+ using MemWriteCallback = std::function<void(uint16_t, uint8_t)>;
24
+ using RegGetCallback8 = std::function<uint8_t()>;
25
+ using RegSetCallback8 = std::function<void(uint8_t)>;
26
+ using RegGetCallback16 = std::function<uint16_t()>;
27
+ using RegSetCallback16 = std::function<void(uint16_t)>;
28
+
29
+ SmartPortCard();
30
+ ~SmartPortCard() override = default;
31
+
32
+ // ExpansionCard interface
33
+ uint8_t readIO(uint8_t offset) override;
34
+ void writeIO(uint8_t offset, uint8_t value) override;
35
+ uint8_t peekIO(uint8_t offset) const override { return 0xFF; }
36
+
37
+ uint8_t readROM(uint8_t offset) override;
38
+ bool hasROM() const override { return hasAnyDevice(); }
39
+
40
+ void reset() override;
41
+ const char* getName() const override { return "SmartPort"; }
42
+ uint8_t getPreferredSlot() const override { return 7; }
43
+
44
+ // State serialization
45
+ size_t getStateSize() const override;
46
+ size_t serialize(uint8_t* buffer, size_t maxSize) const override;
47
+ size_t deserialize(const uint8_t* buffer, size_t size) override;
48
+
49
+ // Slot configuration
50
+ void setSlotNumber(uint8_t slot);
51
+ uint8_t getSlotNumber() const { return slotNum_; }
52
+
53
+ // Device management
54
+ bool insertImage(int device, const uint8_t* data, size_t size, const std::string& filename);
55
+ void ejectImage(int device);
56
+ bool isImageInserted(int device) const;
57
+ const std::string& getImageFilename(int device) const;
58
+ bool isImageModified(int device) const;
59
+ const uint8_t* exportImageData(int device, size_t* size) const;
60
+ const uint8_t* getBlockData(int device, size_t* size) const;
61
+ BlockDevice* getDevice(int device);
62
+ const BlockDevice* getDevice(int device) const;
63
+
64
+ // Callbacks for memory and CPU access
65
+ void setMemReadCallback(MemReadCallback cb) { memRead_ = cb; }
66
+ void setMemWriteCallback(MemWriteCallback cb) { memWrite_ = cb; }
67
+ void setGetA(RegGetCallback8 cb) { getA_ = cb; }
68
+ void setSetA(RegSetCallback8 cb) { setA_ = cb; }
69
+ void setGetP(RegGetCallback8 cb) { getP_ = cb; }
70
+ void setSetP(RegSetCallback8 cb) { setP_ = cb; }
71
+ void setGetSP(RegGetCallback8 cb) { getSP_ = cb; }
72
+ void setSetSP(RegSetCallback8 cb) { setSP_ = cb; }
73
+ void setGetPC(RegGetCallback16 cb) { getPC_ = cb; }
74
+ void setSetPC(RegSetCallback16 cb) { setPC_ = cb; }
75
+ void setSetX(RegSetCallback8 cb) { setX_ = cb; }
76
+ void setSetY(RegSetCallback8 cb) { setY_ = cb; }
77
+
78
+ // Activity tracking for UI
79
+ bool hasActivity() const { return activity_; }
80
+ bool isActivityWrite() const { return activityWrite_; }
81
+ void clearActivity() { activity_ = false; }
82
+
83
+ private:
84
+ void buildROM();
85
+ bool hasAnyDevice() const;
86
+ bool handleBoot();
87
+ void handleProDOSBlock();
88
+ void handleSmartPort();
89
+ void setErrorResult(uint8_t errorCode);
90
+
91
+ uint8_t slotNum_ = 7;
92
+ std::array<uint8_t, 256> rom_;
93
+ std::array<BlockDevice, MAX_DEVICES> devices_;
94
+
95
+ // Boot state: false until first boot completes, then ProDOS calls are handled
96
+ bool booted_ = false;
97
+
98
+ // Activity LED state
99
+ bool activity_ = false;
100
+ bool activityWrite_ = false;
101
+
102
+ // Static empty string for when no device is loaded
103
+ static const std::string emptyString_;
104
+
105
+ // Callbacks
106
+ MemReadCallback memRead_;
107
+ MemWriteCallback memWrite_;
108
+ RegGetCallback8 getA_;
109
+ RegSetCallback8 setA_;
110
+ RegGetCallback8 getP_;
111
+ RegSetCallback8 setP_;
112
+ RegGetCallback8 getSP_;
113
+ RegSetCallback8 setSP_;
114
+ RegGetCallback16 getPC_;
115
+ RegSetCallback16 setPC_;
116
+ RegSetCallback8 setX_;
117
+ RegSetCallback8 setY_;
118
+ };
119
+
120
+ } // namespace a2e
@@ -0,0 +1,237 @@
1
+ /*
2
+ * thunderclock_card.cpp - Thunderclock Plus real-time clock card implementation
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #include "thunderclock_card.hpp"
9
+ #include "roms.cpp" // For embedded ROM data
10
+ #include <ctime>
11
+ #include <cstring>
12
+
13
+ namespace a2e {
14
+
15
+ // I/O offsets (masked with 0x8F)
16
+ static constexpr uint8_t LOC_CONTROL = 0x00; // $C0n0 - Control register
17
+ static constexpr uint8_t LOC_AUX = 0x08; // $C0n8 - Aux register (unused)
18
+
19
+ // Control register flags
20
+ static constexpr uint8_t FLAG_CLOCK = 0x02; // Clock signal
21
+ static constexpr uint8_t FLAG_STROBE = 0x04; // Strobe signal
22
+
23
+ // Commands (bits 3-5 of control register, directly from uPD1990C C0-C2 pins)
24
+ // uPD1990C command encoding:
25
+ // 000 = Register Hold
26
+ // 001 = Register Shift
27
+ // 010 = Time Set
28
+ // 011 = Time Read
29
+ // 100 = TP 64Hz
30
+ // 101 = TP 256Hz
31
+ // 110 = TP 2048Hz
32
+ // 111 = Test Mode
33
+ static constexpr uint8_t CMD_REGHOLD = 0x00; // 000 - Hold register
34
+ static constexpr uint8_t CMD_REGSHIFT = 0x01; // 001 - Shift register
35
+ static constexpr uint8_t CMD_TIMESET = 0x02; // 010 - Time set
36
+ static constexpr uint8_t CMD_TIMEREAD = 0x03; // 011 - Time read
37
+
38
+ ThunderclockCard::ThunderclockCard()
39
+ : rom_(roms::ROM_THUNDERCLOCK)
40
+ , romSize_(roms::ROM_THUNDERCLOCK_SIZE)
41
+ {
42
+ latches_.fill(0);
43
+ reset();
44
+ }
45
+
46
+ void ThunderclockCard::updateLatches() {
47
+ // Get current time
48
+ std::time_t now = std::time(nullptr);
49
+ std::tm* tm = std::localtime(&now);
50
+
51
+ if (!tm) {
52
+ // Fallback if time unavailable
53
+ latches_.fill(0);
54
+ return;
55
+ }
56
+
57
+ int month = tm->tm_mon + 1; // 1-12 (convert from 0-11)
58
+ int dayOfWeek = tm->tm_wday; // 0-6 (Sunday=0)
59
+ int dayOfMonth = tm->tm_mday; // 1-31
60
+ int hour = tm->tm_hour; // 0-23
61
+ int minute = tm->tm_min; // 0-59
62
+ int second = tm->tm_sec; // 0-59
63
+
64
+ // Clear bits array
65
+ bitIndex_ = 0;
66
+
67
+ // Thunderclock Plus format (from AppleWin):
68
+ // 10 BCD nibbles, LSB-first within each nibble
69
+ // Order: sec_ones, sec_tens, min_ones, min_tens, hr_ones, hr_tens, day_ones, day_tens, dow, month
70
+ auto shiftNibbleLSB = [this](int value) {
71
+ for (int i = 0; i < 4; i++) {
72
+ if (bitIndex_ < 64) {
73
+ bits_[bitIndex_++] = (value >> i) & 1;
74
+ }
75
+ }
76
+ };
77
+
78
+ shiftNibbleLSB(second % 10); // Nibble 0: Second ones
79
+ shiftNibbleLSB(second / 10); // Nibble 1: Second tens
80
+ shiftNibbleLSB(minute % 10); // Nibble 2: Minute ones
81
+ shiftNibbleLSB(minute / 10); // Nibble 3: Minute tens
82
+ shiftNibbleLSB(hour % 10); // Nibble 4: Hour ones
83
+ shiftNibbleLSB(hour / 10); // Nibble 5: Hour tens
84
+ shiftNibbleLSB(dayOfMonth % 10); // Nibble 6: Day ones
85
+ shiftNibbleLSB(dayOfMonth / 10); // Nibble 7: Day tens
86
+ shiftNibbleLSB(dayOfWeek); // Nibble 8: Day of week (0-6)
87
+ shiftNibbleLSB(month); // Nibble 9: Month (1-12)
88
+
89
+ // Total: 10 * 4 = 40 bits
90
+ currentBitIndex_ = 0;
91
+ }
92
+
93
+ uint8_t ThunderclockCard::readIO(uint8_t offset) {
94
+ // The Thunderclock uses offset 0 ($C0n0) for the control/data register
95
+ // All 16 I/O addresses appear to return the same register value
96
+ (void)offset;
97
+
98
+ // Return current register value
99
+ // Bit 7 contains the current data bit
100
+ return register_;
101
+ }
102
+
103
+ void ThunderclockCard::writeIO(uint8_t offset, uint8_t value) {
104
+ // The Thunderclock uses offset 0 ($C0n0) for the control register
105
+ // All 16 I/O addresses appear to access the same register
106
+ (void)offset;
107
+
108
+ // Extract control signals
109
+ bool strobe = (value & FLAG_STROBE) != 0;
110
+ bool clock = (value & FLAG_CLOCK) != 0;
111
+
112
+ // Check for strobe rising edge
113
+ if (strobe && !strobe_) {
114
+ // Capture command from bits 3-5 (C0, C1, C2 of uPD1990C)
115
+ command_ = (value >> 3) & 0x07;
116
+
117
+ if (command_ == CMD_TIMEREAD) {
118
+ // Time read command - refresh the bit stream
119
+ updateLatches();
120
+ // First bit should be immediately available on DATA OUT after load
121
+ if (bitIndex_ > 0) {
122
+ register_ = bits_[0] ? 0x80 : 0x00;
123
+ }
124
+ }
125
+ }
126
+
127
+ // Check for clock rising edge
128
+ if (clock && !clock_) {
129
+ // Shift out next bit - increment FIRST, then read
130
+ // This is because bit 0 is already presented after strobe/load
131
+ if (command_ == CMD_REGSHIFT || command_ == CMD_TIMEREAD) {
132
+ currentBitIndex_++; // Advance to next bit position
133
+ if (currentBitIndex_ < bitIndex_) {
134
+ // Set bit 7 of register based on current bit
135
+ if (bits_[currentBitIndex_]) {
136
+ register_ |= 0x80;
137
+ } else {
138
+ register_ &= ~0x80;
139
+ }
140
+ } else {
141
+ register_ &= ~0x80;
142
+ }
143
+ }
144
+ }
145
+
146
+ // Save current state
147
+ strobe_ = strobe;
148
+ clock_ = clock;
149
+ }
150
+
151
+ uint8_t ThunderclockCard::peekIO(uint8_t offset) const {
152
+ (void)offset;
153
+ return register_;
154
+ }
155
+
156
+ uint8_t ThunderclockCard::readROM(uint8_t offset) {
157
+ // Slot ROM is the first 256 bytes
158
+ if (rom_ && romSize_ >= 256) {
159
+ return rom_[offset];
160
+ }
161
+ return 0xFF;
162
+ }
163
+
164
+ uint8_t ThunderclockCard::readExpansionROM(uint16_t offset) {
165
+ // Expansion ROM maps the ENTIRE 2KB ROM at $C800-$CFFF
166
+ // On real Thunderclock Plus hardware, the same 2KB ROM chip is used for both:
167
+ // - Slot ROM ($Cn00-$CnFF): reads bytes 0-255
168
+ // - Expansion ROM ($C800-$CFFF): reads bytes 0-2047 (full ROM)
169
+ if (rom_ && offset < romSize_) {
170
+ return rom_[offset];
171
+ }
172
+ return 0xFF;
173
+ }
174
+
175
+ void ThunderclockCard::reset() {
176
+ // Reset all state
177
+ strobe_ = false;
178
+ clock_ = false;
179
+ command_ = 0;
180
+ register_ = 0;
181
+ bitIndex_ = 0;
182
+ currentBitIndex_ = 0;
183
+
184
+ // Initialize time data
185
+ updateLatches();
186
+ }
187
+
188
+ size_t ThunderclockCard::serialize(uint8_t* buffer, size_t maxSize) const {
189
+ if (maxSize < STATE_SIZE) return 0;
190
+
191
+ size_t offset = 0;
192
+
193
+ // Control state
194
+ buffer[offset++] = strobe_ ? 1 : 0;
195
+ buffer[offset++] = clock_ ? 1 : 0;
196
+ buffer[offset++] = command_;
197
+ buffer[offset++] = register_;
198
+ buffer[offset++] = static_cast<uint8_t>(bitIndex_);
199
+ buffer[offset++] = static_cast<uint8_t>(currentBitIndex_);
200
+
201
+ // Bit stream data (64 bytes)
202
+ for (int i = 0; i < 64; i++) {
203
+ buffer[offset++] = bits_[i];
204
+ }
205
+
206
+ // Reserved (2 bytes for future use)
207
+ buffer[offset++] = 0;
208
+ buffer[offset++] = 0;
209
+
210
+ return offset; // Should be 72 bytes
211
+ }
212
+
213
+ size_t ThunderclockCard::deserialize(const uint8_t* buffer, size_t size) {
214
+ if (size < STATE_SIZE) return 0;
215
+
216
+ size_t offset = 0;
217
+
218
+ // Control state
219
+ strobe_ = buffer[offset++] != 0;
220
+ clock_ = buffer[offset++] != 0;
221
+ command_ = buffer[offset++];
222
+ register_ = buffer[offset++];
223
+ bitIndex_ = buffer[offset++];
224
+ currentBitIndex_ = buffer[offset++];
225
+
226
+ // Bit stream data (64 bytes)
227
+ for (int i = 0; i < 64; i++) {
228
+ bits_[i] = buffer[offset++];
229
+ }
230
+
231
+ // Skip reserved bytes
232
+ offset += 2;
233
+
234
+ return offset; // Should be 72 bytes
235
+ }
236
+
237
+ } // namespace a2e
@@ -0,0 +1,122 @@
1
+ /*
2
+ * thunderclock_card.hpp - Thunderclock Plus real-time clock card
3
+ *
4
+ * Written by
5
+ * Mike Daley <michael_daley@icloud.com>
6
+ */
7
+
8
+ #pragma once
9
+
10
+ #include "expansion_card.hpp"
11
+ #include <array>
12
+ #include <cstdint>
13
+
14
+ namespace a2e {
15
+
16
+ /**
17
+ * ThunderclockCard - ProDOS-compatible clock card
18
+ *
19
+ * Implements the Thunderclock Plus clock card that provides automatic
20
+ * date/time stamping for ProDOS applications. This eliminates the
21
+ * "Enter today's date" prompts and enables proper file timestamps.
22
+ *
23
+ * ProDOS Detection:
24
+ * ProDOS scans slots looking for specific ROM signature bytes:
25
+ * - $Cn00: $08 (PHP instruction)
26
+ * - $Cn02: $28 (signature byte)
27
+ * - $Cn04: $58 (signature byte)
28
+ * - $Cn06: $70 (signature byte)
29
+ *
30
+ * When ProDOS finds these bytes, it:
31
+ * 1. Sets bit 0 of MACHID ($BF98) to indicate clock present
32
+ * 2. Patches the clock driver address at $BF07-$BF08
33
+ * 3. Changes $BF06 from $60 (RTS) to $4C (JMP)
34
+ *
35
+ * Hardware Interface:
36
+ * The Thunderclock Plus uses a serial interface via the control register
37
+ * at $C0n0 (where n = slot + 8). Time data is shifted out bit by bit.
38
+ *
39
+ * Control Register ($C0n0):
40
+ * - Bit 2 (STROBE): Rising edge triggers command execution
41
+ * - Bit 1 (CLOCK): Rising edge shifts next data bit
42
+ * - Bits 3-5: Command (from uPD1990C C0-C2 pins)
43
+ *
44
+ * Time Data Format (40 bits, 10 BCD nibbles, LSB-first within each nibble):
45
+ * - Second ones, Second tens (8 bits)
46
+ * - Minute ones, Minute tens (8 bits)
47
+ * - Hour ones, Hour tens (8 bits)
48
+ * - Day ones, Day tens (8 bits)
49
+ * - Day of week (4 bits, 0-6)
50
+ * - Month (4 bits, 1-12)
51
+ *
52
+ * ROM Space:
53
+ * - Slot ROM ($Cn00-$CnFF): 256 bytes - contains ProDOS driver
54
+ * - Expansion ROM ($C800-$CFFF): 1792 bytes - contains utility routines
55
+ *
56
+ * Typically installed in Slot 5 or Slot 7.
57
+ */
58
+ class ThunderclockCard : public ExpansionCard {
59
+ public:
60
+ ThunderclockCard();
61
+ ~ThunderclockCard() override = default;
62
+
63
+ // Delete copy
64
+ ThunderclockCard(const ThunderclockCard&) = delete;
65
+ ThunderclockCard& operator=(const ThunderclockCard&) = delete;
66
+
67
+ // Allow move
68
+ ThunderclockCard(ThunderclockCard&&) = default;
69
+ ThunderclockCard& operator=(ThunderclockCard&&) = default;
70
+
71
+ // ===== ExpansionCard Interface =====
72
+
73
+ // I/O space ($C0n0-$C0nF)
74
+ uint8_t readIO(uint8_t offset) override;
75
+ void writeIO(uint8_t offset, uint8_t value) override;
76
+ uint8_t peekIO(uint8_t offset) const override;
77
+
78
+ // ROM space ($Cn00-$CnFF)
79
+ uint8_t readROM(uint8_t offset) override;
80
+ bool hasROM() const override { return true; }
81
+
82
+ // Expansion ROM ($C800-$CFFF)
83
+ bool hasExpansionROM() const override { return true; }
84
+ uint8_t readExpansionROM(uint16_t offset) override;
85
+
86
+ void reset() override;
87
+
88
+ const char* getName() const override { return "Thunderclock"; }
89
+ uint8_t getPreferredSlot() const override { return 5; }
90
+
91
+ // State serialization
92
+ static constexpr size_t STATE_SIZE = 72; // 6 bytes state + 64 bytes bits + 2 reserved
93
+ size_t getStateSize() const override { return STATE_SIZE; }
94
+ size_t serialize(uint8_t* buffer, size_t maxSize) const override;
95
+ size_t deserialize(const uint8_t* buffer, size_t size) override;
96
+
97
+ private:
98
+ // ROM data is loaded from embedded ROM file
99
+ const uint8_t* rom_; // Pointer to embedded ROM data
100
+ size_t romSize_; // Size of ROM (should be 2048)
101
+
102
+ // Serial interface state
103
+ bool strobe_ = false; // Previous strobe state
104
+ bool clock_ = false; // Previous clock state
105
+ uint8_t command_ = 0; // Current command
106
+ uint8_t register_ = 0; // Output register (bit 7 is data out)
107
+
108
+ // Time data as bit stream
109
+ std::array<uint8_t, 64> bits_; // Bit buffer
110
+ int bitIndex_ = 0; // Number of valid bits
111
+ int currentBitIndex_ = 0; // Current bit being read
112
+
113
+ // Unused but kept for API compatibility
114
+ std::array<uint8_t, 16> latches_;
115
+
116
+ /**
117
+ * Update the bit stream with current system time
118
+ */
119
+ void updateLatches();
120
+ };
121
+
122
+ } // namespace a2e