mz25key Interface Developer's Guide

mz25key Developer's Guide

The mz25key is a dedicated keyboard interface for the Sharp MZ-2500 and MZ-2800 computers, built around the Espressif ESP32 dual-core microcontroller. It translates modern PS/2 and Bluetooth HID keyboards into the native MZ-2500/MZ-2800 keyboard matrix protocol, allowing any standard USB or wireless keyboard to be used with these vintage Sharp machines. The mz25key shares its entire codebase with the SharpKey multi-host interface project but is built with a simplified configuration targeting only the MZ-2500 or MZ-2800. The firmware is licensed under GPL v3. Author: Philip Smart.
This guide is the primary developer reference for the mz25key firmware. It covers the complete software architecture, every source module, the MZ-2500/MZ-2800 keyboard protocol in depth, the key mapping system, web interface, Bluetooth integration, build environment, CI/CD pipeline, and debugging techniques. For hardware design, schematics, and end-user operation see the mz25key project page. For the full multi-host SharpKey developer's guide see the SharpKey Developer's Guide.
Key difference from SharpKey: The mz25key is the same source code as SharpKey but built with the Kconfig target MZ25KEY_MZ2500 or MZ25KEY_MZ2800 instead of SHARPKEY. The critical difference is that the mz25key git repository does not use git submodules — the required component libraries (arduino-esp32 and esp_littlefs) must be cloned manually into the components/ directory at specific versions. The CI/CD pipeline handles this automatically in a dedicated "Fetch Components" stage.

Prerequisites

Before you can build and flash the mz25key firmware you need a working development environment. There are two approaches: a native ESP-IDF installation, or Docker (covered in the Docker Build section below).
  • ESP-IDF v4.4: The Espressif IoT Development Framework, version 4.4 specifically. Later versions (v5.x) are not compatible due to Arduino component API changes. Install natively or use the official Docker image espressif/idf:v4.4.
  • Python 3.8+: Required by the ESP-IDF build system scripts and menuconfig. Use update-alternatives on Linux to ensure python3 points to a recent version.
  • Git: For cloning the repository and the two required component libraries. Note: unlike SharpKey, the mz25key repository does not use git submodules.
  • USB-to-TTL UART adapter: For flashing firmware and serial debug output. Must support 3.3V logic levels with RTS and DTR signals for automated bootstrap/programming mode entry.
  • Docker (optional): As an alternative to installing ESP-IDF natively. The espressif/idf:v4.4 Docker image contains the complete toolchain.
  • Component versions: arduino-esp32 at tag 2.0.3 and esp_littlefs at tag v1.3.1 (with submodules initialised).

Repository Structure

The mz25key repository is hosted at https://git.eaw.app/eaw/mz25key. It shares the same source files as the SharpKey project but lives in a separate repository with one critical structural difference: there is no .gitmodules file. The components/ directory is empty after a fresh clone and must be populated manually by cloning the two required libraries at their specific versions.
mz25key/
├── main/
│   ├── SharpKey.cpp              — Entry point (same source as SharpKey)
│   ├── MZ2528.cpp                — MZ-2500/2800 host interface (Core 1)
│   ├── MZ5665.cpp                — MZ-5500/5600/6500 (compiled but unused in mz25key build)
│   ├── X1.cpp                    — Sharp X1 interface (compiled but unused)
│   ├── X68K.cpp                  — Sharp X68000 interface (compiled but unused)
│   ├── PC9801.cpp                — NEC PC-9801 interface (compiled but unused)
│   ├── Mouse.cpp                 — Mouse support
│   ├── HID.cpp                   — HID device manager / input aggregator
│   ├── BTHID.cpp                 — Bluetooth HID report translation
│   ├── BT.cpp                    — Bluetooth GAP/GATT discovery and pairing
│   ├── WiFi.cpp                  — WiFi AP/client, HTTP server, OTA, LittleFS
│   ├── PS2KeyAdvanced.cpp        — PS/2 keyboard protocol (interrupt-driven)
│   ├── PS2Mouse.cpp              — PS/2 mouse protocol
│   ├── KeyInterface.cpp          — Virtual base class for host interfaces
│   ├── LED.cpp                   — Status LED control
│   ├── NVS.cpp                   — Non-volatile storage wrapper
│   ├── SWITCH.cpp                — Timed button handler
│   ├── esp_efuse_custom_table.c  — eFuse field accessors (generated)
│   ├── esp_efuse_custom_table.csv — eFuse field definitions (source)
│   ├── Kconfig.projbuild         — Build configuration menu
│   └── include/                  — Headers with key mapping tables
│       ├── MZ2528.h              — MZ-2500/2800 mapping table (165 entries)
│       ├── X1.h, X68K.h, ...    — Other host mapping tables
│       └── ...
├── webserver/                    — HTML/CSS/JS web interface
│   ├── version.txt               — Release version (NOTE: not in project root)
│   └── html/                     — Web page source files
├── components/                   — EMPTY after clone! Must populate manually
│   ├── arduino-esp32/            — Clone from GitHub at tag 2.0.3
│   └── esp_littlefs/             — Clone from GitHub at tag v1.3.1 + submodules
├── build_webfs.sh                — Web filesystem builder script
├── sharpkey_partition_table.csv  — Flash partition layout (same as SharpKey)
├── sdkconfig                     — Default build configuration (MZ25KEY target)
├── CMakeLists.txt                — CMake project file
└── version.txt                   — (May exist — see webserver/version.txt for CI)
Important: After cloning the repository, the components/ directory will be empty. You must manually clone the two component libraries before building:
# Clone the mz25key repository
git clone https://git.eaw.app/eaw/mz25key.git
cd mz25key

# Clone required components manually (NOT submodules!)
mkdir -p components

# Arduino-ESP32 compatibility layer — must be v2.0.3
git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32
cd components/arduino-esp32 && git checkout 2.0.3 && cd ../..

# LittleFS filesystem support — must be v1.3.1 WITH submodules
git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs
cd components/esp_littlefs && git checkout v1.3.1 && git submodule update --init --recursive && cd ../..
The esp_littlefs component requires its own submodules to be initialised — specifically the mklittlefs tool. Without running git submodule update --init --recursive inside components/esp_littlefs/, the build will fail with a "No rule to make target 'dist'" error during the filesystem image generation step.

Differences from SharpKey

The mz25key and SharpKey share the same source code, but there are several important differences in how the project is structured, built, and deployed:
  1. Build target: mz25key uses CONFIG_MZ25KEY_MZ2500 or CONFIG_MZ25KEY_MZ2800 in Kconfig, not CONFIG_SHARPKEY. This selects only the MZ-2500 or MZ-2800 host interface path and disables the runtime GPIO-based host auto-detection used by SharpKey.
  2. No git submodules: The mz25key repository has no .gitmodules file. The components/ directory is empty after a fresh clone. You must manually clone arduino-esp32 (v2.0.3) and esp_littlefs (v1.3.1 with submodules) into components/. SharpKey uses git clone --recursive to fetch these automatically.
  3. Single host interface: Only the MZ-2500 or MZ-2800 interface path is active at runtime. SharpKey detects the connected host via GPIO pin patterns and selects the appropriate interface dynamically.
  4. Simplified configuration: Fewer Kconfig options are exposed. The multi-host GPIO detection pins, X1/X68K/PC9801/MZ5665 specific settings, and the universal build target are not relevant.
  5. Separate repository: https://git.eaw.app/eaw/mz25key (SharpKey is at https://git.eaw.app/eaw/SharpKey).
  6. CI/CD pipeline: Has an additional "Fetch Components" stage that clones the two component libraries. SharpKey's pipeline uses git submodule update --init --recursive instead.
  7. Version file location: The CI pipeline reads the version from webserver/version.txt, not version.txt in the project root as SharpKey does.
  8. Release artifact naming: Uses the mz25key- prefix (e.g. mz25key-FW-v1.00.bin.gz) instead of SharpKey-.

