Skip to content

feat: wire up accessibility switch keys for user port, ADC, and fire buttons#565

Merged
mattgodbolt merged 4 commits intomainfrom
claude/accessibility-userport
Feb 28, 2026
Merged

feat: wire up accessibility switch keys for user port, ADC, and fire buttons#565
mattgodbolt merged 4 commits intomainfrom
claude/accessibility-userport

Conversation

@mattgodbolt-molty
Copy link
Collaborator

@mattgodbolt-molty mattgodbolt-molty commented Feb 26, 2026

Fixes #160.

What was wrong

The existing user port key mapping (added 2018, commit c77e85b) was dead code. It set up handlers in a local emuKeyHandlers map that was never connected to the keyboard system after the 2025 keyboard refactor (commit 64786cf), so pressing any key never actually updated &FE60.

Additionally, keyboard.js called sysvia.keyDown() before checking registered handlers, so any handled key also leaked through to the emulated machine.

Hardware archaeology

From the Brilliant Computing catalogue (the original supplier referenced in the issue):

  • Interface Box — connects to the User Port via ribbon cable; accepts up to 4 switches with ¼" jack plugs
  • Special Ed. Joystick — also User Port; description explicitly says "Plugs straight into the 'User Port' (no Interface Box needed)"

Both devices affect only ?&FE60. The analogue port (ADC) and System VIA fire buttons (PB4/PB5) belong to the standard analogue joystick connector — a completely separate physical port. Real switch hardware never touches them.

An earlier version of this PR also deflected ADC channels and triggered fire buttons alongside user port bits, following a commenter's ideal spec. Testing with Thurrock Care showed this was wrong — coupling Alt+1 to PB4 caused an unwanted "select current item" action to fire every time a switch was pressed.

Changes

src/keyboard.js

  • Check registered handlers first in keyDown(); if one fires, return early without calling sysvia.keyDown(). Handled keys no longer leak through to the BBC.
  • keyUp() still always forwards to sysvia.keyUp() to avoid sticky keys.

src/main.js

  • Remove the orphaned emuKeyHandlers map and dead switchKey loop
  • Register switch handlers via keyboard.registerKeyHandler() after keyboard initialisation
  • Alt+1–8 → user port bits 0–7 (active low). Restores the original 2018 intent; normal number keys reach the BBC unaffected.
  • Alt+F1–F8 → same switch bits, ergonomic alternative; normal BBC function keys (no Alt) are completely unaffected.
  • Nothing else — no ADC deflection, no fire button coupling. Matches real hardware.

tests/unit/test-keyboard.js

  • Add assertion to existing Alt-handler test that sysvia.keyDown was not called
  • Two new tests: handler suppresses sysvia.keyDown; unhandled keys still reach it

Switch mapping

Key User port bit ?&FE60 when pressed
Alt+F1 / Alt+1 0 254
Alt+F2 / Alt+2 1 253
Alt+F3 / Alt+3 2 251
Alt+F4 / Alt+4 3 247
Alt+F5 / Alt+5 4 239
Alt+F6 / Alt+6 5 223
Alt+F7 / Alt+7 6 191
Alt+F8 / Alt+8 7 127

Testing

257 unit tests pass including 2 new keyboard tests. Manual testing with the test discs from the issue:

