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.