Software Architecture

The mz25key firmware shares the same layered software architecture as SharpKey. It is a dual-core ESP32 application built on FreeRTOS, with the two CPU cores serving distinct roles. The input layer accepts key events from PS/2 keyboards and Bluetooth HID devices. The key mapping layer translates these events into MZ-2500/MZ-2800 scan matrix positions via MZ2528::mapKey(). The output layer drives the timing-critical GPIO signals on Core 1 to emulate the native keyboard.

Architecture Overview

 ┌───────────────────────────────────────────────────────────────────────────┐
 │                        ESP-32S (Dual Core 240MHz)                        │
 │                                                                         │
 │  ┌─────────────────────────────┐  ┌──────────────────────────────────┐   │
 │  │       CORE 0 (PRO)         │  │        CORE 1 (APP)              │   │
 │  │   FreeRTOS Scheduled       │  │   Spinlocked (no FreeRTOS)       │   │
 │  │                            │  │                                  │   │
 │  │  ┌──────────────────────┐  │  │  ┌────────────────────────────┐  │   │
 │  │  │ PS/2 Keyboard Input  │  │  │  │  MZ-2500/2800 Interface   │  │   │
 │  │  │  (PS2KeyAdvanced)    │  │  │  │  (MZ2528 class)           │  │   │
 │  │  └─────────┬────────────┘  │  │  │                            │  │   │
 │  │            │               │  │  │  - Monitor RTSN strobe     │  │   │
 │  │  ┌─────────▼────────────┐  │  │  │  - Read row from KDB[3:0] │  │   │
 │  │  │ Bluetooth Keyboard   │  │  │  │  - Output column data      │  │   │
 │  │  │  (BTHID class)       │  │  │  │    via KDO[7:0]           │  │   │
 │  │  └─────────┬────────────┘  │  │  │  - Handle KD4/MPX timing  │  │   │
 │  │            │               │  │  └────────────┬───────────────┘  │   │
 │  │  ┌─────────▼────────────┐  │  │               │                  │   │
 │  │  │ HID Processing       │  │  │               │                  │   │
 │  │  │  PS/2 → internal     │  │  │  ┌────────────▼───────────────┐  │   │
 │  │  │  scan code           │  │  │  │  Virtual Scan Matrix       │  │   │
 │  │  └─────────┬────────────┘  │  │  │  14 rows × 8 columns      │  │   │
 │  │            │               │  │  │  (15 rows for MZ-2800)     │  │   │
 │  │  ┌─────────▼────────────┐  │  │  └────────────────────────────┘  │   │
 │  │  │ MZ2528::mapKey()     │──┼──┤                                  │   │
 │  │  │  internal → matrix   │  │  │                                  │   │
 │  │  └──────────────────────┘  │  │                                  │   │
 │  │                            │  │                                  │   │
 │  │  ┌──────────────────────┐  │  │                                  │   │
 │  │  │ WiFi / Web Server    │  │  │                                  │   │
 │  │  │ OTA / NVS / LED      │  │  │                                  │   │
 │  │  └──────────────────────┘  │  │                                  │   │
 │  └─────────────────────────────┘  └──────────────────────────────────┘   │
 └───────────────────────────────────────────────────────────────────────────┘

         PS/2 Keyboard                       MZ-2500 / MZ-2800
         ┌─────────┐                         ┌───────────────┐
         │ CLK/DAT │◄────── GPIO ──────────►│ RTSN/KDB/KDO  │
         └─────────┘                         │ KD4/MPX       │
                                             └───────────────┘

FreeRTOS Task Model

The ESP32's dual cores are assigned distinct roles to meet the sub-microsecond timing requirements of the MZ-2500/MZ-2800 keyboard protocol:
Task Core Priority Stack Purpose
mz25Interface Core 1 25 4096 MZ-2500 timing-critical GPIO strobe response
mz28Interface Core 1 MAX-1 2048 MZ-2800 timing-critical GPIO strobe response
hidInterface Core 0 0 4096 PS/2 and Bluetooth input polling and key mapping
Only one of mz25Interface or mz28Interface runs at any time, determined by the Kconfig build target (MZ25KEY_MZ2500 or MZ25KEY_MZ2800).
  • Core 0 (PRO_CPU): Runs the standard FreeRTOS scheduler with WiFi/BT stack tasks, the PS/2 keyboard interrupt handler, the HID polling task, LED blinking, switch monitoring, and the WiFi HTTP server. All non-timing-critical work happens here.
  • Core 1 (APP_CPU): Detached from FreeRTOS using a spinlock. It runs a bare-metal tight loop that monitors the host machine's RTSN strobe signal and responds within the required timing window (~600ns for MZ-2500, ~1.78us for MZ-2800). This core does not participate in FreeRTOS scheduling while the host interface is active.
The data flow for a keystroke follows this path:
  1. Physical key press: The PS/2 keyboard generates a scan code (set 2) and clocks it out over the PS/2 DATA and CLK lines, or a Bluetooth HID device sends an HID report.
  2. Input driver: PS2KeyAdvanced captures the scan code via GPIO interrupt and assembles it into a 16-bit internal code (upper byte = modifier flags, lower byte = keycode). For Bluetooth, BTHID translates the HID report into the same 16-bit format using a 179-entry lookup table.
  3. HID aggregator: The HID class polls both input sources and presents a unified stream of key events.
  4. Key mapping: MZ2528::mapKey() receives the 16-bit key code and looks it up in the 165-entry mapping table. The table maps PS/2 key codes to host-specific row/column pairs in the virtual key matrix.
  5. Virtual scan matrix: A 14-element (or 15-element for MZ-2800) array in memory is updated — the relevant bit in the row byte is set (key pressed) or cleared (key released).
  6. GPIO output: Core 1 continuously monitors RTSN. When the host requests a row, Core 1 reads the row index from KDB[3:0] and outputs the corresponding byte via KDO[7:0] through the 74HCT257 multiplexer. All done with IRAM_ATTR functions, spinlock, and direct register access.

MZ-2500 Keyboard Protocol

The MZ-2500 keyboard interface uses a strobe-based parallel protocol. The main unit's gate array continuously scans the 14-row by 8-column keyboard matrix by sending row addresses and reading back column data. The protocol sequence for a single row scan:
  1. RTSN rising edge: The host signals it is sending a row address. The ESP32 begins monitoring.
  2. Read KDB[3:0]: After ~160ns, the 4-bit row number (0-13) is valid on GPIO_IN_REG. The ESP32 reads it via direct register access.
  3. Set KDO[7:0] HIGH: All 8 output bits are initially set to 0xFF (inactive / no keys pressed).
  4. Check KDI4 (KD4): If KD4 is HIGH, the host wants the column data for the specific requested row. If KD4 is LOW, the host wants the STROBEALL value — the logical AND of all rows (used for idle key detection).
  5. Clear active key bits: For the requested row, the active key bits are cleared via GPIO.out_w1tc (atomic clear operation).
  6. RTSN falling edge: The host latches the data from the 74HCT257 multiplexer. The ESP32 waits for this edge before processing the next row.