(I'm Molty, an AI assistant acting on behalf of @mattgodbolt)

…buttons

The existing user port key mapping (keys 1-8 → &FE60 bits 0-7) was
implemented using a local emuKeyHandlers map that was never connected to the
keyboard system — so it never actually worked.

Fix this and extend the feature:

- Remove dead emuKeyHandlers code; register handlers via
  keyboard.registerKeyHandler() after the keyboard is initialised
- Keys 1-8 (K1-K8) continue to map to user port bits 0-7 (active low)
- Function keys F1-F8 are added as an additional set of switch inputs
  (more ergonomic for accessibility use)
- F1/K1 and F2/K2 also set the joystick fire buttons on the System VIA
  (PB4/PB5), so ADVAL(-1) and ADVAL(-2) respond to the switch state
- Add KeyboardSwitchSource (src/keyboard-switch-source.js) — an
  AnalogueSource that deflects ADC channels 0-3 to 0x0000 while a switch
  is pressed, then delegates to the gamepad source otherwise; installed as
  the default source for all four ADC channels so ADVAL(1)-ADVAL(4) also
  respond

Closes #160

🤖 Generated by LLM (Claude, via OpenClaw)
- keyboard.js keyDown() now checks registered handlers *before* calling
  sysvia.keyDown(); if a handler fires the key is not also forwarded to
  the emulated machine, so Alt+key handlers cleanly own their keys.
  (keyUp always forwards to the BBC to avoid sticky keys — a keyUp for a
  key the BBC never received is harmless.)
- Switch key handlers changed from noMod to { alt: true }: Alt+1–8 and
  Alt+F1–F8 trigger switches, restoring the original 2018 intent.
  Normal number keys and BBC function keys are now completely unaffected.
Add two keyboard unit tests:
- Registered Alt-key handler fires without forwarding to BBC (sysvia.keyDown not called)
- Unhandled keys still reach sysvia.keyDown as normal

Also add the missing expect(sysvia.keyDown).not.toHaveBeenCalled() assertion
to the existing Alt-handler registration test.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR restores and enhances accessibility switch support for the BBC Micro emulator by fixing dead code from 2018 and adding comprehensive input mapping. It addresses issue #160 by properly wiring keyboard switch handlers to the user port, ADC channels, and joystick fire buttons, enabling disabled users to access switch-interface software.

Changes:

  • Fixed keyboard event handler ordering to prevent key leakage when handlers are registered
  • Replaced orphaned emuKeyHandlers map with properly registered Alt+1-8 and Alt+F1-F8 switch handlers
  • Added KeyboardSwitchSource to map switch presses to ADC channels for analogue port compatibility

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
src/keyboard.js Reordered keyDown logic to check registered handlers before forwarding to BBC, preventing handled keys from leaking through
src/main.js Removed dead switch handler code, added proper keyboard handler registration for Alt+1-8/F1-F8, and wired switches to user port, ADC channels, and fire buttons
src/keyboard-switch-source.js New AnalogueSource implementation that deflects ADC channels to 0x0000 when switches are pressed, delegating to gamepad otherwise
tests/unit/test-keyboard.js Added test assertions verifying handlers suppress sysvia.keyDown and unhandled keys still reach it
Comments suppressed due to low confidence (1)

src/keyboard-switch-source.js:41

  • The new KeyboardSwitchSource class lacks unit tests. Following the codebase convention where similar AnalogueSource implementations like GamepadSource (test-gamepad-source.js) and MouseJoystickSource (test-mouse-joystick-source.js) have dedicated test files, this class should have a corresponding test-keyboard-switch-source.js file. Tests should verify: 1) getValue returns 0x0000 when a switch is pressed, 2) getValue delegates to the fallback source when no switch is pressed, 3) setSwitch correctly updates switch state for valid indices (0-3), and 4) setSwitch ignores invalid indices.
import { AnalogueSource } from "./analogue-source.js";

/**
 * An AnalogueSource that maps accessibility switch keys to ADC channels.
 *
 * When switch N is pressed, channel N returns 0x0000 (full deflection).
 * When not pressed, the call is delegated to the wrapped fallback source
 * (typically the gamepad source).
 *
 * Switches 0 and 1 additionally map to the joystick fire buttons (PB4/PB5
 * on the System VIA), so ADVAL(-1) / ADVAL(-2) reflect switch state too.
 */
export class KeyboardSwitchSource extends AnalogueSource {
    /**
     * @param {AnalogueSource} fallback - Source to delegate to when no switch is pressed
     */
    constructor(fallback) {
        super();
        this.fallback = fallback;
        this._switchValues = new Array(4).fill(null); // null = not pressed
    }

    /**
     * Activate or deactivate a switch.
     * @param {number} n - Switch index (0-3)
     * @param {boolean} pressed
     */
    setSwitch(n, pressed) {
        if (n >= 0 && n < 4) {
            this._switchValues[n] = pressed ? 0x0000 : null;
        }
    }

    /** @override */
    getValue(channel) {
        if (channel >= 0 && channel < 4 && this._switchValues[channel] !== null) {
            return this._switchValues[channel];
        }
        return this.fallback ? this.fallback.getValue(channel) : 0x8000;
    }
}

On real Brilliant Computing hardware both the switch interface box and the
special-ed joystick connect to the User Port via ribbon cable — they do not
touch the analogue port (ADC) or the System VIA fire buttons (PB4/PB5),
which belong to the standard analogue joystick connector.

Coupling Alt+1/Alt+F1 to the fire button (PB4) caused accessibility
software (e.g. Thurrock Care) to trigger an unwanted 'select current
item' action whenever a switch was pressed.

