PageSourceSearch

https://retroapplejs.github.io/res/EMU_CARD_thunderclock.js

js retroapplejs.github.io collected 2026-10-03 09:51:06 UTC 39,388 bytes, 1,114 lines download raw bytes

1// 2026 Thunderware ThunderClock Plus peripheral for RetroAppleJS.
2//
3// EMU_CARD_tunderclock.js
4//
5// Stage 1 implementation:
6//   - authoritative 2 KiB 2716 ROM image
7//   - $Cn00-$CnFF slot ROM from ROM $0000-$00FF
8//   - shared $C800-$CFFF expansion-ROM ownership/release behavior
9//   - NEC uPD1990AC-compatible 40-bit serial RTC interface
10//   - live clock initialized from the browser host's local date/time
11//   - clock read and clock set through the original ThunderClock firmware
12//   - Serial Pro-style peripheral RTC controls (live view, SET and HOST)
13//
14// Intentionally deferred:
15//   - ThunderClock interrupt latch / IRQ generation (64/256/2048 Hz)
16//   - BSR/X-10 ultrasonic output
17//   - browser-persistent clock offset across page reloads
18//   - uPD1990 test-mode timing details
19//
20// Hardware notes:
21//   - Slot I/O base is $C080 + (Apple slot * $10).
22//   - Writes are decoded only at local offset $0.
23//       bit 0 = uPD1990 DATA IN
24//       bit 1 = uPD1990 CLK
25//       bit 2 = uPD1990 STB
26//       bits 3..5 = uPD1990 command C0..C2
27//   - Reads mirror across the 16-byte slot-I/O range.
28//   - RTC DATA OUT is returned in Apple data-bus bit 7.
29//   - The uPD1990A stores no year.  The emulated year is retained internally
30//     only so JavaScript can advance month/day correctly; the physical 40-bit
31//     serial image remains seconds, minutes, hours, day, month/day-of-week.
32//
33// ROM identity:
34//   CRC32   1B99C4E3
35//   SHA-1   60f434f5325899d7ea257a6e56e6f53eae65146a
36//   SHA-256 ecca793179427fdd193373080bf23e49a95743bd7b5e0b218c747663938b6c03
37//
38// The filename deliberately follows the project-requested spelling:
39// EMU_CARD_tunderclock.js
40
41
42const THUNDERCLOCKPLUS_ROM = new Uint8Array([
43    // $0000-$000F
44    0x08,0x78,0x28,0x2C,0x58,0xFF,0x70,0x05,0x38,0xB0,0x01,0x18,0xB8,0x08,0x78,0x48,
45    // $0010-$001F
46    0x8A,0x48,0x98,0x48,0xAD,0xFF,0xCF,0x20,0x1A,0xC8,0x68,0x68,0xBA,0x8D,0xF8,0x07,
47    // $0020-$002F
48    0x0A,0x0A,0x0A,0x0A,0x8D,0x78,0x07,0x68,0x68,0x68,0xA8,0x68,0x9A,0x09,0x04,0x48,
49    // $0030-$003F
50    0x28,0x98,0xAC,0x78,0x07,0xAE,0xF8,0x07,0x29,0x7F,0x48,0x50,0x0E,0xB8,0xA5,0x36,
51    // $0040-$004F
52    0xD0,0x04,0xE4,0x37,0xF0,0x0A,0xA9,0x08,0x85,0x38,0x38,0x90,0x16,0x4C,0x50,0xC9,
53    // $0050-$005F
54    0xA9,0x0B,0x85,0x36,0xA9,0x5E,0x9D,0xB8,0x04,0xA9,0x0A,0x9D,0x38,0x04,0xA9,0x00,
55    // $0060-$006F
56    0x9D,0x38,0x05,0x68,0xC9,0x25,0xF0,0x5C,0xC9,0x26,0xF0,0x58,0xC9,0x3C,0xF0,0x54,
57    // $0070-$007F
58    0xC9,0x3E,0xF0,0x50,0xC9,0x23,0xF0,0x4C,0xC9,0x5E,0xF0,0x5A,0xC9,0x21,0xF0,0x4A,
59    // $0080-$008F
60    0xC9,0x2A,0xF0,0x52,0xA2,0x20,0xC9,0x2C,0xF0,0x52,0xA2,0x28,0xC9,0x2E,0xF0,0x4C,
61    // $0090-$009F
62    0xA2,0x30,0xC9,0x2F,0xF0,0x46,0xAE,0xF8,0x07,0x48,0xBD,0xB8,0x04,0xC9,0x21,0xF0,
63    // $00A0-$00AF
64    0x41,0xC9,0x5E,0xF0,0x0E,0x68,0x29,0x1F,0x0A,0x38,0xE9,0x01,0x9D,0x38,0x04,0xA9,
65    // $00B0-$00BF
66    0x5E,0xD0,0x23,0x68,0x38,0xE9,0x41,0x30,0x20,0xC9,0x16,0x10,0x1C,0xAA,0xBD,0x8D,
67    // $00C0-$00CF
68    0xCB,0x4C,0x04,0xCB,0x9D,0x38,0x05,0x4C,0x2F,0xC9,0xA9,0x20,0x20,0xB7,0xCA,0xA9,
69    // $00D0-$00DF
70    0x08,0x20,0xB7,0xCA,0xA9,0x21,0x9D,0xB8,0x04,0x4C,0x2F,0xC9,0x8A,0x20,0xB7,0xCA,
71    // $00E0-$00EF
72    0xD0,0xF7,0x68,0xC9,0x0D,0xF0,0x13,0xC9,0x20,0xF0,0x44,0x29,0x0F,0x20,0xCF,0xCA,
73    // $00F0-$00FF
74    0xF0,0x02,0xA9,0x0A,0x9D,0x38,0x07,0x18,0x90,0x35,0x20,0xCF,0xCA,0x7D,0x38,0x07,
75    // $0100-$010F
76    0x48,0xA9,0x09,0x9D,0x38,0x06,0x20,0xCF,0xCA,0x48,0xDE,0x38,0x06,0xD0,0xF7,0xA9,
77    // $0110-$011F
78    0x0A,0x9D,0x38,0x06,0x68,0x20,0xCF,0xCA,0xDE,0x38,0x06,0xD0,0xF7,0xA9,0x10,0x20,
79    // $0120-$012F
80    0xB7,0xCA,0xD0,0xB2,0xBA,0x68,0x68,0x98,0x48,0x68,0x68,0xA9,0x8D,0x48,0x9A,0xAE,
81    // $0130-$013F
82    0xF8,0x07,0xAC,0x78,0x07,0xBD,0xB8,0x03,0xDD,0x38,0x07,0xD0,0x0C,0x1D,0x38,0x07,
83    // $0140-$014F
84    0xF0,0x07,0xC9,0x40,0xD0,0x03,0x99,0x80,0xC0,0x68,0xA8,0x68,0xAA,0x68,0x28,0x60,
85    // $0150-$015F
86    0x68,0xA9,0x18,0x20,0xB7,0xCA,0xA9,0x08,0x20,0xB7,0xCA,0xA9,0x09,0x9D,0x38,0x06,
87    // $0160-$016F
88    0x20,0xCF,0xCA,0xC9,0x0A,0x30,0x02,0xA9,0x00,0x48,0xDE,0x38,0x06,0xD0,0xF1,0x20,
89    // $0170-$017F
90    0xCF,0xCA,0xC9,0x0D,0x30,0x02,0xA9,0x00,0x48,0xBD,0x38,0x05,0xF0,0x04,0xC9,0x23,
91    // $0180-$018F
92    0xD0,0x5E,0xA0,0x00,0x68,0xC9,0x0A,0x30,0x04,0xC8,0x38,0xE9,0x0A,0x48,0x98,0xA2,
93    // $0190-$019F
94    0x00,0x20,0xA6,0xCA,0x68,0x20,0xA6,0xCA,0xAC,0xF8,0x07,0xB9,0x38,0x05,0xD0,0x2A,
95    // $01A0-$01AF
96    0xA9,0xAF,0x20,0xA8,0xCA,0x68,0xA0,0x04,0x68,0x20,0xA6,0xCA,0x68,0x20,0xA6,0xCA,
97    // $01B0-$01BF
98    0x88,0xF0,0x07,0xA9,0x3B,0x20,0xA8,0xCA,0xD0,0xEE,0xA9,0xA0,0x8D,0x05,0x02,0xB9,
99    // $01C0-$01CF
100    0xF3,0xCB,0xF0,0x19,0x20,0xA8,0xCA,0xC8,0xD0,0xF5,0xA9,0x00,0x48,0xA0,0x05,0x20,
101    // $01D0-$01DF
102    0xAF,0xCA,0x68,0x20,0xA6,0xCA,0x68,0x20,0xA6,0xCA,0x88,0xD0,0xF2,0x4C,0x9A,0xCA,
103    // $01E0-$01EF
104    0x68,0xAA,0x68,0x0A,0x0A,0xA8,0x8A,0x0A,0x0A,0x48,0xAE,0xF8,0x07,0xBD,0x38,0x05,
105    // $01F0-$01FF
106    0xA2,0xA0,0xC9,0x3C,0xB0,0x02,0xA2,0xA2,0x8A,0xA2,0x00,0x20,0xA8,0xCA,0xB9,0xA3,
107    // $0200-$020F
108    0xCB,0xC8,0x20,0xA8,0xCA,0xC9,0xA0,0xD0,0xF5,0x68,0xA8,0xB9,0xBF,0xCB,0xC8,0x20,
109    // $0210-$021F
110    0xA8,0xCA,0xC9,0xA0,0xD0,0xF5,0x68,0x20,0xA4,0xCA,0x68,0x20,0xA6,0xCA,0x20,0xB3,
111    // $0220-$022F
112    0xCA,0xA0,0x03,0x68,0x20,0xA4,0xCA,0xD0,0x03,0x20,0xA6,0xCA,0x68,0x20,0xA6,0xCA,
113    // $0230-$023F
114    0x88,0xF0,0x09,0xA9,0xBA,0x20,0xA8,0xCA,0x68,0x4C,0x29,0xCA,0xAC,0xF8,0x07,0xB9,
115    // $0240-$024F
116    0x38,0x05,0xC9,0x25,0xF0,0x04,0xC9,0x3E,0xD0,0x50,0xA0,0x41,0xAD,0x0C,0x02,0xC9,
117    // $0250-$025F
118    0xA0,0xD0,0x02,0xA9,0x30,0x0A,0x0A,0x0A,0x0A,0x8D,0x20,0x02,0xAD,0x0D,0x02,0x29,
119    // $0260-$026F
120    0x0F,0x0D,0x20,0x02,0xC9,0x12,0x30,0x02,0xA0,0x50,0xC9,0x00,0xD0,0x04,0xA9,0x12,
121    // $0270-$027F
122    0xD0,0x09,0xC9,0x13,0x30,0x16,0xF8,0x38,0xE9,0x12,0xD8,0xA2,0x0C,0x48,0x20,0xFF,
123    // $0280-$028F
124    0xCA,0x29,0x0F,0x20,0xA4,0xCA,0x68,0x29,0x0F,0x20,0xA6,0xCA,0xA2,0x14,0x20,0xB3,
125    // $0290-$029F
126    0xCA,0x98,0x20,0xA8,0xCA,0xA9,0x4D,0x20,0xA8,0xCA,0x8A,0xA8,0xA9,0x8D,0x20,0xA8,
127    // $02A0-$02AF
128    0xCA,0x4C,0x24,0xC9,0xF0,0x0D,0x09,0x30,0x09,0x80,0x9D,0x00,0x02,0xE8,0x60,0xA9,
129    // $02B0-$02BF
130    0xAC,0xD0,0xF5,0xA9,0xA0,0xD0,0xF1,0x99,0x80,0xC0,0x09,0x04,0x99,0x80,0xC0,0x20,
131    // $02C0-$02CF
132    0xC7,0xCA,0x49,0x04,0x99,0x80,0xC0,0x20,0xCA,0xCA,0x48,0x48,0x68,0x68,0x60,0x48,
133    // $02D0-$02DF
134    0xA9,0x04,0x9D,0xB8,0x06,0xA9,0x00,0x9D,0xB8,0x05,0xB9,0x80,0xC0,0x0A,0x7E,0xB8,
135    // $02E0-$02EF
136    0x05,0x68,0x48,0x29,0x01,0x99,0x80,0xC0,0x09,0x02,0x99,0x80,0xC0,0x49,0x02,0x99,
137    // $02F0-$02FF
138    0x80,0xC0,0x68,0x6A,0x48,0xDE,0xB8,0x06,0xD0,0xE0,0x68,0xBD,0xB8,0x05,0x18,0x6A,
139    // $0300-$030F
140    0x6A,0x6A,0x6A,0x60,0x48,0xAE,0xF8,0x07,0xBD,0x38,0x04,0x9D,0xB8,0x06,0x20,0x50,
141    // $0310-$031F
142    0xCB,0x68,0x48,0x20,0x4C,0xCB,0x68,0x48,0x49,0xF8,0x20,0x4C,0xCB,0xA2,0xA0,0x20,
143    // $0320-$032F
144    0x69,0xCB,0xA2,0xA0,0x20,0x69,0xCB,0xA0,0x18,0x20,0x60,0xCB,0xAE,0xF8,0x07,0xDE,
145    // $0330-$033F
146    0xB8,0x06,0xD0,0xDA,0x68,0xA0,0xBC,0xCA,0xD0,0xFD,0x88,0xD0,0xFA,0x4C,0x2F,0xC9,
147    // $0340-$034F
148    0x48,0x90,0x05,0x20,0x50,0xCB,0xF0,0x03,0x20,0x59,0xCB,0x68,0x0A,0xD0,0xF1,0x60,
149    // $0350-$035F
150    0xA2,0x50,0x20,0x69,0xCB,0xA0,0x28,0xD0,0x07,0xA2,0x18,0x20,0x69,0xCB,0xA0,0x44,
151    // $0360-$036F
152    0xA2,0x13,0xCA,0xD0,0xFD,0x88,0xD0,0xF8,0x60,0xAC,0x78,0x07,0xA9,0x20,0x99,0x80,
153    // $0370-$037F
154    0xC0,0xEA,0xEA,0xEA,0x49,0x20,0x99,0x80,0xC0,0xEA,0xEA,0xEA,0x49,0x20,0x99,0x80,
155    // $0380-$038F
156    0xC0,0xEA,0xEA,0xEA,0x49,0x20,0x99,0x80,0xC0,0xCA,0xD0,0xE0,0x60,0x64,0xE4,0x24,
157    // $0390-$039F
158    0xA4,0x14,0x94,0x54,0xD4,0x74,0xF4,0x34,0xB4,0x04,0x84,0x44,0xC4,0x2C,0x3C,0x5C,
159    // $03A0-$03AF
160    0x4C,0x1C,0x0C,0x53,0x55,0x4E,0x20,0x4D,0x4F,0x4E,0x20,0x54,0x55,0x45,0x20,0x57,
161    // $03B0-$03BF
162    0x45,0x44,0x20,0x54,0x48,0x55,0x20,0x46,0x52,0x49,0x20,0x53,0x41,0x54,0x20,0x45,
163    // $03C0-$03CF
164    0x52,0x52,0x20,0x4A,0x41,0x4E,0x20,0x46,0x45,0x42,0x20,0x4D,0x41,0x52,0x20,0x41,
165    // $03D0-$03DF
166    0x50,0x52,0x20,0x4D,0x41,0x59,0x20,0x4A,0x55,0x4E,0x20,0x4A,0x55,0x4C,0x20,0x41,
167    // $03E0-$03EF
168    0x55,0x47,0x20,0x53,0x45,0x50,0x20,0x4F,0x43,0x54,0x20,0x4E,0x4F,0x56,0x20,0x44,
169    // $03F0-$03FF
170    0x45,0x43,0x20,0xAE,0xB0,0xB0,0xB0,0x00,0xAE,0xB0,0xB0,0xB0,0x00,0xA0,0xA0,0xC1,
171    // $0400-$040F
172    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
173    // $0410-$041F
174    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
175    // $0420-$042F
176    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
177    // $0430-$043F
178    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
179    // $0440-$044F
180    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
181    // $0450-$045F
182    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
183    // $0460-$046F
184    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
185    // $0470-$047F
186    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
187    // $0480-$048F
188    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
189    // $0490-$049F
190    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
191    // $04A0-$04AF
192    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
193    // $04B0-$04BF
194    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
195    // $04C0-$04CF
196    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
197    // $04D0-$04DF
198    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
199    // $04E0-$04EF
200    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
201    // $04F0-$04FF
202    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
203    // $0500-$050F
204    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
205    // $0510-$051F
206    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
207    // $0520-$052F
208    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
209    // $0530-$053F
210    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
211    // $0540-$054F
212    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
213    // $0550-$055F
214    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
215    // $0560-$056F
216    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
217    // $0570-$057F
218    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
219    // $0580-$058F
220    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
221    // $0590-$059F
222    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
223    // $05A0-$05AF
224    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
225    // $05B0-$05BF
226    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
227    // $05C0-$05CF
228    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
229    // $05D0-$05DF
230    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
231    // $05E0-$05EF
232    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
233    // $05F0-$05FF
234    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
235    // $0600-$060F
236    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
237    // $0610-$061F
238    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
239    // $0620-$062F
240    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
241    // $0630-$063F
242    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
243    // $0640-$064F
244    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
245    // $0650-$065F
246    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
247    // $0660-$066F
248    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
249    // $0670-$067F
250    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
251    // $0680-$068F
252    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
253    // $0690-$069F
254    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
255    // $06A0-$06AF
256    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
257    // $06B0-$06BF
258    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
259    // $06C0-$06CF
260    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
261    // $06D0-$06DF
262    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
263    // $06E0-$06EF
264    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
265    // $06F0-$06FF
266    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
267    // $0700-$070F
268    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
269    // $0710-$071F
270    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
271    // $0720-$072F
272    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
273    // $0730-$073F
274    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
275    // $0740-$074F
276    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
277    // $0750-$075F
278    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
279    // $0760-$076F
280    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
281    // $0770-$077F
282    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
283    // $0780-$078F
284    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
285    // $0790-$079F
286    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
287    // $07A0-$07AF
288    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
289    // $07B0-$07BF
290    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
291    // $07C0-$07CF
292    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
293    // $07D0-$07DF
294    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
295    // $07E0-$07EF
296    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
297    // $07F0-$07FF
298    0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF
299]);
300
301
302// Discovery container. Apple2IO mounts a separate live instance.
303if(oEMU===undefined) var oEMU = {"component":{"IO":{"ThunderClockPlus":new ThunderClockPlusCard()}}}
304else oEMU.component.IO.ThunderClockPlus = new ThunderClockPlusCard();
305
306
307function ThunderClockPlusCard()
308{
309    const bDebug = false;
310
311    this.id = {"PCODE":"TCLKP", "icon":"fa fa-clock"};
312
313    var state =
314    {
315         "active":true
316    };
317
318    this.state = state;
319
320    var thunderclock = this;       // stand-in where 'this' is absent inside callbacks
321
322
323    /*
324     * NEC uPD1990AC command values.
325     *
326     * The physical ThunderClock drives the chip's three parallel command pins
327     * from bits 3..5 of its output latch.  Command 7 is TEST on the uPD1990A.
328     */
329    const RTC_REGISTER_HOLD = 0;
330    const RTC_SHIFT         = 1;
331    const RTC_TIME_SET      = 2;
332    const RTC_TIME_READ     = 3;
333    const RTC_TP_64HZ       = 4;
334    const RTC_TP_256HZ      = 5;
335    const RTC_TP_2048HZ     = 6;
336    const RTC_TEST          = 7;
337
338
339    /*
340     * Browser-backed uPD1990A.
341     *
342     * The real chip has a five-byte (40-bit) serial register:
343     *   byte 0  BCD seconds
344     *   byte 1  BCD minutes
345     *   byte 2  BCD hours
346     *   byte 3  BCD day-of-month
347     *   byte 4  high nibble = month (1..12)
348     *           low nibble  = day-of-week (0=Sunday .. 6=Saturday)
349     *
350     * The chip does not contain a year register.  JavaScript still needs a
351     * year internally to make calendar rollover behave naturally; software
352     * never sees it through the ThunderClock hardware interface.
353     *
354     * baseTimeMs/hostBaseMs use the same "emulated time + host elapsed time"
355     * model as the Serial Pro RTC.  The clock object survives reset()/restart()
356     * while the mounted card instance remains alive.
357     */
358    var rtcStartMs = Date.now();
359    var rtc =
360    {
361         "baseTimeMs":rtcStartMs
362        ,"hostBaseMs":rtcStartMs
363        ,"running":true
364        ,"weekdayOffset":0
365        ,"shift":new Uint8Array(5)
366        ,"mode":RTC_REGISTER_HOLD
367        ,"commandPending":RTC_REGISTER_HOLD
368        ,"control":0x00
369        ,"dataIn":0
370        ,"dataOut":0
371        ,"clk":0
372        ,"stb":0
373        ,"testMode":false
374        ,"tpRate":64
375    };
376
377
378    function rtcToBCD(value)
379    {
380        value = Math.max(0,Math.floor(value));
381        return (((Math.floor(value/10) % 10) << 4) | (value % 10)) & 0xFF;
382    }
383
384
385    function rtcFromBCD(value)
386    {
387        return ((((value >> 4) & 0x0F) * 10) + (value & 0x0F)) & 0xFF;
388    }
389
390
391    function rtcCurrentDate()
392    {
393        var ms = rtc.baseTimeMs;
394
395        if(rtc.running)
396            ms += Date.now() - rtc.hostBaseMs;
397
398        return new Date(ms);
399    }
400
401
402    function rtcRebase(date)
403    {
404        rtc.baseTimeMs = date.getTime();
405        rtc.hostBaseMs = Date.now();
406    }
407
408
409    function rtcSetRunning(running)
410    {
411        running = !!running;
412
413        if(rtc.running==running)
414            return;
415
416        // Freeze the exact current value when stopping.  When starting again,
417        // elapsed wall time begins from that frozen value rather than including
418        // the period for which the uPD1990 counter was stopped.
419        if(!running)
420            rtcRebase(rtcCurrentDate());
421        else
422            rtc.hostBaseMs = Date.now();
423
424        rtc.running = running;
425    }
426
427
428    function rtcPackTime()
429    {
430        var date = rtcCurrentDate();
431
432        rtc.shift[0] = rtcToBCD(date.getSeconds());
433        rtc.shift[1] = rtcToBCD(date.getMinutes());
434        rtc.shift[2] = rtcToBCD(date.getHours());
435        rtc.shift[3] = rtcToBCD(date.getDate());
436
437        var dow = (date.getDay() + rtc.weekdayOffset) % 7;
438        rtc.shift[4] = (((date.getMonth()+1) & 0x0F) << 4) | (dow & 0x0F);
439    }
440
441
442    function rtcApplyShiftToClock()
443    {
444        var current = rtcCurrentDate();
445
446        var second = Math.max(0,Math.min(59,rtcFromBCD(rtc.shift[0])));
447        var minute = Math.max(0,Math.min(59,rtcFromBCD(rtc.shift[1])));
448        var hour   = Math.max(0,Math.min(23,rtcFromBCD(rtc.shift[2])));
449        var day    = Math.max(1,Math.min(31,rtcFromBCD(rtc.shift[3])));
450        var month  = Math.max(1,Math.min(12,(rtc.shift[4] >> 4) & 0x0F));
451        var dow    = rtc.shift[4] & 0x0F;
452        if(dow>6) dow %= 7;
453
454        /*
455         * Preserve the current internal year because a real uPD1990A has no
456         * year field.  Build month/day from day 1 first so JavaScript does not
457         * accidentally carry a large existing day into the following month.
458         */
459        var date = new Date(current.getTime());
460        var year = date.getFullYear();
461
462        date.setFullYear(year,month-1,1);
463        date.setHours(hour,minute,second,0);
464        date.setDate(day);
465
466        rtcRebase(date);
467        rtc.weekdayOffset = (dow - date.getDay() + 7) % 7;
468    }
469
470
471    function rtcShiftClockRising()
472    {
473        if(rtc.mode!=RTC_SHIFT)
474            return;
475
476        /*
477         * uPD1990A shifts toward bit 0.  DATA IN enters bit 7 of the final
478         * byte; DATA OUT becomes the new bit 0 of byte 0 after the edge.
479         */
480        var incoming = rtc.dataIn & 1;
481
482        for(var i=0;i<rtc.shift.length;i++)
483        {
484            var next = i+1<rtc.shift.length ? (rtc.shift[i+1] & 1) : incoming;
485            rtc.shift[i] = ((rtc.shift[i] >> 1) | (next << 7)) & 0xFF;
486        }
487
488        rtc.dataOut = rtc.shift[0] & 1;
489    }
490
491
492    function rtcLatchCommand(command)
493    {
494        command &= 0x07;
495        rtc.mode = command;
496
497        /*
498         * This follows the uPD1990A behavior used by the ThunderClock ROM and
499         * by MAME's uPD1990A device model.
500         */
501        if(command==RTC_REGISTER_HOLD ||
502           command==RTC_TP_64HZ ||
503           command==RTC_TP_256HZ ||
504           command==RTC_TP_2048HZ)
505        {
506            rtcSetRunning(true);
507            rtc.testMode = false;
508        }
509
510        switch(command)
511        {
512            case RTC_REGISTER_HOLD:
513                rtc.tpRate = 64;
514                break;
515
516            case RTC_SHIFT:
517                if(!rtc.testMode)
518                    rtcSetRunning(true);
519                rtc.dataOut = rtc.shift[0] & 1;
520                break;
521
522            case RTC_TIME_SET:
523                rtcSetRunning(false);
524                rtc.dataOut = rtc.shift[0] & 1;
525                rtcApplyShiftToClock();
526                break;
527
528            case RTC_TIME_READ:
529                if(!rtc.testMode)
530                    rtcSetRunning(true);
531                rtcPackTime();
532                break;
533
534            case RTC_TP_64HZ:
535                rtc.tpRate = 64;
536                break;
537
538            case RTC_TP_256HZ:
539                rtc.tpRate = 256;
540                break;
541
542            case RTC_TP_2048HZ:
543                rtc.tpRate = 2048;
544                break;
545
546            case RTC_TEST:
547                rtcSetRunning(false);
548                rtc.testMode = true;
549                break;
550        }
551
552        if(bDebug)
553            console.log("ThunderClock uPD1990 command "+command);
554    }
555
556
557    function rtcReadDataOut()
558    {
559        /*
560         * SHIFT and TIME SET expose the serial register directly.
561         *
562         * REGISTER HOLD and TIME READ make DATA OUT a divider-derived pulse on
563         * the physical uPD1990A.  The ThunderClock firmware does not depend on
564         * that pulse for normal RTC transfers, but returning a reasonable wall-
565         * clock phase here is closer to the chip than leaving a stale bit.
566         */
567        if(rtc.mode==RTC_SHIFT || rtc.mode==RTC_TIME_SET || rtc.testMode)
568            return rtc.dataOut & 1;
569
570        if(rtc.mode==RTC_REGISTER_HOLD)
571            return Math.floor(Date.now()/500) & 1;       // 1 Hz square wave
572
573        if(rtc.mode==RTC_TIME_READ)
574            return Math.floor(Date.now()/1000) & 1;      // 0.5 Hz square wave
575
576        return rtc.dataOut & 1;
577    }
578
579
580    this.action =
581    {
582        "SlotROM":
583        {
584            "RD":{"callback":function(addr,ctx) {
585                return thunderclock.readROM(addr,ctx);
586            }}
587        },
588        "SlotIO":
589        {
590            "RD":{"callback":function(addr,ctx) {
591                return thunderclock.readSlotIO(addr,ctx);
592            }},
593            "WR":{"callback":function(addr,d8,ctx) {
594                return thunderclock.writeSlotIO(addr,d8,ctx);
595            }}
596        },
597        "HostROM":
598        {
599            "RD":{"callback":function(addr,ctx) {
600                return thunderclock.readHostROM(addr,ctx);
601            }}
602        }
603    };
604
605
606    function ownsExpansionROM()
607    {
608        if(!thunderclock.mount) return false;
609        return oEMU.component.IO.ACTION_MAP.Hslot == (thunderclock.mount.slotN-1);
610    }
611
612
613    function selectExpansionROM(ctx)
614    {
615        // Safe/debugger ROM inspection must not alter shared C8 ownership.
616        if(ctx && ctx.bRO===true) return false;
617        if(!thunderclock.mount || !thunderclock.mount.ranges || !thunderclock.mount.ranges.HostROM)
618            return false;
619
620        var CIO = oEMU.component.IO;
621        if(!CIO.ACTION_MAP) return false;
622
623        if(CIO.ACTION_MAP.Hslot == (thunderclock.mount.slotN-1))
624            return true;
625
626        CIO.ACTION_MAP.Hslot = thunderclock.mount.slotN-1;
627
628        var range = thunderclock.mount.ranges.HostROM;
629        for(var i=range.from;i<=range.to;i++)
630            CIO.ACTION_MAP.RD[i] = thunderclock.action.HostROM.RD.callback;
631
632        if(bDebug)
633            console.log("ThunderClock Plus claims $C800-$CFFF from PR#"+(thunderclock.mount.slotN-1));
634
635        return true;
636    }
637
638
639    function releaseExpansionROM()
640    {
641        if(!thunderclock.mount || !oEMU.component.IO.ACTION_MAP) return false;
642
643        var CIO = oEMU.component.IO;
644        if(CIO.ACTION_MAP.Hslot != (thunderclock.mount.slotN-1))
645            return false;
646
647        CIO.ACTION_MAP.Hslot = null;
648        return true;
649    }
650
651
652    this.readROM = function(addr,ctx)
653    {
654        /*
655         * $Cn00-$CnFF is the first 256 bytes of the 2716.  The firmware is
656         * slot-independent; unlike Serial Pro, there are no seven preassembled
657         * slot pages.
658         */
659        selectExpansionROM(ctx);
660
661        addr &= 0xFF;
662        return THUNDERCLOCKPLUS_ROM[addr] & 0xFF;
663    };
664
665
666    this.readSlotIO = function(addr,ctx)
667    {
668        /*
669         * The real card mirrors its read result over local offsets $0..$F.
670         * DATA OUT is physically presented on D7.  Interrupt status on D5 is
671         * deliberately deferred until the card's IRQ latch is implemented.
672         */
673        addr &= 0x0F;
674        return rtcReadDataOut() ? 0x80 : 0x00;
675    };
676
677
678    this.writeSlotIO = function(addr,d8,ctx)
679    {
680        addr &= 0x0F;
681        d8 &= 0xFF;
682
683        // Hardware only decodes writes to $C0n0.  $C0n1-$C0nF are ignored.
684        if(addr!=0x00)
685            return 0x00;
686
687        var nextDataIn = d8 & 0x01;
688        var nextClk    = (d8 >> 1) & 0x01;
689        var nextStb    = (d8 >> 2) & 0x01;
690        var nextCmd    = (d8 >> 3) & 0x07;
691
692        rtc.dataIn = nextDataIn;
693        rtc.commandPending = nextCmd;
694
695        /*
696         * On the physical card the command pins and DATA IN settle before STB
697         * and CLK are applied.  MAME models the same order: command/STB first,
698         * then CLK.  Preserve that ordering when both edges occur in one write.
699         */
700        if(!rtc.stb && nextStb)
701            rtcLatchCommand(nextCmd);
702
703        if(!rtc.clk && nextClk)
704            rtcShiftClockRising();
705
706        rtc.stb = nextStb;
707        rtc.clk = nextClk;
708        rtc.control = d8;
709
710        return 0x00;
711    };
712
713
714    this.readHostROM = function(addr,ctx)
715    {
716        /*
717         * $C800-$CFFF maps the complete 2 KiB 2716 ROM without banking.
718         */
719        var rel_addr;
720
721        if(ctx && Number.isFinite(ctx.rel_addr))
722            rel_addr = ctx.rel_addr;
723        else if(ctx && Number.isFinite(ctx.line))
724            rel_addr = ctx.line + (addr & 0xFF);
725        else
726            rel_addr = (thunderclock.action.HostROM.base || 0x800) + (addr & 0xFF);
727
728        if(!ownsExpansionROM()) return 0x00;
729
730        var rom_addr = rel_addr - 0x800;
731        var d8 = rom_addr>=0 && rom_addr<0x800
732            ? THUNDERCLOCKPLUS_ROM[rom_addr]
733            : 0x00;
734
735        // Conventional Apple II C8-space release switch.
736        if((rel_addr & 0x0FFF)==0x0FFF && !(ctx && ctx.bRO===true))
737            releaseExpansionROM();
738
739        return d8 & 0xFF;
740    };
741
742
743    /*
744     * Peripheral controls: compact live RTC editor.
745     *
746     * This intentionally mirrors the RTC row used by EMU_CARD_serialpro.js:
747     *   - YYYY-MM-DD and HH:MM:SS fields
748     *   - optional live display refresh
749     *   - SET to apply the edited fields
750     *   - HOST to copy the browser host's local date/time
751     *
752     * UI operations rebase only the emulated calendar.  They do not change the
753     * uPD1990 command/mode/running state, so opening or editing the controls can
754     * never disturb a transfer currently being performed by card firmware.
755     */
756    var rtcUI =
757    {
758         "syncRequested":false
759        ,"controlID":""
760        ,"refreshEvent":""
761    };
762
763
764    function rtcUIPad2(value) { return String(value).padStart(2,"0"); }
765
766
767    function rtcUIValues()
768    {
769        var date = rtcCurrentDate();
770
771        return {
772             "date": date.getFullYear()
773                    + "-" + rtcUIPad2(date.getMonth()+1)
774                    + "-" + rtcUIPad2(date.getDate())
775            ,"time": rtcUIPad2(date.getHours())
776                    + ":" + rtcUIPad2(date.getMinutes())
777                    + ":" + rtcUIPad2(date.getSeconds())
778        };
779    }
780
781
782    function rtcUIElement(controlID,suffix)
783    {
784        return document.getElementById(controlID+"_"+suffix);
785    }
786
787
788    function rtcUIFieldValidity(element,valid)
789    {
790        if(element)
791            element.style.borderColor = valid ? "transparent" : "#D00000";
792    }
793
794
795    function rtcUIParse(controlID,markValidity)
796    {
797        var dateEl = rtcUIElement(controlID,"rtc_date");
798        var timeEl = rtcUIElement(controlID,"rtc_time");
799        if(!dateEl || !timeEl)
800            return {"valid":false,"dateValid":false,"timeValid":false,"date":null};
801
802        var dm = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateEl.value);
803        var tm = /^(\d{2}):(\d{2}):(\d{2})$/.exec(timeEl.value);
804
805        var dateValid = !!dm;
806        var timeValid = !!tm;
807        var year,month,day,hour,minute,second;
808
809        if(dateValid)
810        {
811            year  = Number(dm[1]);
812            month = Number(dm[2]);
813            day   = Number(dm[3]);
814
815            var calendar = new Date(0);
816            calendar.setFullYear(year,month-1,day);
817            calendar.setHours(12,0,0,0);
818
819            dateValid =
820                month>=1 && month<=12 &&
821                day>=1 && day<=31 &&
822                calendar.getFullYear()==year &&
823                calendar.getMonth()==month-1 &&
824                calendar.getDate()==day;
825        }
826
827        if(timeValid)
828        {
829            hour   = Number(tm[1]);
830            minute = Number(tm[2]);
831            second = Number(tm[3]);
832
833            timeValid =
834                hour>=0 && hour<=23 &&
835                minute>=0 && minute<=59 &&
836                second>=0 && second<=59;
837        }
838
839        if(markValidity)
840        {
841            rtcUIFieldValidity(dateEl,dateValid);
842            rtcUIFieldValidity(timeEl,timeValid);
843        }
844
845        if(!dateValid || !timeValid)
846            return {
847                 "valid":false
848                ,"dateValid":dateValid
849                ,"timeValid":timeValid
850                ,"date":null
851            };
852
853        var date = new Date(0);
854        date.setFullYear(year,month-1,day);
855        date.setHours(hour,minute,second,0);
856
857        // Reject values JavaScript normalized, including impossible calendar
858        // dates and non-existent local wall-clock times during DST transitions.
859        var valid =
860            date.getFullYear()==year &&
861            date.getMonth()==month-1 &&
862            date.getDate()==day &&
863            date.getHours()==hour &&
864            date.getMinutes()==minute &&
865            date.getSeconds()==second;
866
867        if(markValidity && !valid)
868        {
869            rtcUIFieldValidity(dateEl,false);
870            rtcUIFieldValidity(timeEl,false);
871        }
872
873        return {
874             "valid":valid
875            ,"dateValid":valid && dateValid
876            ,"timeValid":valid && timeValid
877            ,"date":valid ? date : null
878        };
879    }
880
881
882    function rtcUIUpdateSyncButton()
883    {
884        if(!rtcUI.controlID) return false;
885
886        var monitor = rtcUIElement(rtcUI.controlID,"rtc_monitoring");
887        if(!monitor) return false;
888
889        var active = rtcUI.syncRequested;
890
891        oCOM.POPUP.set_class(
892             monitor
893            ,"fa-stop-circle"
894            ,"fa-sync-alt"
895            ,active
896        );
897        monitor.title = active ? "Stop date/time sync" : "Start date/time sync";
898        return active;
899    }
900
901
902    function rtcUIUpdateSetButton(controlID,valid)
903    {
904        var setButton = rtcUIElement(controlID,"rtc_set");
905        if(setButton) setButton.disabled = !valid;
906        return !!valid;
907    }
908
909
910    function rtcUIRefresh(controlID,preserveFocus)
911    {
912        var values = rtcUIValues();
913        var dateEl = rtcUIElement(controlID,"rtc_date");
914        var timeEl = rtcUIElement(controlID,"rtc_time");
915        var activeEl = document.activeElement;
916
917        if(dateEl && (!preserveFocus || activeEl!==dateEl))
918            dateEl.value = values.date;
919
920        if(timeEl && (!preserveFocus || activeEl!==timeEl))
921            timeEl.value = values.time;
922
923        return true;
924    }
925
926
927    this.deviceToolRTCInput = function(controlID)
928    {
929        rtcUI.controlID = controlID;
930
931        var parsed = rtcUIParse(controlID,true);
932        rtcUIUpdateSetButton(controlID,parsed.valid);
933
934        return parsed.valid;
935    };
936
937
938    this.deviceToolRTCSyncToggle = function(controlID)
939    {
940        rtcUI.controlID = controlID;
941        rtcUI.syncRequested = !rtcUI.syncRequested;
942
943        var active = rtcUIUpdateSyncButton();
944
945        if(active)
946        {
947            rtcUIRefresh(controlID,true);
948            var parsed = rtcUIParse(controlID,true);
949            rtcUIUpdateSetButton(controlID,parsed.valid);
950        }
951
952        return active;
953    };
954
955
956    this.deviceToolRTCMonitoring = function()
957    {
958        if(!rtcUI.controlID || !rtcUI.syncRequested) return false;
959
960        // As on Serial Pro, never rewrite the focused field while live refresh
961        // is active; this preserves cursor position while the user edits it.
962        rtcUIRefresh(rtcUI.controlID,true);
963
964        var parsed = rtcUIParse(rtcUI.controlID,true);
965        rtcUIUpdateSetButton(rtcUI.controlID,parsed.valid);
966        return true;
967    };
968
969
970    this.deviceToolRTCSet = function(controlID)
971    {
972        rtcUI.controlID = controlID;
973
974        var parsed = rtcUIParse(controlID,true);
975        rtcUIUpdateSetButton(controlID,parsed.valid);
976
977        if(!parsed.valid) return false;
978
979        rtc.weekdayOffset = 0;
980        rtcRebase(parsed.date);
981        rtcUIRefresh(controlID,false);
982
983        rtcUIFieldValidity(rtcUIElement(controlID,"rtc_date"),true);
984        rtcUIFieldValidity(rtcUIElement(controlID,"rtc_time"),true);
985        rtcUIUpdateSetButton(controlID,true);
986        return true;
987    };
988
989
990    this.deviceToolRTCHost = function(controlID)
991    {
992        rtcUI.controlID = controlID;
993        rtc.weekdayOffset = 0;
994        rtcRebase(new Date());
995        rtcUIRefresh(controlID,false);
996
997        rtcUIFieldValidity(rtcUIElement(controlID,"rtc_date"),true);
998        rtcUIFieldValidity(rtcUIElement(controlID,"rtc_time"),true);
999        rtcUIUpdateSetButton(controlID,true);
1000        return true;
1001    };
1002
1003
1004    this.deviceToolSlotHTML = function(ctx)
1005    {
1006        ctx = ctx || {};
1007
1008        var toolboxID = ctx.toolboxID || ("device_tool_"+ctx.slotID);
1009        var slotID = ctx.slotID==null ? "?" : String(ctx.slotID);
1010        var slotN = Number(ctx.slotN);
1011        var values = rtcUIValues();
1012
1013        // Child controls must not use the "device_tool_" prefix: Apple2IO
1014        // reserves it for complete toolbox containers.
1015        var controlID = "tclkp_ctrl_"+slotID;
1016        var call = "apple2plus.hwObj().io.SLOT2obj("+slotN+")";
1017
1018        rtcUI.controlID = controlID;
1019        rtcUI.refreshEvent = "TCLKP_RTC_monitoring_"+slotID;
1020
1021        // Use the same dashboard refresh sequencer as Serial Pro.  The callback
1022        // remains registered; syncRequested decides whether it updates the DOM.
1023        oCOM.addRefreshEvent(
1024            function(){ thunderclock.deviceToolRTCMonitoring(); },
1025            rtcUI.refreshEvent,
1026            true
1027        );
1028
1029        var syncClass = rtcUI.syncRequested ? "fa-stop-circle" : "fa-sync-alt";
1030
1031        return ""
1032            + "<div class=toolbox id=\""+toolboxID+"\" hidden>"
1033            + " <div class=appbox style=\"box-sizing:border-box;text-align:left;height:30px;padding:3px 6px;display:flex;flex-direction:column;gap:2px;font-size:11px;white-space:nowrap;\">"
1034            + "  <div style=\"height:21px;
1034display:flex;align-items:center;gap:4px;\">"
1035            + "   <b style=\"display:inline-block;width:28px;\">RTC</b>"
1036            + "   <input id=\""+controlID+"_rtc_date\" type=text value=\""+values.date+"\" maxlength=10"
1037            + "          title=\"Date YYYY-MM-DD; edit and press SET\""
1038            + "          oninput=\""+call+".deviceToolRTCInput('"+controlID+"')\""
1039            + "          style=\"box-sizing:border-box;width:76px;height:19px;font:11px monospace;padding:0px 2px;border:1px solid transparent;\">"
1040            + "   <input id=\""+controlID+"_rtc_time\" type=text value=\""+values.time+"\" maxlength=8"
1041            + "          title=\"Time HH:MM:SS; edit and press SET\""
1042            + "          oninput=\""+call+".deviceToolRTCInput('"+controlID+"')\""
1043            + "          style=\"box-sizing:border-box;width:62px;height:19px;font:11px monospace;padding:0px 2px;border:1px solid transparent;\">"
1044            + "   <button class=\"appbut\" type=button style=\"text-align:center;margin-left:0px;padding:4px\" title=\"Date/time sync\">"
1045            + "     <i class=\"fa "+syncClass+"\" id=\""+controlID+"_rtc_monitoring\""
1046            + "        title=\""+(rtcUI.syncRequested ? "Stop date/time sync" : "Start date/time sync")+"\""
1047            + "        onclick=\""+call+".deviceToolRTCSyncToggle('"+controlID+"')\"></i>"
1048            + "   </button>"
1049            + "   <button class=\"appbut skinny\" id=\""+controlID+"_rtc_set\" type=button title=\"Set RTC from fields\""
1050            + "           onclick=\""+call+".deviceToolRTCSet('"+controlID+"')\">SET</button>"
1051            + "   <button class=\"appbut skinny\" type=button title=\"Set RTC from browser host time\""
1052            + "           onclick=\""+call+".deviceToolRTCHost('"+controlID+"')\">HOST</button>"
1053            + "  </div>"
1054            + " </div>"
1055            + "</div>";
1056    };
1057
1058
1059    /*
1060     * Diagnostic helper.  It deliberately exposes a copy rather than the live
1061     * mutable RTC arrays/state, following the card encapsulation pattern used
1062     * elsewhere in RetroAppleJS.
1063     */
1064    this.getRTCState = function()
1065    {
1066        var date = rtcCurrentDate();
1067
1068        return {
1069             "date":new Date(date.getTime())
1070            ,"running":rtc.running
1071            ,"mode":rtc.mode
1072            ,"control":rtc.control & 0xFF
1073            ,"dataIn":rtc.dataIn & 1
1074            ,"dataOut":rtcReadDataOut() & 1
1075            ,"tpRate":rtc.tpRate
1076            ,"testMode":rtc.testMode
1077            ,"shift":Array.from(rtc.shift)
1078        };
1079    };
1080
1081
1082    function resetInterface()
1083    {
1084        releaseExpansionROM();
1085
1086        /*
1087         * RESET affects the Apple-side latch/interface, not the battery-backed
1088         * calendar.  Resume the counter and return the external control lines to
1089         * a benign low state while preserving the current date/time.
1090         */
1091        rtcSetRunning(true);
1092        rtc.mode = RTC_REGISTER_HOLD;
1093        rtc.commandPending = RTC_REGISTER_HOLD;
1094        rtc.control = 0x00;
1095        rtc.dataIn = 0;
1096        rtc.dataOut = 0;
1097        rtc.clk = 0;
1098        rtc.stb = 0;
1099        rtc.testMode = false;
1100        rtc.tpRate = 64;
1101    }
1102
1103
1104    this.reset = function()
1105    {
1106        resetInterface();
1107    };
1108
1109
1110    this.restart = function()
1111    {
1112        resetInterface();
1113    };
1114}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.