Timing: approximately 1.2us per RTSN cycle. A full matrix scan of 14 rows takes approximately 16.8us. All hot-path code uses IRAM_ATTR to ensure it resides in instruction RAM (not flash cache), and direct GPIO register access (REG_READ/REG_WRITE) bypasses the ESP-IDF HAL for single-cycle (~4ns at 240MHz) performance.
  MZ-2500 Key Data Retrieval Protocol (one row scan cycle):

                 ┌────────── 660ns ──────────┐┌────────── 680ns ──────────┐
                 │                            ││                           │
        ┌────────┘                            └┘                           └────────┐
  RTSN  │   HIGH (row address phase)          │    LOW (data return phase)          │
        │                                     │                                     │
        │  ┌─ 160ns ─┐                        │                                     │
        │  │         │                        │                                     │
  KDB   │  │  ROW VALID                      │ ┌── nibble hi ──┐┌── nibble lo ──┐  │
  [3:0] │  │  (row 0-13)                     │ │   KDO[7:4]    ││   KDO[3:0]    │  │
        │  │                                  │ │               ││               │  │
        │                                     │                                     │
  KD4   ──── HIGH (row data mode) ──────────── HIGH ────────────────────────────────
        │                                     │                                     │
  MPX   ──────────────────────────────────────┘ ┌── 320ns ──┐┌── 320ns ──┐          │
        │                                       │   HIGH    ││   LOW     │          │
        │                                       │ upper nib ││ lower nib │          │
        └───────────────────────────────────────────────────────────────────────────┘
STROBEALL Mode: When no key is pressed, KD4 is held LOW by the host. The main unit sends a fixed row number continuously (row 4 on MZ-2500, row 0 on MZ-2800 — the value is ignored). In this mode, the keyboard returns the logical AND of all rows for each column — if any key in a column is pressed, the corresponding AND bit goes LOW, triggering the host to switch to key data retrieval mode where it scans each row individually.
Key Data Retrieval: When a key press is detected via STROBEALL, the host switches KD4 HIGH and begins scanning specific rows. It first probes row 11 (modifier keys: CTRL, SHIFT, LOCK, KANA, GRAPH) for approximately 30us to allow for debounce, then row 12 (Japanese transform keys) for approximately 50us, followed by a sequential scan from row 0 through row 13. When the row containing the pressed key is found, that row is scanned for over 600us for debounce verification before the sequential scan continues.
Performance note: Direct register access (REG_READ/REG_WRITE) is used instead of the Arduino digitalRead()/digitalWrite() functions. The Arduino GPIO functions add significant overhead (function call, parameter validation, pin mapping lookup) which would exceed the ~300ns timing window. Direct register access completes in a single clock cycle at 240MHz (~4ns).
The Core 1 spinlock is critical. It is acquired in app_main() before calling the host interface loop and is never released. This prevents FreeRTOS from scheduling any tasks on Core 1 or servicing interrupts, ensuring the RTSN monitoring loop runs with zero jitter:
// In app_main(), after all Core 0 tasks are created:
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
portENTER_CRITICAL(&mux);      // Disables interrupts, prevents scheduling
hostInterface->run();           // Never returns — tight RTSN polling loop
portEXIT_CRITICAL(&mux);       // Never reached

MZ-2800 Keyboard Protocol

The MZ-2800 uses the same physical signals as the MZ-2500 but with different timing and an additional row:
  • Longer setup times: KDB[3:0] row data trails RTSN by at least 650ns (compared to 160ns on the MZ-2500). The ESP32 must insert a longer delay before reading the row number.
  • Higher RTSN frequency: The RTSN period is approximately 1.78us.
  • Higher task priority: The MZ-2800 interface task runs at MAX_PRIORITIES-1 instead of priority 25.
  • Row 14: In native 80286 mode, the MZ-2800 scans a 15th row (row 14) containing keys specific to the 80286 operating system. In MZ-2500 compatibility mode, only rows 0-13 are scanned.
  • Shorter debounce: ~100us initial, ~300us repeat (compared to ~600us on the MZ-2500).
  • Different connector: The MZ-2800 uses an AMP 9-pin D-Sub connector (3M 10120-6000L/10220-5212PL compatible replacement). The MZ-2500 uses an 8-pin mini-DIN.
  • STROBEALL row: The MZ-2800 sends row 0 during STROBEALL mode; the MZ-2500 sends row 4. In both cases the row value is ignored.
These differences are handled automatically by the Kconfig build target selection (MZ25KEY_MZ2500 vs MZ25KEY_MZ2800), which adjusts timing constants and enables the 15th row.

Key Matrix Structure

The virtual scan matrix is 14 rows by 8 columns for the MZ-2500 (15 rows for MZ-2800). Each row is stored as a single byte with active-LOW logic — 0xFF means no keys pressed in that row:
  Row 0:  F1, F2, F3, F4, F5, NUM, GRAPH, KANA
  Row 1:  F6, F7, F8, F9, F10, HOME/CLR, semicolons, colon
  Row 2:  1, 2, 3, 4, 5, 6, 7, 8
  Row 3:  9, 0, -, ^, YEN, TAB, Q, W
  Row 4:  E, R, T, Y, U, I, O, P
  Row 5:  @, [, RETURN, A, S, D, F, G
  Row 6:  H, J, K, L, ;, :, ], SPACE
  Row 7:  Z, X, C, V, B, N, M, ,
  Row 8:  ., /, _, cursor down, cursor left, cursor right, cursor up, DEL/BS
  Row 9:  ROLLUP, ROLLDOWN, INS, CLR, cursor keys (alternate)
  Row 10: Tenkey 0, 1, 2, 3, 4, 5, 6, 7
  Row 11: Tenkey 8, 9, *, +, -, /, ., =
  Row 12: CTRL, SHIFT, LOCK, KANA_LOCK, GRAPH_LOCK
  Row 13: Japanese transform keys (HENKAN, MUHENKAN, etc.)
  Row 14: MZ-2800 specific keys (80286 mode only — not scanned on MZ-2500)

Source Modules

