ac6502 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 (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/components/CPU.js +1170 -0
  4. package/dist/components/CPU.js.map +1 -0
  5. package/dist/components/Cart.js +23 -0
  6. package/dist/components/Cart.js.map +1 -0
  7. package/dist/components/IO/Empty.js +19 -0
  8. package/dist/components/IO/Empty.js.map +1 -0
  9. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
  10. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
  11. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
  12. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
  13. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
  14. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
  15. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
  16. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
  17. package/dist/components/IO/GPIOCard.js +597 -0
  18. package/dist/components/IO/GPIOCard.js.map +1 -0
  19. package/dist/components/IO/InputBoard.js +19 -0
  20. package/dist/components/IO/InputBoard.js.map +1 -0
  21. package/dist/components/IO/LCDCard.js +19 -0
  22. package/dist/components/IO/LCDCard.js.map +1 -0
  23. package/dist/components/IO/RAMCard.js +63 -0
  24. package/dist/components/IO/RAMCard.js.map +1 -0
  25. package/dist/components/IO/RTCCard.js +483 -0
  26. package/dist/components/IO/RTCCard.js.map +1 -0
  27. package/dist/components/IO/SerialCard.js +282 -0
  28. package/dist/components/IO/SerialCard.js.map +1 -0
  29. package/dist/components/IO/SoundCard.js +620 -0
  30. package/dist/components/IO/SoundCard.js.map +1 -0
  31. package/dist/components/IO/StorageCard.js +428 -0
  32. package/dist/components/IO/StorageCard.js.map +1 -0
  33. package/dist/components/IO/VGACard.js +9 -0
  34. package/dist/components/IO/VGACard.js.map +1 -0
  35. package/dist/components/IO/VideoCard.js +623 -0
  36. package/dist/components/IO/VideoCard.js.map +1 -0
  37. package/dist/components/IO.js +3 -0
  38. package/dist/components/IO.js.map +1 -0
  39. package/dist/components/Machine.js +310 -0
  40. package/dist/components/Machine.js.map +1 -0
  41. package/dist/components/RAM.js +24 -0
  42. package/dist/components/RAM.js.map +1 -0
  43. package/dist/components/ROM.js +23 -0
  44. package/dist/components/ROM.js.map +1 -0
  45. package/dist/index.js +441 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/tests/CPU.test.js +1626 -0
  48. package/dist/tests/CPU.test.js.map +1 -0
  49. package/dist/tests/Cart.test.js +119 -0
  50. package/dist/tests/Cart.test.js.map +1 -0
  51. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
  52. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
  53. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
  54. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
  55. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
  56. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
  57. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
  58. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
  59. package/dist/tests/IO/GPIOCard.test.js +503 -0
  60. package/dist/tests/IO/GPIOCard.test.js.map +1 -0
  61. package/dist/tests/IO/RAMCard.test.js +229 -0
  62. package/dist/tests/IO/RAMCard.test.js.map +1 -0
  63. package/dist/tests/IO/RTCCard.test.js +177 -0
  64. package/dist/tests/IO/RTCCard.test.js.map +1 -0
  65. package/dist/tests/IO/SerialCard.test.js +423 -0
  66. package/dist/tests/IO/SerialCard.test.js.map +1 -0
  67. package/dist/tests/IO/SoundCard.test.js +528 -0
  68. package/dist/tests/IO/SoundCard.test.js.map +1 -0
  69. package/dist/tests/IO/StorageCard.test.js +647 -0
  70. package/dist/tests/IO/StorageCard.test.js.map +1 -0
  71. package/dist/tests/IO/VideoCard.test.js +549 -0
  72. package/dist/tests/IO/VideoCard.test.js.map +1 -0
  73. package/dist/tests/Machine.test.js +383 -0
  74. package/dist/tests/Machine.test.js.map +1 -0
  75. package/dist/tests/RAM.test.js +160 -0
  76. package/dist/tests/RAM.test.js.map +1 -0
  77. package/dist/tests/ROM.test.js +123 -0
  78. package/dist/tests/ROM.test.js.map +1 -0
  79. package/jest.config.cjs +9 -0
  80. package/package.json +43 -0
  81. package/src/components/CPU.ts +1371 -0
  82. package/src/components/Cart.ts +20 -0
  83. package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
  84. package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
  85. package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
  86. package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
  87. package/src/components/IO/GPIOCard.ts +677 -0
  88. package/src/components/IO/RAMCard.ts +68 -0
  89. package/src/components/IO/RTCCard.ts +518 -0
  90. package/src/components/IO/SerialCard.ts +335 -0
  91. package/src/components/IO/SoundCard.ts +711 -0
  92. package/src/components/IO/StorageCard.ts +473 -0
  93. package/src/components/IO/VideoCard.ts +730 -0
  94. package/src/components/IO.ts +11 -0
  95. package/src/components/Machine.ts +364 -0
  96. package/src/components/RAM.ts +23 -0
  97. package/src/components/ROM.ts +19 -0
  98. package/src/index.ts +474 -0
  99. package/src/tests/CPU.test.ts +2045 -0
  100. package/src/tests/Cart.test.ts +149 -0
  101. package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
  102. package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
  103. package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
  104. package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
  105. package/src/tests/IO/GPIOCard.test.ts +644 -0
  106. package/src/tests/IO/RAMCard.test.ts +284 -0
  107. package/src/tests/IO/RTCCard.test.ts +222 -0
  108. package/src/tests/IO/SerialCard.test.ts +530 -0
  109. package/src/tests/IO/SoundCard.test.ts +659 -0
  110. package/src/tests/IO/StorageCard.test.ts +787 -0
  111. package/src/tests/IO/VideoCard.test.ts +668 -0
  112. package/src/tests/Machine.test.ts +437 -0
  113. package/src/tests/RAM.test.ts +196 -0
  114. package/src/tests/ROM.test.ts +154 -0
  115. package/tsconfig.json +12 -0
@@ -0,0 +1,465 @@
1
+ import { GPIOAttachmentBase } from './GPIOAttachment'
2
+
3
+ /**
4
+ * USB HID Keycode to ASCII mapping table
5
+ * Maps USB HID usage IDs to ASCII characters
6
+ */
7
+ const USB_HID_TO_ASCII: { [key: number]: number } = {
8
+ 0x04: 0x61, // a
9
+ 0x05: 0x62, // b
10
+ 0x06: 0x63, // c
11
+ 0x07: 0x64, // d
12
+ 0x08: 0x65, // e
13
+ 0x09: 0x66, // f
14
+ 0x0A: 0x67, // g
15
+ 0x0B: 0x68, // h
16
+ 0x0C: 0x69, // i
17
+ 0x0D: 0x6A, // j
18
+ 0x0E: 0x6B, // k
19
+ 0x0F: 0x6C, // l
20
+ 0x10: 0x6D, // m
21
+ 0x11: 0x6E, // n
22
+ 0x12: 0x6F, // o
23
+ 0x13: 0x70, // p
24
+ 0x14: 0x71, // q
25
+ 0x15: 0x72, // r
26
+ 0x16: 0x73, // s
27
+ 0x17: 0x74, // t
28
+ 0x18: 0x75, // u
29
+ 0x19: 0x76, // v
30
+ 0x1A: 0x77, // w
31
+ 0x1B: 0x78, // x
32
+ 0x1C: 0x79, // y
33
+ 0x1D: 0x7A, // z
34
+ 0x1E: 0x31, // 1
35
+ 0x1F: 0x32, // 2
36
+ 0x20: 0x33, // 3
37
+ 0x21: 0x34, // 4
38
+ 0x22: 0x35, // 5
39
+ 0x23: 0x36, // 6
40
+ 0x24: 0x37, // 7
41
+ 0x25: 0x38, // 8
42
+ 0x26: 0x39, // 9
43
+ 0x27: 0x30, // 0
44
+ 0x28: 0x0D, // Enter
45
+ 0x29: 0x1B, // Escape
46
+ 0x2A: 0x08, // Backspace
47
+ 0x2B: 0x09, // Tab
48
+ 0x2C: 0x20, // Space
49
+ 0x2D: 0x2D, // -
50
+ 0x2E: 0x3D, // =
51
+ 0x2F: 0x5B, // [
52
+ 0x30: 0x5D, // ]
53
+ 0x31: 0x5C, // backslash
54
+ 0x33: 0x3B, // ;
55
+ 0x34: 0x27, // '
56
+ 0x35: 0x60, // `
57
+ 0x36: 0x2C, // ,
58
+ 0x37: 0x2E, // .
59
+ 0x38: 0x2F, // /
60
+ 0x4C: 0x7F, // Delete
61
+ 0x4F: 0x1D, // Right Arrow
62
+ 0x50: 0x1C, // Left Arrow
63
+ 0x51: 0x1F, // Down Arrow
64
+ 0x52: 0x1E, // Up Arrow
65
+ 0x49: 0x1A, // Insert
66
+ }
67
+
68
+ /**
69
+ * GPIOKeyboardEncoderAttachment - Emulates a keyboard encoder that provides ASCII-encoded
70
+ * key data on both GPIO ports A and B.
71
+ *
72
+ * This attachment uses the VIA control lines to signal data availability:
73
+ * - CA2 LOW enables Port A
74
+ * - CB2 LOW enables Port B
75
+ * - CA1 interrupt signals data ready on Port A
76
+ * - CB1 interrupt signals data ready on Port B
77
+ *
78
+ * The encoder supports extensive modifier key combinations:
79
+ * - MENU key: 0x80 (alone), 0x90 (with Alt)
80
+ * - Function keys F1-F15: 0x81-0x8F (alone), 0x91-0x9F (with Alt)
81
+ * - Ctrl combinations: Control codes 0x00-0x1F
82
+ * - Alt+Shift: Extended character set 0xA0-0xFF
83
+ * - Alt: Extended character set 0xE0-0xFF
84
+ * - Shift: Uppercase letters and shifted symbols
85
+ */
86
+ export class GPIOKeyboardEncoderAttachment extends GPIOAttachmentBase {
87
+ // Port A state
88
+ private asciiDataA: number = 0x00
89
+ private dataReadyA: boolean = false
90
+ private interruptPendingA: boolean = false
91
+ private enabledA: boolean = false
92
+
93
+ // Port B state
94
+ private asciiDataB: number = 0x00
95
+ private dataReadyB: boolean = false
96
+ private interruptPendingB: boolean = false
97
+ private enabledB: boolean = false
98
+
99
+ // Modifier key states
100
+ private shiftPressed: boolean = false
101
+ private ctrlPressed: boolean = false
102
+ private altPressed: boolean = false
103
+ private menuPressed: boolean = false
104
+
105
+ // Control line states
106
+ private stateCA1: boolean = false
107
+ private stateCA2: boolean = false
108
+ private stateCB1: boolean = false
109
+ private stateCB2: boolean = false
110
+
111
+ constructor(priority: number = 0) {
112
+ // Uses CA1, CA2, CB1, CB2
113
+ super(priority, true, true, true, true)
114
+ this.reset()
115
+ }
116
+
117
+ reset(): void {
118
+ super.reset()
119
+ this.asciiDataA = 0x00
120
+ this.dataReadyA = false
121
+ this.interruptPendingA = false
122
+ this.enabledA = false
123
+ this.asciiDataB = 0x00
124
+ this.dataReadyB = false
125
+ this.interruptPendingB = false
126
+ this.enabledB = false
127
+ this.shiftPressed = false
128
+ this.ctrlPressed = false
129
+ this.altPressed = false
130
+ this.menuPressed = false
131
+ }
132
+
133
+ readPortA(ddrA: number, orA: number): number {
134
+ // Only provide data when enabled and data is ready
135
+ if (this.enabledA && this.dataReadyA) {
136
+ // Reading the port will clear data ready flag (done via clearInterrupts)
137
+ return this.asciiDataA
138
+ }
139
+
140
+ // No data to provide
141
+ return 0xFF
142
+ }
143
+
144
+ readPortB(ddrB: number, orB: number): number {
145
+ // Only provide data when enabled and data is ready
146
+ if (this.enabledB && this.dataReadyB) {
147
+ // Reading the port will clear data ready flag (done via clearInterrupts)
148
+ return this.asciiDataB
149
+ }
150
+
151
+ // No data to provide
152
+ return 0xFF
153
+ }
154
+
155
+ updateControlLines(ca1: boolean, ca2: boolean, cb1: boolean, cb2: boolean): void {
156
+ this.stateCA1 = ca1
157
+ this.stateCA2 = ca2
158
+ this.stateCB1 = cb1
159
+ this.stateCB2 = cb2
160
+
161
+ // Enabled when CA2 is LOW for Port A
162
+ this.enabledA = !ca2
163
+
164
+ // Enabled when CB2 is LOW for Port B
165
+ this.enabledB = !cb2
166
+ }
167
+
168
+ hasCA1Interrupt(): boolean {
169
+ return this.interruptPendingA && this.enabledA
170
+ }
171
+
172
+ hasCB1Interrupt(): boolean {
173
+ return this.interruptPendingB && this.enabledB
174
+ }
175
+
176
+ clearInterrupts(ca1: boolean, ca2: boolean, cb1: boolean, cb2: boolean): void {
177
+ if (ca1) {
178
+ this.interruptPendingA = false
179
+ this.dataReadyA = false // Clear data ready flag when Port A is read
180
+ }
181
+ if (cb1) {
182
+ this.interruptPendingB = false
183
+ this.dataReadyB = false // Clear data ready flag when Port B is read
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Map a USB HID keycode to ASCII with modifier keys applied
189
+ */
190
+ private mapKeyWithModifiers(usbHidKeycode: number): number {
191
+ // Handle MENU key (USB HID 0xE3 Left GUI, 0xE7 Right GUI)
192
+ if (usbHidKeycode === 0xE3 || usbHidKeycode === 0xE7) {
193
+ if (this.altPressed) {
194
+ return 0x90 // Alt+MENU
195
+ }
196
+ return 0x80 // MENU alone
197
+ }
198
+
199
+ // Handle function keys F1-F15 (USB HID 0x3A-0x45 for F1-F12, 0x68-0x6A for F13-F15)
200
+ // F1-F15 without Alt → 0x81-0x8F
201
+ // Alt+F1-F15 → 0x91-0x9F
202
+ if (usbHidKeycode >= 0x3A && usbHidKeycode <= 0x45) {
203
+ // F1-F12 (0x3A-0x45)
204
+ const fKeyOffset = usbHidKeycode - 0x3A // 0-11 for F1-F12
205
+ if (this.altPressed) {
206
+ return 0x91 + fKeyOffset // Alt+F1-F12 → 0x91-0x9C
207
+ }
208
+ return 0x81 + fKeyOffset // F1-F12 → 0x81-0x8C
209
+ }
210
+ if (usbHidKeycode >= 0x68 && usbHidKeycode <= 0x6A) {
211
+ // F13-F15 (0x68-0x6A)
212
+ const fKeyOffset = usbHidKeycode - 0x68 + 12 // 12-14 for F13-F15
213
+ if (this.altPressed) {
214
+ return 0x91 + fKeyOffset // Alt+F13-F15 → 0x9D-0x9F
215
+ }
216
+ return 0x81 + fKeyOffset // F13-F15 → 0x8D-0x8F
217
+ }
218
+
219
+ // Get base ASCII character from USB HID keycode
220
+ const baseChar = USB_HID_TO_ASCII[usbHidKeycode] || 0x00
221
+
222
+ // If no base mapping, return 0x00
223
+ if (baseChar === 0x00) {
224
+ return 0x00
225
+ }
226
+
227
+ // Handle Ctrl combinations - control codes
228
+ if (this.ctrlPressed && !this.altPressed) {
229
+ // Ctrl with letters produces control codes 0x01-0x1A
230
+ if (baseChar >= 0x61 && baseChar <= 0x7A) { // a-z
231
+ return baseChar - 0x61 + 0x01
232
+ }
233
+ if (baseChar >= 0x41 && baseChar <= 0x5A) { // A-Z
234
+ return baseChar - 0x41 + 0x01
235
+ }
236
+
237
+ // Ctrl with other special keys
238
+ switch (baseChar) {
239
+ case 0x32: case 0x40: return 0x00 // Ctrl+2 or Ctrl+@ = NUL
240
+ case 0x36: case 0x5E: return 0x1E // Ctrl+6 or Ctrl+^ = RS (UP arrow position)
241
+ case 0x2D: case 0x5F: return 0x1F // Ctrl+- or Ctrl+_ = US (DOWN arrow position)
242
+ case 0x5B: case 0x7B: return 0x1B // Ctrl+[ or Ctrl+{ = ESC
243
+ case 0x5C: case 0x7C: return 0x1C // Ctrl+\ or Ctrl+| = FS (LEFT arrow position)
244
+ case 0x5D: case 0x7D: return 0x1D // Ctrl+] or Ctrl+} = GS (RIGHT arrow position)
245
+ }
246
+ }
247
+
248
+ // Handle Alt+Shift combinations - extended character set
249
+ if (this.altPressed && this.shiftPressed) {
250
+ switch (baseChar) {
251
+ case 0x31: return 0xA1 // '1' -> ¡
252
+ case 0x27: return 0xA2 // '\'' -> ¢
253
+ case 0x33: return 0xA3 // '3' -> £
254
+ case 0x34: return 0xA4 // '4' -> ¤
255
+ case 0x35: return 0xA5 // '5' -> ¥
256
+ case 0x37: return 0xA6 // '7' -> ¦
257
+ case 0x39: return 0xA8 // '9' -> ¨
258
+ case 0x30: return 0xA9 // '0' -> ©
259
+ case 0x38: return 0xAA // '8' -> ª
260
+ case 0x3D: return 0xAB // '=' -> «
261
+ case 0x3B: return 0xBA // ';' -> º
262
+ case 0x2C: return 0xBC // ',' -> ¼
263
+ case 0x2E: return 0xBE // '.' -> ¾
264
+ case 0x2F: return 0xBF // '/' -> ¿
265
+ case 0x32: return 0xC0 // '2' -> À
266
+ case 0x61: case 0x41: return 0xC1 // 'a'/'A' -> Á
267
+ case 0x62: case 0x42: return 0xC2 // 'b'/'B' -> Â
268
+ case 0x63: case 0x43: return 0xC3 // 'c'/'C' -> Ã
269
+ case 0x64: case 0x44: return 0xC4 // 'd'/'D' -> Ä
270
+ case 0x65: case 0x45: return 0xC5 // 'e'/'E' -> Å
271
+ case 0x66: case 0x46: return 0xC6 // 'f'/'F' -> Æ
272
+ case 0x67: case 0x47: return 0xC7 // 'g'/'G' -> Ç
273
+ case 0x68: case 0x48: return 0xC8 // 'h'/'H' -> È
274
+ case 0x69: case 0x49: return 0xC9 // 'i'/'I' -> É
275
+ case 0x6A: case 0x4A: return 0xCA // 'j'/'J' -> Ê
276
+ case 0x6B: case 0x4B: return 0xCB // 'k'/'K' -> Ë
277
+ case 0x6C: case 0x4C: return 0xCC // 'l'/'L' -> Ì
278
+ case 0x6D: case 0x4D: return 0xCD // 'm'/'M' -> Í
279
+ case 0x6E: case 0x4E: return 0xCE // 'n'/'N' -> Î
280
+ case 0x6F: case 0x4F: return 0xCF // 'o'/'O' -> Ï
281
+ case 0x70: case 0x50: return 0xD0 // 'p'/'P' -> Ð
282
+ case 0x71: case 0x51: return 0xD1 // 'q'/'Q' -> Ñ
283
+ case 0x72: case 0x52: return 0xD2 // 'r'/'R' -> Ò
284
+ case 0x73: case 0x53: return 0xD3 // 's'/'S' -> Ó
285
+ case 0x74: case 0x54: return 0xD4 // 't'/'T' -> Ô
286
+ case 0x75: case 0x55: return 0xD5 // 'u'/'U' -> Õ
287
+ case 0x76: case 0x56: return 0xD6 // 'v'/'V' -> Ö
288
+ case 0x77: case 0x57: return 0xD7 // 'w'/'W' -> ×
289
+ case 0x78: case 0x58: return 0xD8 // 'x'/'X' -> Ø
290
+ case 0x79: case 0x59: return 0xD9 // 'y'/'Y' -> Ù
291
+ case 0x7A: case 0x5A: return 0xDA // 'z'/'Z' -> Ú
292
+ case 0x5B: return 0xFB // '[' -> û
293
+ case 0x5C: return 0xFC // '\\' -> ü
294
+ case 0x5D: return 0xFD // ']' -> ý
295
+ case 0x60: return 0xFE // '`' -> þ
296
+ case 0x36: return 0xDE // '6' -> Þ
297
+ case 0x2D: return 0xDF // '-' -> ß
298
+ }
299
+ }
300
+
301
+ // Handle Alt combinations (without shift)
302
+ if (this.altPressed && !this.shiftPressed) {
303
+ switch (baseChar) {
304
+ case 0x20: return 0xA0 // Space -> nbsp
305
+ case 0x27: return 0xA7 // '\'' -> §
306
+ case 0x2C: return 0xAC // ',' -> ¬
307
+ case 0x2D: return 0xAD // '-' -> soft hyphen
308
+ case 0x2E: return 0xAE // '.' -> ®
309
+ case 0x2F: return 0xAF // '/' -> ¯
310
+ case 0x30: return 0xB0 // '0' -> °
311
+ case 0x31: return 0xB1 // '1' -> ±
312
+ case 0x32: return 0xB2 // '2' -> ²
313
+ case 0x33: return 0xB3 // '3' -> ³
314
+ case 0x34: return 0xB4 // '4' -> ´
315
+ case 0x35: return 0xB5 // '5' -> µ
316
+ case 0x36: return 0xB6 // '6' -> ¶
317
+ case 0x37: return 0xB7 // '7' -> ·
318
+ case 0x38: return 0xB8 // '8' -> ¸
319
+ case 0x39: return 0xB9 // '9' -> ¹
320
+ case 0x3B: return 0xBB // ';' -> »
321
+ case 0x3D: return 0xBD // '=' -> ½
322
+ case 0x5B: return 0xDB // '[' -> Û
323
+ case 0x5C: return 0xDC // '\\' -> Ü
324
+ case 0x5D: return 0xDD // ']' -> Ý
325
+ case 0x60: return 0xE0 // '`' -> à
326
+ case 0x61: case 0x41: return 0xE1 // 'a'/'A' -> á
327
+ case 0x62: case 0x42: return 0xE2 // 'b'/'B' -> â
328
+ case 0x63: case 0x43: return 0xE3 // 'c'/'C' -> ã
329
+ case 0x64: case 0x44: return 0xE4 // 'd'/'D' -> ä
330
+ case 0x65: case 0x45: return 0xE5 // 'e'/'E' -> å
331
+ case 0x66: case 0x46: return 0xE6 // 'f'/'F' -> æ
332
+ case 0x67: case 0x47: return 0xE7 // 'g'/'G' -> ç
333
+ case 0x68: case 0x48: return 0xE8 // 'h'/'H' -> è
334
+ case 0x69: case 0x49: return 0xE9 // 'i'/'I' -> é
335
+ case 0x6A: case 0x4A: return 0xEA // 'j'/'J' -> ê
336
+ case 0x6B: case 0x4B: return 0xEB // 'k'/'K' -> ë
337
+ case 0x6C: case 0x4C: return 0xEC // 'l'/'L' -> ì
338
+ case 0x6D: case 0x4D: return 0xED // 'm'/'M' -> í
339
+ case 0x6E: case 0x4E: return 0xEE // 'n'/'N' -> î
340
+ case 0x6F: case 0x4F: return 0xEF // 'o'/'O' -> ï
341
+ case 0x70: case 0x50: return 0xF0 // 'p'/'P' -> ð
342
+ case 0x71: case 0x51: return 0xF1 // 'q'/'Q' -> ñ
343
+ case 0x72: case 0x52: return 0xF2 // 'r'/'R' -> ò
344
+ case 0x73: case 0x53: return 0xF3 // 's'/'S' -> ó
345
+ case 0x74: case 0x54: return 0xF4 // 't'/'T' -> ô
346
+ case 0x75: case 0x55: return 0xF5 // 'u'/'U' -> õ
347
+ case 0x76: case 0x56: return 0xF6 // 'v'/'V' -> ö
348
+ case 0x77: case 0x57: return 0xF7 // 'w'/'W' -> ÷
349
+ case 0x78: case 0x58: return 0xF8 // 'x'/'X' -> ø
350
+ case 0x79: case 0x59: return 0xF9 // 'y'/'Y' -> ù
351
+ case 0x7A: case 0x5A: return 0xFA // 'z'/'Z' -> ú
352
+ case 0x7F: return 0xFF // DEL -> ÿ
353
+ }
354
+ }
355
+
356
+ // Handle Shift combinations - uppercase and shifted symbols
357
+ if (this.shiftPressed && !this.ctrlPressed && !this.altPressed) {
358
+ // Letters become uppercase
359
+ if (baseChar >= 0x61 && baseChar <= 0x7A) { // a-z
360
+ return baseChar - 0x61 + 0x41 // A-Z
361
+ }
362
+
363
+ // Shifted symbols
364
+ switch (baseChar) {
365
+ case 0x31: return 0x21 // '1' -> '!'
366
+ case 0x32: return 0x40 // '2' -> '@'
367
+ case 0x33: return 0x23 // '3' -> '#'
368
+ case 0x34: return 0x24 // '4' -> '$'
369
+ case 0x35: return 0x25 // '5' -> '%'
370
+ case 0x36: return 0x5E // '6' -> '^'
371
+ case 0x37: return 0x26 // '7' -> '&'
372
+ case 0x38: return 0x2A // '8' -> '*'
373
+ case 0x39: return 0x28 // '9' -> '('
374
+ case 0x30: return 0x29 // '0' -> ')'
375
+ case 0x2D: return 0x5F // '-' -> '_'
376
+ case 0x3D: return 0x2B // '=' -> '+'
377
+ case 0x5B: return 0x7B // '[' -> '{'
378
+ case 0x5D: return 0x7D // ']' -> '}'
379
+ case 0x5C: return 0x7C // '\\' -> '|'
380
+ case 0x3B: return 0x3A // ';' -> ':'
381
+ case 0x27: return 0x22 // '\'' -> '"'
382
+ case 0x2C: return 0x3C // ',' -> '<'
383
+ case 0x2E: return 0x3E // '.' -> '>'
384
+ case 0x2F: return 0x3F // '/' -> '?'
385
+ case 0x60: return 0x7E // '`' -> '~'
386
+ }
387
+ }
388
+
389
+ // No modifiers or unhandled combination - return base character
390
+ return baseChar
391
+ }
392
+
393
+ /**
394
+ * Update the keyboard state based on a USB HID key press or release
395
+ * @param usbHidKeycode USB HID keycode
396
+ * @param pressed true for key press, false for key release
397
+ */
398
+ updateKey(usbHidKeycode: number, pressed: boolean): void {
399
+ // Handle modifier keys - update state
400
+ switch (usbHidKeycode) {
401
+ case 0xE0: // Left Ctrl
402
+ case 0xE4: // Right Ctrl
403
+ this.ctrlPressed = pressed
404
+ return // Don't generate output for modifier keys alone
405
+
406
+ case 0xE1: // Left Shift
407
+ case 0xE5: // Right Shift
408
+ this.shiftPressed = pressed
409
+ return
410
+
411
+ case 0xE2: // Left Alt
412
+ case 0xE6: // Right Alt
413
+ this.altPressed = pressed
414
+ return
415
+
416
+ case 0xE3: // Left GUI (MENU)
417
+ case 0xE7: // Right GUI (MENU)
418
+ this.menuPressed = pressed
419
+ // MENU key generates output, so don't return - fall through
420
+ break
421
+ }
422
+
423
+ // Only process key presses, not releases (encoder only reports keypress events)
424
+ if (!pressed) {
425
+ return
426
+ }
427
+
428
+ // Map the key with active modifiers
429
+ const mappedValue = this.mapKeyWithModifiers(usbHidKeycode)
430
+
431
+ // Ignore keys with no mapping (0x00 unless it's a valid control code like Ctrl+2 = NUL)
432
+ // Valid 0x00: Ctrl+2, Ctrl+@
433
+ if (mappedValue === 0x00 && !this.ctrlPressed) {
434
+ return
435
+ }
436
+
437
+ // Update both ports with the mapped data
438
+ this.asciiDataA = mappedValue
439
+ this.asciiDataB = mappedValue
440
+ this.dataReadyA = true
441
+ this.dataReadyB = true
442
+
443
+ // Trigger interrupts on both ports if enabled
444
+ if (this.enabledA) {
445
+ this.interruptPendingA = true
446
+ }
447
+ if (this.enabledB) {
448
+ this.interruptPendingB = true
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Check if Port A has data ready
454
+ */
455
+ hasDataReadyA(): boolean {
456
+ return this.dataReadyA && this.enabledA
457
+ }
458
+
459
+ /**
460
+ * Check if Port B has data ready
461
+ */
462
+ hasDataReadyB(): boolean {
463
+ return this.dataReadyB && this.enabledB
464
+ }
465
+ }