ac6502 1.0.0 → 1.1.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 (32) hide show
  1. package/README.md +2 -2
  2. package/dist/components/IO/EmptyCard.js +17 -0
  3. package/dist/components/IO/EmptyCard.js.map +1 -0
  4. package/dist/components/IO/GPIOAttachments/GPIOKeypadAttachment.js +141 -0
  5. package/dist/components/IO/GPIOAttachments/GPIOKeypadAttachment.js.map +1 -0
  6. package/dist/components/IO/GPIOAttachments/GPIOLCDAttachment.js +716 -0
  7. package/dist/components/IO/GPIOAttachments/GPIOLCDAttachment.js.map +1 -0
  8. package/dist/components/IO/SerialCard.js +4 -4
  9. package/dist/components/IO/SerialCard.js.map +1 -1
  10. package/dist/components/Machine.js +84 -45
  11. package/dist/components/Machine.js.map +1 -1
  12. package/dist/index.js +175 -52
  13. package/dist/index.js.map +1 -1
  14. package/dist/tests/IO/GPIOAttachments/GPIOKeypadAttachment.test.js +323 -0
  15. package/dist/tests/IO/GPIOAttachments/GPIOKeypadAttachment.test.js.map +1 -0
  16. package/dist/tests/IO/GPIOAttachments/GPIOLCDAttachment.test.js +627 -0
  17. package/dist/tests/IO/GPIOAttachments/GPIOLCDAttachment.test.js.map +1 -0
  18. package/dist/tests/IO/SerialCard.test.js +4 -4
  19. package/dist/tests/IO/SerialCard.test.js.map +1 -1
  20. package/dist/tests/Machine.test.js +9 -3
  21. package/dist/tests/Machine.test.js.map +1 -1
  22. package/package.json +3 -3
  23. package/src/components/IO/EmptyCard.ts +16 -0
  24. package/src/components/IO/GPIOAttachments/GPIOKeypadAttachment.ts +153 -0
  25. package/src/components/IO/GPIOAttachments/GPIOLCDAttachment.ts +791 -0
  26. package/src/components/IO/SerialCard.ts +4 -4
  27. package/src/components/Machine.ts +107 -61
  28. package/src/index.ts +179 -87
  29. package/src/tests/IO/GPIOAttachments/GPIOKeypadAttachment.test.ts +389 -0
  30. package/src/tests/IO/GPIOAttachments/GPIOLCDAttachment.test.ts +795 -0
  31. package/src/tests/IO/SerialCard.test.ts +4 -4
  32. package/src/tests/Machine.test.ts +10 -3