The firmware comprises 17 source files organised into four groups. All source resides in the main/ directory. For mz25key builds, only the MZ2528 host interface is active — the other host interface modules (X1, X68K, PC9801, MZ5665) are compiled but their code paths are never entered.
File Class Purpose
SharpKey.cpp Application entry point, GPIO host detection, FreeRTOS task creation
MZ2528.h / MZ2528.cpp MZ2528 MZ-2500/MZ-2800 host keyboard interface (timing-critical, Core 1)
X1.h / X1.cpp X1 Sharp X1 series host interface (compiled but unused)
X68K.h / X68K.cpp X68K Sharp X68000 host interface (compiled but unused)
PC9801.h / PC9801.cpp PC9801 NEC PC-9801 host interface (compiled but unused)
MZ5665.h / MZ5665.cpp MZ5665 Sharp MZ-5600/6500 host interface (compiled but unused)
Mouse.h / Mouse.cpp Mouse PS/2 mouse pass-through
KeyInterface.h / KeyInterface.cpp KeyInterface Virtual base class for all host interfaces
HID.h / HID.cpp HID Input device aggregator (PS/2 + Bluetooth)
PS2KeyAdvanced.h / PS2KeyAdvanced.cpp PS2KeyAdvanced PS/2 keyboard driver (interrupt-driven, scan code set 2)
PS2Mouse.h / PS2Mouse.cpp PS2Mouse PS/2 mouse driver
BT.h / BT.cpp BT Bluetooth GAP/GATT device discovery and pairing
BTHID.h / BTHID.cpp BTHID Bluetooth HID report translation (179-entry lookup table)
NVS.h / NVS.cpp NVS Thread-safe non-volatile storage (mutex-protected key-value)
LED.h / LED.cpp LED Status LED (OFF/ON/BLINK/BLINK_ONESHOT with duty cycle)
SWITCH.h / SWITCH.cpp SWITCH Timed button handler (1s=cancel, 2-4s=WiFi, 5-9s=AP, 10-14s=BT, 15-19s=reset)
WiFi.h / WiFi.cpp WiFi WiFi AP/client, HTTP server, REST API, OTA, LittleFS
All host interfaces inherit from KeyInterface, which defines the common API: virtual void hostInterface() (the main loop driving GPIO signals, marked IRAM_ATTR for timing-critical interfaces), virtual void mapKey(uint16_t keyCode) (translates 16-bit key codes into matrix positions), virtual void persistConfig() (saves active keymap to NVS), and virtual uint8_t* getKeyMapData(size_t *len) (returns binary keymap data for the web interface).

Key Mapping System

The key mapping system is the core of the mz25key's functionality. It translates PS/2 scan codes into the specific row/column activations expected by the MZ-2500/MZ-2800 keyboard controller. The system supports multiple mapping layers and runtime switching between keyboard layouts.

Scan Code Pipeline

Each keystroke passes through three translation stages before reaching the host machine:
  PS/2 Scan Code Set 2    Internal 16-bit Code      MZ Matrix Position
  ┌──────────────────┐    ┌──────────────────┐      ┌──────────────────┐
  │ Make: 0x1C       │    │ Upper: modifiers │      │ Row: 5           │
  │ Break: 0xF0 0x1C │──►│ Lower: 0x41 (A)  │──►  │ Col bit: 0x08    │
  │ (PS/2 protocol)  │    │ (PS2KeyAdvanced) │      │ (MZ2528::mapKey) │
  └──────────────────┘    └──────────────────┘      └──────────────────┘

  Stage 1: PS2KeyAdvanced     Stage 2: HID class     Stage 3: MZ2528::mapKey()
  (interrupt-driven,          (normalisation,         (165-entry table lookup,
   scan code set 2)            modifier tracking)      row/column output)
The 16-bit internal key code format uses the upper byte for modifier flags and the lower byte for the key identifier:
  Bit 15    Bit 14    Bit 13    Bit 12    Bit 11    Bit 10    Bit 9     Bit 8
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│  BREAK  │  SHIFT  │  CTRL   │  CAPS   │  ALT    │ ALT_GR  │  GUI    │FUNCTION │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
  Bits 7-0: Key code (unique identifier for each physical key)

Mapping Table

The MZ2528 mapping table (PS2TBL[] in MZ2528.h) contains 165+ entries. Each entry maps a PS/2 key code to one or two positions in the virtual key matrix. The table fields are:
  Column          Type      Description
  ──────────────  ────────  ──────────────────────────────────────────────────
  PS2 Keycode     uint8_t   Lower byte of the 16-bit internal key code
  Ctrl Key        uint8_t   Modifier key requirements (PS2CTRL_NONE, SHIFT, etc.)
  Keyboard Model  uint8_t   PS/2 keyboard model filter (0 = all models)
  Host Model      uint8_t   Target machine (MZ_ALL, MZ_80B, MZ_2000, MZ_2500, MZ_2800)
  Make Row1       uint8_t   Row in virtual matrix to activate on key press
  Make Key1       uint8_t   Column bit mask to set in that row
  Make Row2       uint8_t   Second row (for SHIFT+key combos; 0xFF = unused)
  Make Key2       uint8_t   Second column bit mask
  Break Row1      uint8_t   Row to deactivate on key release
  Break Key1      uint8_t   Column to clear on release
  Break Row2      uint8_t   Second row to deactivate on release
  Break Key2      uint8_t   Second column bit mask on release
A single PS/2 key press can activate up to two positions in the virtual matrix simultaneously. This is essential for keys that need to appear as a modifier+key combination on the host — for example, mapping PS/2 @ to the MZ-2500 SHIFT+2 position. Machine-specific variants (MZ_ALL, MZ_80B, MZ_2000, MZ_2500, MZ_2800) allow the same table to serve different keyboard layouts.

Updating Key Mappings

Key mappings can be modified in two ways:
Method 1: Web Interface (recommended) — When WiFi is enabled, navigate to the keymap editor page (keymap.html). The editor displays the current mapping table and allows editing, saving, exporting, and importing keymaps. Changes are saved as binary keymap files on the LittleFS partition and take effect immediately.
Method 2: Source Code — Edit the PS2TBL[] array in MZ2528.h. Each row follows the format described above. After editing, rebuild and reflash the firmware.
Example of adding a key mapping entry:
// Map PS/2 F11 to MZ row 3, bit 7 (HELP key)
// { PS2Code, CtrlKey, KbdModel, HostModel, MkRow1, MkKey1, MkRow2, MkKey2,
//   BkRow1, BkKey1, BkRow2, BkKey2 }
{PS2_KEY_F11, PS2CTRL_NONE, 0, MZ_ALL, {3, 0x80, 0, 0, 0, 0}, {3, 0x80, 0, 0}},

// Map PS/2 Backslash to MZ row 6, bit 2 (single-position mapping)
// 0xFF in Row2 means no second matrix position needed
{0x5D, 0x00, 0x00, 0x00, 6, 0x04, 0xFF, 0x00, 6, 0x04, 0xFF, 0x00},
The ALT+F1/F2/F3 hotkeys switch between pre-loaded keymap files at runtime:
  • ALT+F1: MZ-2500 layout (default) — the standard 14-row matrix.
  • ALT+F2: MZ-2000 layout — adjusts the matrix to match MZ-2000 key positions.
  • ALT+F3: MZ-80B layout — reconfigures for the MZ-80B keyboard matrix arrangement.
The active layout is saved to NVS and persists across power cycles. Each layout uses a different subset of rows in the mapping table, filtered by the Host Model column.

Supported Keyboards

The following PS/2 and Bluetooth keyboards have been tested with the mz25key interface:
  • Wyse KB-3926 (PS/2, compact layout)
  • OADG 109 Japanese keyboard (PS/2)
  • Sanwa SKBL1 (PS/2, backlit)
  • Periboard 810 (Bluetooth, compact)
  • Omoton K8508 (Bluetooth, slim)
  • Standard IBM Model M (PS/2, via DIN-to-PS/2 adapter)
  • Cherry G80 series (PS/2)
  • Logitech K380 (Bluetooth multi-device)
