SFT-5988: Keypad test with multiple colors and press/release visualization
What changed, and why it matters
This commit adds documentation for a factory test mode and improves the on-screen keypad test so it shows different colors when keys are pressed and released. It is a routine manufacturing/quality-assurance change with no security relevance.
No security action required. Treat as a normal factory-test and UI improvement commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change consists of two parts: (1) a new README_FACTORY_TEST.md describing how to build and use the Passport factory-test bootloader, including a shared SRAM4 memory interface at 0x38000800 for triggering tests such as LCD, camera, EEPROM, keypad, SD card, fuel gauge, external flash, secure element, and avalanche noise source; and (2) an update to the UI keypad view that changes press/release tracking from boolean flags to integer counters and uses alternating colors (blue/red borders, pink/blue backgrounds) to visualize multiple press/release cycles during the keypad factory test.
Changed components
ports/stm32/boards/Passport/modules/views/keypad.pyREADME_FACTORY_TEST.mdInspect captured patch +121 / −36
diff --git a/README_FACTORY_TEST.md b/README_FACTORY_TEST.md
new file mode 100644
index 0000000..13b5d2f
--- /dev/null
+++ b/README_FACTORY_TEST.md
@@ -0,0 +1,71 @@
+# Factory Test Mode for Passport
+
+## Building Factory Test Bootloader
+
+To build the bootloader with factory test functionality enabled:
+
+```bash
+# For mono screen
+just ports/stm32/boards/Passport/bootloader/build mono release factory_test
+
+# For color screen
+just ports/stm32/boards/Passport/bootloader/build color release factory_test
+```
+
+## Flashing Factory Test Bootloader
+
+```bash
+# Flash the factory test bootloader
+just ports/stm32/boards/Passport/bootloader/flash mono release factory_test
+```
+
+## Using Factory Test Mode
+
+The factory test bootloader provides a communication interface through SRAM4 memory at address `0x38000800`. The provisioning tool can write commands to this memory location to trigger various tests.
+
+### Available Factory Tests
+
+1. **LCD Test** (Function 1)
+2. **Camera Test** (Function 2)
+3. **EEPROM Test** (Function 3)
+4. **Keypad Test** (Function 4) ← This is what you want
+5. **SD Card Test** (Function 5)
+6. **Fuel Gauge Test** (Function 6)
+7. **External Flash Test** (Function 7)
+8. **Secure Element Test** (Function 8)
+9. **Avalanche Noise Source Test** (Function 9)
+
+### Keypad Test Details
+
+The keypad test (Function 4) performs the following:
+- Initializes the keypad controller (ADP-5587)
+- Reads the number of keys in the queue
+- Verifies the keypad is responding correctly
+
+### Memory Interface
+
+The factory test uses a shared memory structure at `0x38000800`:
+
+```c
+typedef struct FactoryTestInfo {
+ volatile uint32_t function; // Test function number (1-9)
+ volatile uint32_t param1; // Function parameter 1
+ volatile uint32_t param2; // Function parameter 2
+ volatile uint32_t progress; // Progress indicator
+ volatile uint32_t result_code; // Result code
+ volatile char message[128]; // Result message
+} FactoryTestInfo;
+```
+
+### Running Keypad Test
+
+To run the keypad test:
+
+1. Set `function = 4` (FACTORY_TEST_FUNC_KEYPAD)
+2. Set `progress = 0xFFFFFFFF` (FACTORY_TEST_COMMAND_READY)
+3. Wait for `progress` to change to 100 (test complete)
+4. Check `result_code` for success (0) or error
+
+The test will return:
+- **Success (0)**: "OK" - Keypad is working
+- **Error (111)**: Various error messages like "Can't init keypad" or "Can't read number of keys"
diff --git a/ports/stm32/boards/Passport/modules/views/keypad.py b/ports/stm32/boards/Passport/modules/views/keypad.py
index a00b5f9..d07ca82 100644
--- a/ports/stm32/boards/Passport/modules/views/keypad.py
+++ b/ports/stm32/boards/Passport/modules/views/keypad.py
@@ -3,11 +3,15 @@
#
import lvgl as lv
-from styles.colors import FD_BLUE, TEXT_GREY, VERY_LIGHT_GREY, WHITE
+from styles.colors import FD_BLUE, TEXT_GREY, VERY_LIGHT_GREY, WHITE, RED
from styles.local_style import LocalStyle
from styles.style import Stylize
from .view import View
+# Define custom colors for the keypad
+LIGHT_PINK = lv.color_hex(0xFFB6C1)
+LIGHT_BLUE = lv.color_hex(0xADD8E6)
+
WIDTH = 210
HEIGHT = 300
HALF_WIDTH = WIDTH // 2
@@ -42,24 +46,24 @@ class Keypad(View):
self.set_size(lv.pct(100), lv.pct(100))
self.key_state = {
- '1': {'pressed': False, 'released': False, 'frame': None},
- '2': {'pressed': False, 'released': False, 'frame': None},
- '3': {'pressed': False, 'released': False, 'frame': None},
- '4': {'pressed': False, 'released': False, 'frame': None},
- '5': {'pressed': False, 'released': False, 'frame': None},
- '6': {'pressed': False, 'released': False, 'frame': None},
- '7': {'pressed': False, 'released': False, 'frame': None},
- '8': {'pressed': False, 'released': False, 'frame': None},
- '9': {'pressed': False, 'released': False, 'frame': None},
- '0': {'pressed': False, 'released': False, 'frame': None},
- '*': {'pressed': False, 'released': False, 'frame': None},
- '#': {'pressed': False, 'released': False, 'frame': None},
- 'l': {'pressed': False, 'released': False, 'frame': None},
- 'r': {'pressed': False, 'released': False, 'frame': None},
- 'u': {'pressed': False, 'released': False, 'frame': None},
- 'd': {'pressed': False, 'released': False, 'frame': None},
- 'x': {'pressed': False, 'released': False, 'frame': None},
- 'y': {'pressed': False, 'released': False, 'frame': None},
+ '1': {'pressed': 0, 'released': 0, 'frame': None},
+ '2': {'pressed': 0, 'released': 0, 'frame': None},
+ '3': {'pressed': 0, 'released': 0, 'frame': None},
+ '4': {'pressed': 0, 'released': 0, 'frame': None},
+ '5': {'pressed': 0, 'released': 0, 'frame': None},
+ '6': {'pressed': 0, 'released': 0, 'frame': None},
+ '7': {'pressed': 0, 'released': 0, 'frame': None},
+ '8': {'pressed': 0, 'released': 0, 'frame': None},
+ '9': {'pressed': 0, 'released': 0, 'frame': None},
+ '0': {'pressed': 0, 'released': 0, 'frame': None},
+ '*': {'pressed': 0, 'released': 0, 'frame': None},
+ '#': {'pressed': 0, 'released': 0, 'frame': None},
+ 'l': {'pressed': 0, 'released': 0, 'frame': None},
+ 'r': {'pressed': 0, 'released': 0, 'frame': None},
+ 'u': {'pressed': 0, 'released': 0, 'frame': None},
+ 'd': {'pressed': 0, 'released': 0, 'frame': None},
+ 'x': {'pressed': 0, 'released': 0, 'frame': None},
+ 'y': {'pressed': 0, 'released': 0, 'frame': None},
}
y = TOP_MARGIN
@@ -120,42 +124,52 @@ class Keypad(View):
if key_state is not None:
key_frame = key_state.get('frame')
if key_frame is not None:
- pressed = key_state.get('pressed')
- released = key_state.get('released')
+ pressed_count = key_state.get('pressed')
+ released_count = key_state.get('released')
with LocalStyle(key_frame) as style:
- if pressed:
- style.border_width(3)
- else:
+ # Border styling based on pressed count
+ if pressed_count == 0:
style.border_width(1)
style.border_color(TEXT_GREY)
+ elif pressed_count % 2 == 1: # odd
+ style.border_width(3)
+ style.border_color(FD_BLUE)
+ else: # even and > 0
+ style.border_width(3)
+ style.border_color(RED)
- if released:
- style.bg_color(FD_BLUE)
- style.text_color(WHITE)
- else:
+ # Background styling based on released count
+ if released_count == 0:
style.bg_color(VERY_LIGHT_GREY)
+ elif released_count % 2 == 1: # odd
+ style.bg_color(LIGHT_PINK)
+ else: # even and > 0
+ style.bg_color(LIGHT_BLUE)
key_label = key_state.get('label')
if key_label is not None:
+ released_count = key_state.get('released')
with LocalStyle(key_label) as style:
- if released:
+ # Adjust text color based on background
+ if released_count == 0:
+ style.text_color(TEXT_GREY)
+ else:
style.text_color(WHITE)
def should_finish(self):
all_were_pressed = True
for key in self.key_state:
- if not self.key_state[key]['released']:
+ if self.key_state[key]['released'] == 0:
all_were_pressed = False
return all_were_pressed
def on_key(self, key, pressed):
if key in self.key_state:
- # Setting these states is a one-way trip
- if self.key_state.get(key)['pressed'] is False and pressed:
- self.key_state.get(key)['pressed'] = True
-
- if self.key_state.get(key)['released'] is False and not pressed:
- self.key_state.get(key)['released'] = True
+ # Increment counts on each event
+ if pressed:
+ self.key_state.get(key)['pressed'] += 1
+ else:
+ self.key_state.get(key)['released'] += 1
self.update_key(key)
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.