@@ -49,11 +49,11 @@ export class SerialCard implements IO {
49
49
  case 0x01: // Status Register
50
50
  return this.readStatus()
51
51
 
52
- case 0x02: // Command Register (write-only, returns 0)
53
- return 0
52
+ case 0x02: // Command Register
53
+ return this.commandRegister
54
54
 
55
- case 0x03: // Control Register (write-only, returns 0)
56
- return 0
55
+ case 0x03: // Control Register
56
+ return this.controlRegister
57
57
 
58
58
  default:
59
59
  return 0
@@ -12,6 +12,10 @@ import { VideoCard } from './IO/VideoCard'
12
12
  import { GPIOKeyboardMatrixAttachment } from './IO/GPIOAttachments/GPIOKeyboardMatrixAttachment'
13
13
  import { GPIOKeyboardEncoderAttachment } from './IO/GPIOAttachments/GPIOKeyboardEncoderAttachment'
14
14
  import { GPIOJoystickAttachment } from './IO/GPIOAttachments/GPIOJoystickAttachment'
15
+ import { GPIOLCDAttachment } from './IO/GPIOAttachments/GPIOLCDAttachment'
16
+ import { GPIOKeypadAttachment } from './IO/GPIOAttachments/GPIOKeypadAttachment'
17
+ import { EmptyCard } from './IO/EmptyCard'
18
+ import { IO } from './IO'
15
19
  import { readFile } from 'fs/promises'
16
20
 
17
21
  export class Machine {
@@ -25,21 +29,27 @@ export class Machine {
25
29
  cpu: CPU
26
30
  ram: RAM
27
31
  rom: ROM
28
- io1: RAMCard
29
- io2: RAMCard
30
- io3: RTCCard
31
- io4: StorageCard
32
+ io1: IO
33
+ io2: IO
34
+ io3: IO
35
+ io4: IO
32
36
  io5: SerialCard
33
- io6: GPIOCard
34
- io7: SoundCard
35
- io8: VideoCard
37
+ io6: IO
38
+ io7: IO
39
+ io8: IO
36
40
 
37
41
  cart?: Cart
42
+ kim: boolean
38
43
 
39
44
  // GPIO Attachments
40
45
  keyboardMatrixAttachment: GPIOKeyboardMatrixAttachment
41
46
  keyboardEncoderAttachment: GPIOKeyboardEncoderAttachment
42
- joystickAttachment: GPIOJoystickAttachment
47
+ joystickAttachmentA: GPIOJoystickAttachment
48
+ joystickAttachmentB: GPIOJoystickAttachment
49
+
50
+ // KIM mode attachments
51
+ lcdAttachment?: GPIOLCDAttachment
52
+ keypadAttachment?: GPIOKeypadAttachment
43
53
 
44
54
  isAlive: boolean = false
45
55
  isRunning: boolean = false
@@ -52,73 +62,101 @@ export class Machine {
52
62
  previousTime: number = performance.now()
53
63
 
54
64
  transmit?: (data: number) => void
55
- render?: (buffer: Buffer<ArrayBufferLike>) => void
65
+ render?: () => void
56
66
  pushAudioSamples?: (samples: Float32Array) => void
57
67
 
58
68
  //
59
69
  // Initialization
60
70
  //
61
71
 
62
- constructor() {
72
+ constructor(kim: boolean = false) {
73
+ this.kim = kim
63
74
  this.cpu = new CPU(this.read.bind(this), this.write.bind(this))
64
75
  this.ram = new RAM()
65
76
  this.rom = new ROM()
66
- this.io1 = new RAMCard()
67
- this.io2 = new RAMCard()
68
- this.io3 = new RTCCard()
69
- this.io4 = new StorageCard()
70
- this.io5 = new SerialCard()
71
- this.io6 = new GPIOCard()
72
- this.io7 = new SoundCard()
73
- this.io8 = new VideoCard()
74
77
 
75
- // Connect RTCCard IRQ/NMI to CPU
76
- this.io3.raiseIRQ = () => this.cpu.irq()
77
- this.io3.raiseNMI = () => this.cpu.nmi()
78
+ this.io5 = new SerialCard()
78
79
 
79
80
  // Connect SerialCard IRQ/NMI to CPU
80
81
  this.io5.raiseIRQ = () => this.cpu.irq()
81
82
  this.io5.raiseNMI = () => this.cpu.nmi()
82
83
 
83
- // Connect SerialCard transmit callback (use arrow function to look up this.transmit at call time)
84
+ // Connect SerialCard transmit callback
84
85
  this.io5.transmit = (data: number) => {
85
86
  if (this.transmit) {
86
87
  this.transmit(data)
87
88
  }
88
89
  }
89
90
 
90
- // Connect VideoCard IRQ/NMI to CPU
91
- this.io8.raiseIRQ = () => this.cpu.irq()
92
- this.io8.raiseNMI = () => this.cpu.nmi()
93
-
94
- // Connect SoundCard pushSamples callback (use arrow function to look up this.pushAudioSamples at call time)
95
- this.io7.pushSamples = (samples: Float32Array) => {
96
- if (this.pushAudioSamples) {
97
- this.pushAudioSamples(samples)
98
- }
99
- }
100
-
101
- // Create GPIO Attachments
102
- // Keyboard matrix (manual scanning) - highest priority for Port A rows (priority 10)
91
+ // Always create standard GPIO attachments (for type stability)
103
92
  this.keyboardMatrixAttachment = new GPIOKeyboardMatrixAttachment(10)
104
-
105
- // Keyboard encoder (ASCII on both Port A and Port B) - medium priority (priority 20)
106
93
  this.keyboardEncoderAttachment = new GPIOKeyboardEncoderAttachment(20)
107
-
108
- // Joystick (Port B) - lowest priority, fallback (priority 100)
109
- this.joystickAttachment = new GPIOJoystickAttachment(false, 100)
94
+ this.joystickAttachmentA = new GPIOJoystickAttachment(false, 100)
95
+ this.joystickAttachmentB = new GPIOJoystickAttachment(false, 100)
96
+
97
+ if (kim) {
98
+ this.io1 = new EmptyCard()
99
+ this.io2 = new EmptyCard()
100
+ this.io3 = new EmptyCard()
101
+ this.io4 = new EmptyCard()
102
+ this.io6 = new EmptyCard()
103
+ this.io7 = new EmptyCard()
104
+
105
+ const gpioCard = new GPIOCard()
106
+ this.io8 = gpioCard
107
+
108
+ // Connect GPIOCard IRQ/NMI to CPU
109
+ gpioCard.raiseIRQ = () => this.cpu.irq()
110
+ gpioCard.raiseNMI = () => this.cpu.nmi()
111
+
112
+ // Create KIM GPIO Attachments
113
+ this.lcdAttachment = new GPIOLCDAttachment(16, 2, 10)
114
+ this.keypadAttachment = new GPIOKeypadAttachment(true, 20)
115
+
116
+ // Attach LCD to Port A (control: RS/RW/E on bits 5-7) and Port B (data bus)
117
+ gpioCard.attachToPortA(this.lcdAttachment)
118
+ gpioCard.attachToPortB(this.lcdAttachment)
119
+
120
+ // Attach keypad to Port A (bits 0-4)
121
+ gpioCard.attachToPortA(this.keypadAttachment)
122
+ } else {
123
+ const rtcCard = new RTCCard()
124
+ const storageCard = new StorageCard()
125
+ const gpioCard = new GPIOCard()
126
+ const soundCard = new SoundCard()
127
+ const videoCard = new VideoCard()
128
+
129
+ this.io1 = new RAMCard()
130
+ this.io2 = new RAMCard()
131
+ this.io3 = rtcCard
132
+ this.io4 = storageCard
133
+ this.io6 = gpioCard
134
+ this.io7 = soundCard
135
+ this.io8 = videoCard
136
+
137
+ // Connect RTCCard IRQ/NMI to CPU
138
+ rtcCard.raiseIRQ = () => this.cpu.irq()
139
+ rtcCard.raiseNMI = () => this.cpu.nmi()
140
+
141
+ // Connect VideoCard IRQ/NMI to CPU
142
+ videoCard.raiseIRQ = () => this.cpu.irq()
143
+ videoCard.raiseNMI = () => this.cpu.nmi()
144
+
145
+ // Connect SoundCard pushSamples callback
146
+ soundCard.pushSamples = (samples: Float32Array) => {
147
+ if (this.pushAudioSamples) {
148
+ this.pushAudioSamples(samples)
149
+ }
150
+ }
110
151
 
111
- // Attach peripherals to GPIO Card
112
- // Keyboard matrix supports both ports
113
- this.io6.attachToPortA(this.keyboardMatrixAttachment)
114
- this.io6.attachToPortB(this.keyboardMatrixAttachment)
115
-
116
- // Keyboard encoder supports both ports
117
- this.io6.attachToPortA(this.keyboardEncoderAttachment)
118
- this.io6.attachToPortB(this.keyboardEncoderAttachment)
119
-
120
- // Joystick attached to Port B only
121
- this.io6.attachToPortB(this.joystickAttachment)
152
+ // Attach peripherals to GPIO Card
153
+ gpioCard.attachToPortA(this.keyboardMatrixAttachment)
154
+ gpioCard.attachToPortB(this.keyboardMatrixAttachment)
155
+ gpioCard.attachToPortA(this.keyboardEncoderAttachment)
156
+ gpioCard.attachToPortB(this.keyboardEncoderAttachment)
157
+ gpioCard.attachToPortA(this.joystickAttachmentA)
158
+ gpioCard.attachToPortB(this.joystickAttachmentB)
159
+ }
122
160
 
123
161
  this.cpu.reset()
124
162
  }
@@ -147,6 +185,7 @@ export class Machine {
147
185
  }
148
186
 
149
187
  start(): void {
188
+ this.cpu.reset()
150
189
  this.startTime = Date.now()
151
190
  this.isRunning = true
152
191
  this.isAlive = true
@@ -213,20 +252,27 @@ export class Machine {
213
252
  }
214
253
 
215
254
  onKeyDown(scancode: number): void {
216
- // Route keyboard input to attachments
217
- this.keyboardMatrixAttachment.updateKey(scancode, true) // Update keyboard matrix
218
- this.keyboardEncoderAttachment.updateKey(scancode, true) // Update keyboard encoder
255
+ if (this.kim) {
256
+ this.keypadAttachment?.updateKey(scancode, true)
257
+ } else {
258
+ this.keyboardMatrixAttachment.updateKey(scancode, true)
259
+ this.keyboardEncoderAttachment.updateKey(scancode, true)
260
+ }
219
261
  }
220
262
 
221
263
  onKeyUp(scancode: number): void {
222
- // Release key from keyboard matrix and encoder
223
- this.keyboardMatrixAttachment.updateKey(scancode, false) // Update keyboard matrix
224
- this.keyboardEncoderAttachment.updateKey(scancode, false) // Update keyboard encoder
264
+ if (!this.kim) {
265
+ this.keyboardMatrixAttachment.updateKey(scancode, false)
266
+ this.keyboardEncoderAttachment.updateKey(scancode, false)
267
+ }
268
+ }
269
+
270
+ onJoystickA(buttons: number): void {
271
+ this.joystickAttachmentA?.updateJoystick(buttons)
225
272
  }
226
273
 
227
- onJoystick(buttons: number): void {
228
- // Update joystick attachment with button states
229
- this.joystickAttachment.updateJoystick(buttons)
274
+ onJoystickB(buttons: number): void {
275
+ this.joystickAttachmentB?.updateJoystick(buttons)
230
276
  }
231
277
 
232
278
  //
@@ -274,7 +320,7 @@ export class Machine {
274
320
  }
275
321
 
276
322
  if (this.render) {
277
- this.render(this.io8.buffer)
323
+ this.render()
278
324
  this.frames += 1
279
325
  }
280
326
 
package/src/index.ts CHANGED
@@ -4,9 +4,12 @@ import figlet from 'figlet'
4
4
  import { Machine } from './components/Machine'
5
5
  import { Command } from 'commander'
6
6
  import { SerialPort } from 'serialport'
7
+ import { VideoCard } from './components/IO/VideoCard'
8
+ import { StorageCard } from './components/IO/StorageCard'
9
+ import { SoundCard } from './components/IO/SoundCard'
7
10
  import sdl from '@kmamal/sdl'
8
11
 
9
- const VERSION = '1.0.0'
12
+ const VERSION = '1.1.0'
10
13
  const WIDTH = 320
11
14
  const HEIGHT = 240
12
15
 
@@ -40,6 +43,7 @@ interface EmulatorOptions {
40
43
  stopbits?: string
41
44
  port?: string
42
45
  storage?: string
46
+ kim?: boolean
43
47
  }
44
48
 
45
49
  class Emulator {
@@ -48,14 +52,16 @@ class Emulator {
48
52
  private window?: any
49
53
  private audioDevice?: any
50
54
  private controllers: Map<number, any>
51
- private joystickButtonState: number
55
+ private joystickButtonStateA: number
56
+ private joystickButtonStateB: number
52
57
  private options: EmulatorOptions
53
58
 
54
59
  constructor(options: EmulatorOptions) {
55
60
  this.options = options
56
- this.machine = new Machine()
61
+ this.machine = new Machine(options.kim ?? false)
57
62
  this.controllers = new Map()
58
- this.joystickButtonState = 0x00
63
+ this.joystickButtonStateA = 0x00
64
+ this.joystickButtonStateB = 0x00
59
65
  }
60
66
 
61
67
  async initialize(): Promise<void> {
@@ -98,8 +104,8 @@ class Emulator {
98
104
  console.log('Loaded Cart: NONE')
99
105
  }
100
106
 
101
- if (this.options.storage) {
102
- await this.machine.io4.loadFromFile(this.options.storage)
107
+ if (this.options.storage && !this.options.kim) {
108
+ await (this.machine.io4 as StorageCard).loadFromFile(this.options.storage)
103
109
  }
104
110
  }
105
111
 
@@ -177,6 +183,7 @@ class Emulator {
177
183
  }
178
184
 
179
185
  private setupAudio(): void {
186
+ if (this.options.kim) return
180
187
  try {
181
188
  this.audioDevice = sdl.audio.openDevice({ type: 'playback' }, {
182
189
  channels: AUDIO_CHANNELS as 1,
@@ -186,7 +193,7 @@ class Emulator {
186
193
  })
187
194
 
188
195
  // Configure SoundCard sample rate to match audio device
189
- this.machine.io7.sampleRate = this.audioDevice.frequency
196
+ ;(this.machine.io7 as SoundCard).sampleRate = this.audioDevice.frequency
190
197
 
191
198
  // Connect the Machine's audio callback to the SDL audio device
192
199
  this.machine.pushAudioSamples = (samples: Float32Array) => {
@@ -211,10 +218,29 @@ class Emulator {
211
218
  }
212
219
 
213
220
  private setupWindow(): void {
221
+ const isKIM = this.options.kim ?? false
222
+ const lcd = this.machine.lcdAttachment
223
+
224
+ // LCD dot-matrix rendering constants
225
+ const DOT_SIZE = 2 // Each LCD dot rendered as DOT_SIZE x DOT_SIZE pixels
226
+ const DOT_GAP = 1 // Gap between dots
227
+ const LCD_PADDING = 8 // Green border padding around the display
228
+ const CELL = DOT_SIZE + DOT_GAP
229
+
230
+ let windowWidth: number
231
+ let windowHeight: number
232
+ if (isKIM && lcd) {
233
+ windowWidth = LCD_PADDING * 2 + lcd.pixelsWidth * CELL
234
+ windowHeight = LCD_PADDING * 2 + lcd.pixelsHeight * CELL
235
+ } else {
236
+ windowWidth = WIDTH
237
+ windowHeight = HEIGHT
238
+ }
239
+
214
240
  this.window = sdl.video.createWindow({
215
- title: "6502 Emulator",
216
- width: WIDTH * this.machine.scale,
217
- height: HEIGHT * this.machine.scale,
241
+ title: isKIM ? "6502 Emulator (KIM)" : "6502 Emulator",
242
+ width: windowWidth * this.machine.scale,
243
+ height: windowHeight * this.machine.scale,
218
244
  accelerated: true,
219
245
  vsync: true
220
246
  })
@@ -229,15 +255,89 @@ class Emulator {
229
255
  this.machine.onKeyUp(event.scancode)
230
256
  })
231
257
 
232
- this.machine.render = (buffer: Buffer) => {
233
- if (!this.window) { return }
234
- this.window.render(WIDTH, HEIGHT, WIDTH * 4, 'rgba32', buffer)
258
+ if (isKIM && lcd) {
259
+ const lcdWidth = lcd.pixelsWidth
260
+ const lcdHeight = lcd.pixelsHeight
261
+ const renderWidth = windowWidth
262
+ const renderHeight = windowHeight
263
+ const rgbaBuffer = Buffer.alloc(renderWidth * renderHeight * 4)
264
+
265
+ // Pre-fill with background color (LCD green for padding + gaps)
266
+ for (let i = 0; i < renderWidth * renderHeight; i++) {
267
+ const off = i * 4
268
+ rgbaBuffer[off] = 0x50
269
+ rgbaBuffer[off + 1] = 0x88
270
+ rgbaBuffer[off + 2] = 0x38
271
+ rgbaBuffer[off + 3] = 0xFF
272
+ }
273
+
274
+ this.machine.render = () => {
275
+ if (!this.window) { return }
276
+ const buf = lcd.buffer
277
+
278
+ // Reset buffer to background color
279
+ for (let i = 0; i < renderWidth * renderHeight; i++) {
280
+ const off = i * 4
281
+ rgbaBuffer[off] = 0x50
282
+ rgbaBuffer[off + 1] = 0x88
283
+ rgbaBuffer[off + 2] = 0x38
284
+ rgbaBuffer[off + 3] = 0xFF
285
+ }
286
+
287
+ // Render each buffer pixel as a dot block
288
+ for (let by = 0; by < lcdHeight; by++) {
289
+ for (let bx = 0; bx < lcdWidth; bx++) {
290
+ const val = buf[by * lcdWidth + bx]
291
+
292
+ if (val < 0) {
293
+ // Gap pixel - skip, shows background color
294
+ continue
295
+ }
296
+
297
+ let r: number, g: number, b: number
298
+ if (val === 0) {
299
+ // Pixel off - slightly brighter than background for visible dot grid
300
+ r = 0x60; g = 0xA0; b = 0x40
301
+ } else {
302
+ // Pixel on - dark
303
+ r = 0x10; g = 0x20; b = 0x10
304
+ }
305
+
306
+ // Draw DOT_SIZE x DOT_SIZE block
307
+ const screenX = LCD_PADDING + bx * CELL
308
+ const screenY = LCD_PADDING + by * CELL
309
+ for (let dy = 0; dy < DOT_SIZE; dy++) {
310
+ for (let dx = 0; dx < DOT_SIZE; dx++) {
311
+ const off = ((screenY + dy) * renderWidth + (screenX + dx)) * 4
312
+ rgbaBuffer[off] = r
313
+ rgbaBuffer[off + 1] = g
314
+ rgbaBuffer[off + 2] = b
315
+ rgbaBuffer[off + 3] = 0xFF
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ this.window.render(renderWidth, renderHeight, renderWidth * 4, 'rgba32', rgbaBuffer)
322
+ }
323
+ } else {
324
+ const videoCard = this.machine.io8 as VideoCard
325
+ this.machine.render = () => {
326
+ if (!this.window) { return }
327
+ this.window.render(WIDTH, HEIGHT, WIDTH * 4, 'rgba32', videoCard.buffer)
328
+ }
235
329
  }
236
330
 
237
331
  this.window.on('close', () => this.shutdown())
238
332
  }
239
333
 
334
+ private playerForController(deviceId: number): 'A' | 'B' {
335
+ const ids = Array.from(this.controllers.keys())
336
+ return ids.indexOf(deviceId) === 0 ? 'B' : 'A'
337
+ }
338
+
240
339
  private setupControllers(): void {
340
+ if (this.options.kim) return
241
341
  // Controller device add/remove handlers
242
342
  (sdl.controller as any).on('deviceAdd', (device: any) => {
243
343
  console.log(`Controller added: ${device.name || device.id}`)
@@ -246,9 +346,10 @@ class Emulator {
246
346
  const controller = sdl.controller.openDevice(device)
247
347
  this.controllers.set(device.id, controller)
248
348
 
249
- this.setupControllerHandlers(controller, device)
349
+ const player = this.playerForController(device.id)
350
+ this.setupControllerHandlers(controller, device, player)
250
351
 
251
- console.log(`Controller ${device.name || device.id} opened successfully`)
352
+ console.log(`Controller ${device.name || device.id} opened as Player ${player}`)
252
353
  } catch (error) {
253
354
  console.error(`Failed to open controller ${device.name || device.id}:`, error)
254
355
  }
@@ -257,16 +358,20 @@ class Emulator {
257
358
  ;(sdl.controller as any).on('deviceRemove', (device: any) => {
258
359
  console.log(`Controller removed: ${device.name || device.id}`)
259
360
 
361
+ const player = this.playerForController(device.id)
260
362
  const controller = this.controllers.get(device.id)
261
363
  if (controller && !controller.closed) {
262
364
  controller.close()
263
365
  }
264
366
  this.controllers.delete(device.id)
265
367
 
266
- // Clear joystick state when all controllers are removed
267
- if (this.controllers.size === 0) {
268
- this.joystickButtonState = 0x00
269
- this.machine.onJoystick(this.joystickButtonState)
368
+ // Clear joystick state for the removed controller's player
369
+ if (player === 'A') {
370
+ this.joystickButtonStateA = 0x00
371
+ this.machine.onJoystickA(this.joystickButtonStateA)
372
+ } else {
373
+ this.joystickButtonStateB = 0x00
374
+ this.machine.onJoystickB(this.joystickButtonStateB)
270
375
  }
271
376
  })
272
377
 
@@ -280,9 +385,10 @@ class Emulator {
280
385
  const controller = sdl.controller.openDevice(device)
281
386
  this.controllers.set(device.id, controller)
282
387
 
283
- this.setupControllerHandlers(controller, device)
388
+ const player = this.playerForController(device.id)
389
+ this.setupControllerHandlers(controller, device, player)
284
390
 
285
- console.log(`Controller ${device.name || device.id} opened successfully`)
391
+ console.log(`Controller ${device.name || device.id} opened as Player ${player}`)
286
392
  } catch (error) {
287
393
  console.error(`Failed to open controller ${device.name || device.id}:`, error)
288
394
  }
@@ -292,90 +398,75 @@ class Emulator {
292
398
  }
293
399
  }
294
400
 
295
- private setupControllerHandlers(controller: any, device: any): void {
296
- (controller as any).on('buttonDown', (button: string) => {
401
+ private setupControllerHandlers(controller: any, device: any, player: 'A' | 'B'): void {
402
+ const getState = () => player === 'A' ? this.joystickButtonStateA : this.joystickButtonStateB
403
+ const setState = (v: number) => {
404
+ if (player === 'A') this.joystickButtonStateA = v
405
+ else this.joystickButtonStateB = v
406
+ }
407
+ const send = () => {
408
+ if (player === 'A') this.machine.onJoystickA(this.joystickButtonStateA)
409
+ else this.machine.onJoystickB(this.joystickButtonStateB)
410
+ }
411
+
412
+ ;(controller as any).on('buttonDown', (button: string) => {
413
+ let state = getState()
297
414
  switch (button) {
298
- case 'dpadUp':
299
- this.joystickButtonState |= BUTTON_UP
300
- break
301
- case 'dpadDown':
302
- this.joystickButtonState |= BUTTON_DOWN
303
- break
304
- case 'dpadLeft':
305
- this.joystickButtonState |= BUTTON_LEFT
306
- break
307
- case 'dpadRight':
308
- this.joystickButtonState |= BUTTON_RIGHT
309
- break
310
- case 'a':
311
- this.joystickButtonState |= BUTTON_A
312
- break
313
- case 'b':
314
- this.joystickButtonState |= BUTTON_B
315
- break
316
- case 'back':
317
- this.joystickButtonState |= BUTTON_SELECT
318
- break
319
- case 'start':
320
- this.joystickButtonState |= BUTTON_START
321
- break
415
+ case 'dpadUp': state |= BUTTON_UP; break
416
+ case 'dpadDown': state |= BUTTON_DOWN; break
417
+ case 'dpadLeft': state |= BUTTON_LEFT; break
418
+ case 'dpadRight': state |= BUTTON_RIGHT; break
419
+ case 'a': state |= BUTTON_A; break
420
+ case 'b': state |= BUTTON_B; break
421
+ case 'back': state |= BUTTON_SELECT; break
422
+ case 'start': state |= BUTTON_START; break
322
423
  }
323
- this.machine.onJoystick(this.joystickButtonState)
424
+ setState(state)
425
+ send()
324
426
  })
325
427
 
326
428
  ;(controller as any).on('buttonUp', (button: string) => {
429
+ let state = getState()
327
430
  switch (button) {
328
- case 'dpadUp':
329
- this.joystickButtonState &= ~BUTTON_UP
330
- break
331
- case 'dpadDown':
332
- this.joystickButtonState &= ~BUTTON_DOWN
333
- break
334
- case 'dpadLeft':
335
- this.joystickButtonState &= ~BUTTON_LEFT
336
- break
337
- case 'dpadRight':
338
- this.joystickButtonState &= ~BUTTON_RIGHT
339
- break
340
- case 'a':
341
- this.joystickButtonState &= ~BUTTON_A
342
- break
343
- case 'b':
344
- this.joystickButtonState &= ~BUTTON_B
345
- break
346
- case 'back':
347
- this.joystickButtonState &= ~BUTTON_SELECT
348
- break
349
- case 'start':
350
- this.joystickButtonState &= ~BUTTON_START
351
- break
431
+ case 'dpadUp': state &= ~BUTTON_UP; break
432
+ case 'dpadDown': state &= ~BUTTON_DOWN; break
433
+ case 'dpadLeft': state &= ~BUTTON_LEFT; break
434
+ case 'dpadRight': state &= ~BUTTON_RIGHT; break
435
+ case 'a': state &= ~BUTTON_A; break
436
+ case 'b': state &= ~BUTTON_B; break
437
+ case 'back': state &= ~BUTTON_SELECT; break
438
+ case 'start': state &= ~BUTTON_START; break
352
439
  }
353
- this.machine.onJoystick(this.joystickButtonState)
440
+ setState(state)
441
+ send()
354
442
  })
355
443
 
356
444
  controller.on('axisMotion', ({ axis, value }: { axis: string; value: number }) => {
445
+ let state = getState()
357
446
  if (axis === 'leftStickX') {
358
447
  if (value < -AXIS_THRESHOLD) {
359
- this.joystickButtonState |= BUTTON_LEFT
360
- this.joystickButtonState &= ~BUTTON_RIGHT
448
+ state |= BUTTON_LEFT
449
+ state &= ~BUTTON_RIGHT
361
450
  } else if (value > AXIS_THRESHOLD) {
362
- this.joystickButtonState |= BUTTON_RIGHT
363
- this.joystickButtonState &= ~BUTTON_LEFT
451
+ state |= BUTTON_RIGHT
452
+ state &= ~BUTTON_LEFT
364
453
  } else {
365
- this.joystickButtonState &= ~(BUTTON_LEFT | BUTTON_RIGHT)
454
+ state &= ~(BUTTON_LEFT | BUTTON_RIGHT)
366
455
  }
367
- this.machine.onJoystick(this.joystickButtonState)
456
+ setState(state)
457
+ send()
368
458
  } else if (axis === 'leftStickY') {
369
459
  if (value < -AXIS_THRESHOLD) {
370
- this.joystickButtonState |= BUTTON_UP
371
- this.joystickButtonState &= ~BUTTON_DOWN
460
+ state |= BUTTON_UP
461
+ state &= ~BUTTON_DOWN
372
462
  } else if (value > AXIS_THRESHOLD) {
373
- this.joystickButtonState |= BUTTON_DOWN
374
- this.joystickButtonState &= ~BUTTON_UP
463
+ state |= BUTTON_DOWN
464
+ state &= ~BUTTON_UP
375
465
  } else {
376
- this.joystickButtonState &= ~(BUTTON_UP | BUTTON_DOWN)
466
+ state &= ~(BUTTON_UP | BUTTON_DOWN)
377
467
  }
378
- this.machine.onJoystick(this.joystickButtonState)
468
+ setState(state)
469
+ send()
379
470
  }
380
471
  })
381
472
 
@@ -422,8 +513,8 @@ class Emulator {
422
513
  })
423
514
 
424
515
  // Save storage data if path was provided
425
- if (this.options.storage) {
426
- this.machine.io4.saveToFile(this.options.storage).then(() => {
516
+ if (this.options.storage && !this.options.kim) {
517
+ (this.machine.io4 as StorageCard).saveToFile(this.options.storage).then(() => {
427
518
  process.exit(0)
428
519
  }).catch(() => {
429
520
  process.exit(1)
@@ -455,6 +546,7 @@ program
455
546
  .option('-t, --stopbits <stopbits>', 'Stop Bits (1 | 1.5 | 2)', '1')
456
547
  .option('-p, --port <port>', 'Path to the serial port (e.g., /dev/ttyUSB0)')
457
548
  .option('-S, --storage <path>', 'Path to storage data file for Compact Flash card persistence')
549
+ .option('-K, --kim', 'Configure IO for the KIM system target', false)
458
550
  .addHelpText('beforeAll', figlet.textSync('6502 Emulator', { font: 'cricket' }) + '\n' + `Version: ${VERSION} | A.C. Wright Design\n`)
459
551
  .parse(process.argv)
460
552