Any PS/2 keyboard that sends scan code set 2 (the default for AT/PS/2 keyboards) should work. Bluetooth keyboards must support the HID profile (UUID 0x1124). The Keyboard Model field in the mapping table allows model-specific overrides for keyboards with non-standard scan code behaviour.

Binary Keymap Files

In addition to the compiled-in static mapping table, the mz25key supports loading custom keymaps from binary files stored on the LittleFS filesystem partition. Binary keymap files follow the naming convention:
  MZ2528_KeyMap.BIN     — custom keymap for MZ-2500/MZ-2800
The binary file contains the same data as the static array but in a packed binary format. When a binary keymap file exists on the filesystem, it takes precedence over the compiled-in table. This allows end users to customise key mappings through the web interface without recompiling the firmware. The web UI keymap editor downloads the current keymap via the REST API, displays it in an editable HTML table, validates changes, and uploads the modified keymap back to the device as a MZ2528_KeyMap.BIN file on the LittleFS partition.

GPIO Configuration

The ESP32 GPIO pin assignments are configured via Kconfig and match the SharpKey PCB layout. The key pins are:
Signal GPIO Direction Notes
RTSNI (Row Strobe) 35 Input Input-only pin, read via GPIO_IN1_REG bank
KDB0 36 Input Row select bit 0 (input-only)
KDB1 37 Input Row select bit 1 (input-only)
KDB2 38 Input Row select bit 2 (input-only)
KDB3 39 Input Row select bit 3 (input-only)
KDO0 12 Output Column data bit 0 to 74HCT257
KDO1 13 Output Column data bit 1
KDO2 14 Output Column data bit 2
KDO3 15 Output Column data bit 3
KDO4 16 Output Column data bit 4
KDO5 17 Output Column data bit 5
KDO6 18 Output Column data bit 6
KDO7 19 Output Column data bit 7
KDI4 34 Input KD4 type strobe (input-only)
MPXI 33 Input MPX nibble select
PS/2 CLK 26 Input PS/2 keyboard clock (interrupt)
PS/2 DATA 27 Input/Output PS/2 keyboard data (bidirectional)
LED 2 Output Status LED
SWITCH 0 Input WiFi/mode button
Important notes:
  • RTSNI on GPIO 35: This is an input-only pin on the ESP32. It is read via the GPIO_IN1_REG bank (GPIOs 32-39), not GPIO_IN_REG (GPIOs 0-31). The firmware uses a pre-computed bit mask for direct register access.
  • KDO0-7 output: These 8 pins drive the 74HCT257 quad 2:1 multiplexer. The MPX signal from the host directly controls the multiplexer to select the upper or lower nibble, eliminating the need for the ESP32 to respond to MPX timing.
  • ADC2/WiFi GPIO conflict: GPIOs 0, 2, 4, 12-15, 25-27 are shared with ADC2, which cannot be used while WiFi is active. The PCB design uses these pins for digital GPIO only, avoiding the conflict.

Bluetooth Integration

The mz25key firmware includes Bluetooth Classic HID support, allowing wireless keyboards to be used as an input source instead of or in addition to the PS/2 connector. The Bluetooth stack is split across two classes: BT handles GAP/GATT device discovery and connection management; BTHID handles HID report parsing and translation to the internal key code format.
  • Protocols: Bluetooth Classic + BLE HID support.
  • Bonded devices: Up to 5 devices can be stored in NVS.
  • Pairing window: 60 seconds after initiating scan via the button (hold 10-14 seconds).
  • PIN: "1234" for devices that require legacy pairing. Most modern devices use SSP (no PIN).
  • Priority: PS/2 keyboard input has priority over Bluetooth when both are connected. Events from both sources are merged by the HID aggregator.
Pairing process:
  1. Press and hold the mz25key button for 10-14 seconds. The LED enters fast-blink mode.
  2. Put the Bluetooth keyboard into pairing mode (usually by holding its pairing button).
  3. The mz25key scans for BT HID devices for approximately 60 seconds.
  4. When a device is found, pairing initiates automatically (SSP, no PIN required for most devices).
  5. On success, the LED returns to normal blink. The device address is saved to NVS for automatic reconnection on subsequent power-ups.
Limitations: WiFi and Bluetooth are mutually exclusive in the current firmware to avoid latency spikes on the keyboard interface. Enabling WiFi disables Bluetooth and vice versa. The ESP32 uses a single antenna shared between both radios.

WiFi and Web Interface

The mz25key firmware includes an embedded web server for browser-based configuration, key mapping, and firmware management. The web interface is identical to SharpKey's and runs on Core 0 as a FreeRTOS task.
  • AP mode: The mz25key creates its own WiFi network (default SSID: sharpkey). Connect a device to this network and access the web interface at http://192.168.4.1.
  • Client mode: The mz25key connects to an existing WiFi network using credentials stored in NVS. The web interface is accessible at the DHCP-assigned IP address.
  • Mode switching: Hold the button 2-4 seconds for client mode, 5-9 seconds for AP mode. A reboot is required to switch between WiFi and Bluetooth.
The web interface serves files from a LittleFS filesystem partition (640 KB, named filesys). Five HTML pages are available:
Page URL Purpose
index.html / Dashboard — firmware version, host type, WiFi/BT status
keymap.html /keymap.html Visual key mapping editor
mouse.html /mouse.html Mouse configuration (if applicable)
ota.html /ota.html Firmware and filesystem OTA update
wifimanager.html /wifimanager.html WiFi network scan, credentials, mode switching
REST API endpoints for programmatic access:
Method Endpoint Description
GET /api/status Device status (version, host type, uptime)
GET /api/keymap Download current keymap as binary
POST /api/keymap Upload new keymap
GET /api/wifi/scan Trigger WiFi scan and return results
POST /api/wifi/connect Connect to a WiFi network
POST /api/ota/firmware Upload firmware update
POST /api/ota/filesystem Upload filesystem update
POST /api/reboot Trigger device reboot

Building the Web Filesystem (build_webfs.sh)

The build_webfs.sh script prepares the web files for inclusion in the firmware image:
  1. Copies all HTML files from webserver/html/ into the webfs/ staging directory.
  2. Compresses CSS and JavaScript files with gzip (the HTTP server serves them with Content-Encoding: gzip to save flash space).
  3. Copies any binary assets (default keymap files, favicon) into webfs/.
  4. The ESP-IDF build system then packages the webfs/ directory into a LittleFS filesystem image (build/filesys.bin) that is flashed to the filesys partition.
# Build the web filesystem before building firmware
chmod +x build_webfs.sh
./build_webfs.sh

# The output is used automatically by the ESP-IDF build
If you modify any web page, JavaScript, or CSS file, re-run build_webfs.sh before rebuilding the firmware. The LittleFS partition is 640 KB — this limits the total size of all web assets including compressed JavaScript and CSS.
Adding new web pages: Create the HTML file in webserver/html/. If the page needs new REST API endpoints, add the handler registrations in WiFi.cpp in the startWebServer() function. Add a navigation link in index.html. Run build_webfs.sh to package the new files. Rebuild and reflash both firmware and filesystem.

OTA Updates

The mz25key supports Over-The-Air firmware and filesystem updates via the web interface. This allows updating the device without a physical UART connection — only WiFi access is needed.

Partition Scheme