Remove KeyboardSwitchSource and all ADC/fire-button coupling from the
switch handler.  Switch keys now only toggle switchState, which is read
via userPort.read() as &FE60.  This matches the real hardware behaviour.
@mattgodbolt-molty
Copy link
Collaborator Author

Code archaeology & headless testing results

I downloaded both test disc images and ran them headlessly via MachineSession to verify the implementation. Here's what I found.


Thurrock Care (DISC000.ssd) — detokenised BASIC + headless run

Every program on the disc polls both ?&FE60 and INKEY(0) together on the same line:

K%=INKEY(0): face%=?&FE60: UNTIL K%=-1 AND face%=255

INKEY(0) returns immediately (-1 = no key, otherwise the key's ASCII/scan code). The UNTIL condition is K%=-1 AND face%=255 — i.e. loop while no keyboard key AND no switch pressed. So any keypress or any user port switch press triggers the action. They're OR'd together as alternative inputs for users with different abilities.

Running the disc headlessly and capturing the screen confirms the control scheme directly from the UI:

SWITCH: (4) move selector; (1-3) choose.

  • Alt+4 → bit 3 low → ?&FE60=247 → moves selector ✓
  • Alt+1/2/3 → bits 0–2 low → ?&FE60=254/253/251 → select option 1/2/3 ✓

No ADVAL anywhere in Thurrock Care — purely ?&FE60 + keyboard fallback.


Joystick Games 1 (DISC013.ssd) — detokenised BASIC + headless run

The menu immediately asks:

1. Switches connected to USER PORT.
2. Switches connected to ANALOGUE PORT

The FNinput function used by every game is:

DEF FNinput
IF Z%=1 THEN =?&FE60
ELSE =255 + (ADVAL(1)<1000) + (ADVAL(1)>65000)*2 + (ADVAL(2)>65000)*4 + (ADVAL(2)<1000)*8

USER PORT mode (Z%=1): reads ?&FE60 only. ADVAL is never touched.
ANALOGUE PORT mode: synthesises a ?&FE60-style bitmask from ADVAL(1) and ADVAL(2).

The game menu says: "Press the Space Bar or Switch when the game you want is highlighted" — it's a scanning interface, cycling through options automatically; any switch press selects.

The reported lockup is explained by these two startup guards in JMENU:

880 UNTIL ?&FE60 = 255        ← waits for user port to clear
910 UNTIL ADVAL(1) IN 1000..65000 AND ADVAL(2) IN 1000..65000 AND (ADVAL(0)AND3)<>1

With this PR:

  • ?&FE60 defaults to switchState = 0xFF = 255 → line 880 passes immediately ✓
  • ADVAL() returns 0x8000 = 32768 (neutral) from GamepadSource when no pad connected — well within 1000–65000 → line 910 passes immediately ✓

Both lockups are resolved without needing any ADVAL/fire-button coupling from the switch keys. The earlier version of this PR coupled Alt+1 → PB4 fire button, which caused Thurrock Care to trigger an unwanted "select" action. Removing that coupling was correct.


Summary

Thurrock Care Joystick Games (USER PORT) Joystick Games (ANALOGUE)
Reads ?&FE60 ✅ primary input ✅ only input ❌ ignored
Reads ADVAL ✅ primary input
Reads INKEY ✅ keyboard fallback ✅ Space Bar keyboard nav
Fire buttons PB4/5 lockup guard only
Lockup fixed? n/a ✅ (ADC returns neutral)
Switch keys work? n/a (needs real joystick)

The PR implementation — Alt+1–8 / Alt+F1–F8 setting only ?&FE60 bits — is correct for all the cases it can reasonably cover.

(I'm Molty, an AI assistant acting on behalf of @mattgodbolt)

@mattgodbolt mattgodbolt merged commit faa63b9 into main Feb 28, 2026
4 checks passed
@mattgodbolt mattgodbolt deleted the claude/accessibility-userport branch February 28, 2026 14:03
@mattgodbolt
Copy link
Owner

@oneswitch FYI some behind the scenes research stuff :)

@oneswitch
Copy link

Ah magic. I'll bury into this properly ASAP, but I can see already it's going to bring those things back to life. Thanks so much!

@mattgodbolt
Copy link
Owner

Please let me know whatever we can do to get this working well fo your use cases!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

USER Port access (especially for switch interface box and joystick Special Needs access)

4 participants