The ESP32 flash is divided into partitions defined in sharpkey_partition_table.csv:
  Name            Type    SubType   Offset     Size       Description
  ──────────────  ──────  ────────  ─────────  ─────────  ────────────────────────────
  nvs             data    nvs       0x9000     0x5000     Non-volatile storage
  otadata         data    ota       0xE000     0x2000     OTA boot selection data
  ota_0           app     ota_0     0x10000    0x1A0000   Firmware bank 0 (1664 KB)
  ota_1           app     ota_1     0x1B0000   0x1A0000   Firmware bank 1 (1664 KB)
  filesys         data    0x80     0x350000   0xA0000    LittleFS web filesystem (640 KB)
  coredump        data    coredump  0x3F0000   0x10000    Core dump storage (64 KB)
The dual-bank OTA scheme (ota_0 and ota_1) means the device always has a known-good firmware image. New firmware is written to the inactive bank while the active bank continues running. After a successful write and SHA-256 verification, the otadata partition is updated to boot from the new bank on the next reset. If the new firmware fails to boot, the boot loader automatically rolls back to the previous bank.
Update process:
  1. Connect to the mz25key web interface (AP or client mode).
  2. Navigate to /ota.html.
  3. Select "Firmware Update" (uploads main.bin) or "Filesystem Update" (uploads filesys.bin).
  4. Choose the binary file and click Upload. A progress bar shows transfer status.
  5. After upload, the device verifies image integrity (SHA-256 hash check).
  6. If verification passes, the device reboots into the new firmware. Version validation prevents downgrade.

eFuse Custom Fields

The ESP32 contains one-time-programmable eFuse bits that can be used to store permanent device information. The mz25key firmware defines custom eFuse fields in EFUSE_BLK3, generated by the ESP-IDF efuse_table_gen.py tool from the CSV definition file (main/esp_efuse_custom_table.csv):
Field Block Bits Description
HARDWARE_REVISION BLOCK3 0-7 PCB hardware revision number (e.g. 0x12 = v1.2)
SERIAL_NO BLOCK3 8-23 Unique serial number for the device (16-bit)
DISABLE_RESTRICTIONS BLOCK3 24-31 Feature gate flags — each bit enables/disables a restricted feature
RESERVED1 BLOCK3 32-47 Reserved for future use
BUILD_DATE BLOCK3 48-63 Build date encoded as days since 2020-01-01
# Read all eFuse values
espefuse.py --port /dev/ttyUSB0 summary

# Write the hardware revision (WARNING: one-time-programmable, cannot be undone!)
espefuse.py --port /dev/ttyUSB0 burn_efuse BLOCK3 0x12 --offset 0 --size 8

# Write the serial number
espefuse.py --port /dev/ttyUSB0 burn_efuse BLOCK3 0x0042 --offset 8 --size 16
WARNING: eFuse bits are one-time-programmable. Once a bit is set to 1, it cannot be reverted to 0. Double-check all values before writing. The DISABLE_RESTRICTIONS field allows different product SKUs to be built from the same firmware image by programming different eFuse patterns during manufacturing.
The eFuse table is generated during the CI/CD build. The .gitignore excludes the generated C source file but includes the CSV definition, ensuring the field definitions are version-controlled while the generated code is always rebuilt from the CSV.
Feature gating via DISABLE_RESTRICTIONS: The DISABLE_RESTRICTIONS field contains 8 bits, each controlling access to a specific feature. When a bit is 0 (default, unprogrammed), the feature is restricted; when set to 1, the feature is enabled. The firmware reads this field at boot and configures feature availability accordingly. This mechanism allows different product SKUs to be built from the same firmware image by programming different eFuse patterns during manufacturing.

Build Environment

Native Build Setup

To build the mz25key firmware natively, install ESP-IDF v4.4 following the Espressif Getting Started Guide:
# Install ESP-IDF v4.4
mkdir -p ~/esp
cd ~/esp
git clone -b v4.4 --recursive https://github.com/espressif/esp-idf.git esp-idf-v4.4
cd esp-idf-v4.4
./install.sh

# Activate the environment (needed in every new terminal session)
. ~/esp/esp-idf-v4.4/export.sh

Clone and Build

CRITICAL: Unlike SharpKey, the mz25key repository does not use git submodules. You must manually clone the two component libraries at the correct versions before building. This is the most common source of build failures.
# Clone the mz25key repository (no --recursive needed)
git clone https://git.eaw.app/eaw/mz25key.git
cd mz25key

# Clone required components manually (NOT submodules!)
mkdir -p components

# Arduino-ESP32 compatibility layer — must be exactly v2.0.3
git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32
cd components/arduino-esp32 && git checkout 2.0.3 && cd ../..

# LittleFS filesystem support — must be v1.3.1 WITH submodules initialised
git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs
cd components/esp_littlefs && git checkout v1.3.1 && git submodule update --init --recursive && cd ../..

# Configure the build target (select MZ25KEY_MZ2500 or MZ25KEY_MZ2800)
idf.py menuconfig

# Build the web filesystem (required before first firmware build)
chmod +x build_webfs.sh && ./build_webfs.sh

# Build the firmware
idf.py build

# Flash to the device
idf.py -p /dev/ttyUSB0 flash

# Flash and monitor in one command
idf.py -p /dev/ttyUSB0 flash monitor
Build output files:
  build/main.bin                           — Firmware binary (for OTA update)
  build/filesys.bin                        — Web filesystem image (for OTA update)
  build/bootloader/bootloader.bin          — Bootloader
  build/partition_table/partition-table.bin — Partition table
  build/ota_data_initial.bin               — OTA data partition

Run idf.py menuconfig to open the configuration editor. The key mz25key-specific options are under the SharpKey Configuration menu:
  • Build Target: Select mz25key - MZ2500 or mz25key - MZ2800. This sets the Kconfig symbol MZ25KEY_MZ2500 or MZ25KEY_MZ2800 and determines the protocol timing, row count, and task priority.
  • PS/2 Keyboard GPIO: Configure the DATA and CLK pins for the PS/2 keyboard interface.
  • Host Interface GPIO: Pin assignments for RTSN, KDB[3:0], KDO[7:0], KD4, MPX.
  • WiFi Settings: Default SSID, password, AP mode channel.
  • Bluetooth: Enable/disable Bluetooth HID stack.
  • Debug Options: Enable serial debug output (CONFIG_DEBUG_SERIAL), disable individual GPIO groups for testing.
A pre-configured sdkconfig file is included in the repository with sensible defaults for the MZ-2500 target. You only need to run menuconfig if you want to change the target machine, pin assignments, or enable debug features.

Docker Build

An alternative to installing ESP-IDF natively is to use Espressif's official Docker image. This requires only Docker — the entire toolchain runs inside the container:
# Clone and prepare (same manual component cloning required)
git clone https://git.eaw.app/eaw/mz25key.git
cd mz25key
mkdir -p components
git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32
cd components/arduino-esp32 && git checkout 2.0.3 && cd ../..
git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs
cd components/esp_littlefs && git checkout v1.3.1 && git submodule update --init --recursive && cd ../..

# Build web filesystem
chmod +x build_webfs.sh && ./build_webfs.sh

# Build firmware using Docker
docker run --rm -v $(pwd):/project -w /project espressif/idf:v4.4 idf.py build

# Flash via Docker (requires USB device access)
docker run --rm --privileged \
    --volume /dev:/dev --volume /sys:/sys:ro --volume /dev/bus/usb:/dev/bus/usb \
    -v $(pwd):/project -w /project \
    espressif/idf:v4.4 \
    idf.py -p /dev/ttyUSB0 build flash monitor
For convenience, create a shell alias:
# Add to ~/.bashrc or ~/.zshrc
alias idf44='docker run --rm --privileged --volume /dev:/dev --volume /sys:/sys:ro \
    --volume /dev/bus/usb:/dev/bus/usb -v $PWD:/project -w /project -it \
    espressif/idf:v4.4 idf.py "$@"'

# Then use:
idf44 build
idf44 menuconfig
idf44 -p /dev/ttyUSB0 flash monitor

Continuous Integration (Jenkins)

What is CI/CD? Continuous Integration (CI) is a practice where a dedicated server automatically builds your project every time you push code changes. Instead of manually compiling the firmware and uploading release files by hand, a CI server clones the repository, builds the firmware, packages the release artifacts, and publishes them to a download page — all automatically. If the build breaks, you receive an email notification immediately.
The mz25key project uses Jenkins running on a VPS to automatically build the ESP32 firmware on every push to the master branch. The build uses the Espressif IDF v4.4 Docker image, so no native ESP-IDF installation is needed on the server.

Pipeline Overview

The mz25key Jenkins pipeline is similar to SharpKey's but includes an additional "Fetch Components" stage. The pipeline stages are:
  1. Checkout: Clean workspace, clone repository from Gitea. Note: NO submodule init is performed (there are no submodules).
  2. Fetch Components: Clone arduino-esp32 at tag 2.0.3 and esp_littlefs at tag v1.3.1 (with submodules) into components/. This stage is unique to mz25key — SharpKey uses git submodule update --init --recursive instead.
  3. Determine Version: Auto-increment from the latest Gitea release tag, or read from webserver/version.txt.
  4. Build Web Filesystem: Run build_webfs.sh to package HTML, CSS, and JavaScript into the LittleFS image.
  5. Generate eFuse Table: Run efuse_table_gen.py inside the IDF Docker container to generate the eFuse C source from the CSV definition.
  6. Build Firmware: Launch the espressif/idf:v4.4 Docker container and run idf.py build.
  7. Package Release: Create versioned FW, FilePack, and FlashPack archives.
  8. Create Gitea Release: Upload artifacts to Gitea as a tagged release.

Fetch Components Stage

This is the key CI/CD difference from SharpKey. Because the mz25key repository has no .gitmodules, the pipeline must explicitly clone the component libraries:
stage('Fetch Components') {
    steps {
        sh """
            mkdir -p components

            git clone https://github.com/espressif/arduino-esp32.git components/arduino-esp32
            cd components/arduino-esp32 && git checkout 2.0.3 && cd ../..

            git clone https://github.com/joltwallet/esp_littlefs.git components/esp_littlefs
            cd components/esp_littlefs && git checkout v1.3.1 \
                && git submodule update --init --recursive && cd ../..
        """
    }
}
Critical note: The esp_littlefs component requires its submodules to be initialised. Without running git submodule update --init --recursive inside components/esp_littlefs/, the mklittlefs tool will not be available and the build will fail with a "No rule to make target 'dist'" error during the web filesystem generation step.

Webhook Configuration

In the Gitea repository, go to Settings -> Webhooks -> Add Webhook -> Gitea and set:
  • Target URL: http://your-server:8080/generic-webhook-trigger/invoke?token=mz25key-build-trigger
  • Content Type: application/json
  • Trigger On: Push Events
  • Branch Filter: refs/heads/(main|master)
After saving, push a commit to master and check Jenkins — a new build should appear automatically.

Version Management

The mz25key uses the same version scheme as SharpKey but reads the version from a different location. The CI pipeline reads webserver/version.txt (not version.txt in the project root as SharpKey does). Version auto-incrementing works by querying the latest Gitea release tag and bumping the minor version.

Release Artifacts

Three release artifacts are produced by each successful build:
  • mz25key-FW-vX.XX.bin.gz — Gzip-compressed firmware binary for OTA update via the web interface. Upload to the device through /ota.html as a "Firmware" update.
  • mz25key-FilePack-vX.XX.bin.gz — Gzip-compressed LittleFS filesystem image for OTA update. Upload as a "Filesystem" update to update web pages, keymaps, and other web-served content.
  • mz25key-FlashPack-vX.XX.tar.gz — Complete flash image archive containing bootloader, partition table, firmware, filesystem, and OTA data. Used for initial programming via esptool.py or for factory recovery.

Pipeline Configuration

The Jenkins server setup is shared with the FusionX and SharpKey projects. See the FusionX Developer's Guide — Continuous Integration section for the full Jenkins installation walkthrough including Docker, docker-compose.yml, initial configuration, and plugin installation.
The Jenkins docker-compose.yml must mount the Docker socket so the pipeline can launch the ESP-IDF container as a sibling:
volumes:
  - /srv/jenkins/data:/var/jenkins_home
  - /var/run/docker.sock:/var/run/docker.sock    # Required for ESP-IDF sibling container
The pipeline uses Docker sibling containers (not Docker-in-Docker), so workspace paths must be mapped correctly. The WORKSPACE_HOST_PATH environment variable provides the host-side path to the Jenkins workspace, which is passed to the ESP-IDF Docker container via -v. Post-build notifications are sent via email on both success and failure.

eFuse Table Generation

The "Generate eFuse Table" pipeline stage runs efuse_table_gen.py inside the IDF Docker container to convert the CSV eFuse field definitions (main/esp_efuse_custom_table.csv) into a C source file (main/esp_efuse_custom_table.c). The .gitignore excludes the generated .c file but includes the .csv source, ensuring the definitions are version-controlled while the generated code is always rebuilt from the authoritative CSV during CI.

Differences from SharpKey CI Pipeline

The mz25key CI pipeline differs from SharpKey's in the following specific ways:
  • Fetch Components stage: mz25key explicitly clones arduino-esp32 and esp_littlefs into components/. SharpKey uses git submodule update --init --recursive during the Checkout stage instead.
  • Generate eFuse Table stage: mz25key includes an additional pipeline stage that generates the eFuse configuration table from the CSV definition. The SharpKey pipeline handles this within the Build stage.
  • Version file location: mz25key reads its version from webserver/version.txt, whereas SharpKey reads from version.txt in the project root.
  • Artifact naming: Release artifacts use the mz25key- prefix (e.g. mz25key-FW-v1.00.bin.gz) instead of SharpKey-.
  • Webhook token: Uses mz25key-build-trigger (not sharpkey-build-trigger).
  • No submodule init: The Checkout stage does not run any submodule commands because there are no submodules. SharpKey's Checkout stage runs git submodule update --init --recursive.

Debugging

Debugging an embedded firmware that interfaces with vintage hardware at sub-microsecond timing can be challenging. The following techniques and tools help diagnose common issues.

Serial Monitor

Enable CONFIG_DEBUG_SERIAL in menuconfig to activate verbose debug output over the UART. Connect a USB-to-TTL adapter to the programming header and run:
idf.py -p /dev/ttyUSB0 monitor

# Or with Docker:
docker run --rm --privileged --volume /dev:/dev --volume /sys:/sys:ro \
    -v $PWD:/project -w /project -it espressif/idf:v4.4 \
    idf.py -p /dev/ttyUSB0 monitor
The firmware uses the ESP-IDF logging framework. Key log tags include:
  Tag         Source              Description
  ──────────  ──────────────────  ───────────────────────────────────────
  sharpkey    SharpKey.cpp        Application-level events (init, host detection)
  MZ2528      MZ2528.cpp          MZ-2500/MZ-2800 interface events
  HID         HID.cpp             Key event processing
  PS2         PS2KeyAdvanced.cpp  PS/2 scan code reception
  BT          BT.cpp              Bluetooth discovery and pairing
  BTHID       BTHID.cpp           Bluetooth HID report translation
  WiFi        WiFi.cpp            WiFi and HTTP server events
  NVS         NVS.cpp             Configuration storage
  LED         LED.cpp             LED state changes
  SWITCH      SWITCH.cpp          Button press events
Set the log level per-tag to focus on specific subsystems:
esp_log_level_set("*", ESP_LOG_WARN);        // Default: warnings and errors only
esp_log_level_set("MZ2528", ESP_LOG_DEBUG);   // Verbose MZ2528 output

GPIO Debug Flags

The menuconfig debug menu provides options to selectively disable individual GPIO groups. This is invaluable for isolating problems through progressive testing — start with all groups disabled and enable them one at a time:
  • CONFIG_DEBUG_DISABLE_KDB: Disables the keyboard bidirectional data bus (KD[3:0]). Use when testing host strobe detection without driving data back.
  • CONFIG_DEBUG_DISABLE_KDO: Disables the keyboard data output (scan data to multiplexer). Use when testing input-side logic only.
  • CONFIG_DEBUG_DISABLE_RTSNI: Disables the RTSN input monitoring. Use to test passive matrix updates without host polling.
  • CONFIG_DEBUG_DISABLE_MPXI: Disables the MPX input monitoring. Use when debugging row strobe detection independently of nibble multiplexing.
  • CONFIG_DEBUG_DISABLE_KDI: Disables the KD4 input. Use when testing without the type strobe signal.

Common Issues

  • Components not cloned — "component not found" errors:
    • The most common build failure. After cloning the mz25key repository, the components/ directory is empty. You must manually clone arduino-esp32 (v2.0.3) and esp_littlefs (v1.3.1) as described in the Clone and Build section.
  • esp_littlefs submodules missing — mklittlefs build fails:
    • The esp_littlefs component has its own git submodules (including the mklittlefs tool). If you forget to run git submodule update --init --recursive inside components/esp_littlefs/, the web filesystem build will fail with "No rule to make target 'dist'".
  • Timing issues — host not detecting keys:
    • Ensure the MZ2528 interface is pinned to Core 1 with the spinlock. Any FreeRTOS scheduling on Core 1 will cause missed RTSN strobes.
    • Verify all hot-path functions have IRAM_ATTR — flash cache misses add hundreds of nanoseconds of latency.
    • Use a logic analyser to verify the RTSN response time is within the required window.
  • ADC2/WiFi GPIO conflict:
    • GPIOs 0, 2, 4, 12-15, 25-27 are shared with ADC2. Analog reads on these pins return garbage when WiFi is active. The mz25key PCB uses these pins for digital GPIO only.
  • eFuse CSV not committed:
    • The .gitignore may exclude *.c files in the eFuse directory. Ensure the CSV definition file (esp_efuse_custom_table.csv) is committed. The generated C file is rebuilt during CI.
  • MZ-2800 mode keys not working:
    • Ensure the build target is MZ25KEY_MZ2800 (not MZ25KEY_MZ2500). The MZ-2800 has longer setup times (650ns for KDB vs 160ns) and an additional 15th row.
  • PS/2 keyboard not detected:
    • Check PS/2 DATA and CLK wiring. PS/2 uses open-collector signalling; 4.7K pull-up resistors to 5V are required.
    • Some keyboards require a power cycle after ESP32 reset.
  • WiFi will not connect:
    • The ESP32 supports 2.4GHz only — it cannot connect to 5GHz networks.
    • WiFi and Bluetooth are mutually exclusive. Disable BT before enabling WiFi.
  • Bluetooth keyboard not pairing:
    • Ensure WiFi is disabled (mutual exclusion).
    • Put the keyboard into pairing mode before initiating the scan from the mz25key.
    • Some keyboards require removing the existing pairing first.
  • OTA update fails:
    • Ensure the correct file type — main.bin for firmware, filesys.bin for filesystem.
    • Version validation prevents downgrade — upload a newer version.
  • ESP32 core panic / reboot loop:
    • Usually a stack overflow or null pointer. The UART monitor shows the panic register dump. Increase stack sizes in menuconfig if needed.

Reference Sites

The following resources are useful for mz25key development:
Resource URL
mz25key Project Page https://eaw.app/mz25key/
mz25key Gitea Repository https://git.eaw.app/eaw/mz25key
SharpKey Developer’s Guide https://eaw.app/sharpkey-developersguide/
SharpKey Gitea Repository https://git.eaw.app/eaw/SharpKey
ESP-IDF v4.4 Documentation https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/
ESP-IDF Arduino Component https://docs.espressif.com/projects/arduino-esp32/en/latest/esp-idf_component.html
esp_littlefs Component https://github.com/joltwallet/esp_littlefs
PS/2 Scan Code Set 2 OSDev Wiki — PS/2 Scan Code Set 2
USB HID Usage Tables USB.org — HID Usage Tables 1.4
ESP32 Technical Reference ESP32 Technical Reference Manual
FreeRTOS Documentation FreeRTOS Reference Manual
FusionX CI/CD Setup Guide Jenkins CI/CD Setup
Sharp MZ-2500 Technical Manual Available from original.sharpmz.org
GPL v3 License https://www.gnu.org/licenses/gpl-3.0.en.html

Wireless Regulatory Notice

This device incorporates a pre-certified ESP32-S wireless module (AI Thinker) that transmits in the 2.4 GHz ISM band, making it an intentional radiator under radio-frequency regulations worldwide (including FCC Part 15 Subpart C in the United States, and the Radio Equipment Directive 2014/53/EU in the European Union).
The ESP32-S module is used unmodified and carries its own regulatory certifications (FCC, CE, and others). However, those module-level certifications do not automatically extend to a finished product that incorporates the module. The pre-certified module exemption permits individual hobbyists to build a limited number of devices for personal, experimental, or educational use without obtaining separate equipment authorisation.
Important Limitations
  • Assembled devices must not be sold, offered for sale, gifted, or otherwise distributed to third parties unless the finished product has been independently tested and granted its own equipment authorisation (e.g. FCC ID, CE marking with a Notified Body assessment) in the relevant jurisdiction.
  • Building this project for personal use in limited quantities is generally permitted under hobbyist and experimental-use provisions (e.g. FCC § 15.23), provided the device does not cause harmful interference.
  • Regulatory requirements vary by country. Builders outside the United States should consult their national radio-frequency authority for applicable rules.
Builder’s Responsibility
It is the builder’s sole responsibility to ensure that any device constructed from these designs complies with all applicable radio-frequency regulations in their jurisdiction. The author provides these designs for personal, educational, and hobbyist use and makes no representation that a device built from them satisfies the regulatory requirements for commercial distribution.