What changed, and why it matters
This commit adds support for a new hardware device called 'Embed Fire' to the Krux Bitcoin signer firmware. It includes new display and touchscreen driver code, build/flash tooling updates, simulator support, documentation, and a large set of unit tests. There is no indication in the commit or supplied references that this change fixes or introduces a security vulnerability.
No security action required. Review as normal feature code if auditing the new CST816S driver or device integration for correctness.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit is a feature addition for a new Kendryte K210 device variant (Embed Fire). Key code changes include: a new CST816S touchscreen driver (src/krux/touchscreens/cst816.py), device-specific LCD initialization in src/krux/display.py, camera orientation handling in src/krux/camera.py, a new board type flag in src/krux/kboard.py, touch driver selection logic in src/krux/touch.py, simulator mocks and assets, build/flash script updates, and extensive test coverage. The change to ft6x36.py replaces returning an exception object with printing an error message, which is a minor robustness improvement, not a security fix. No security-relevant behavior, vulnerability disclosure, or attribution is present in the commit materials.
Changed components
src/krux/display.pysrc/krux/camera.pysrc/krux/kboard.pysrc/krux/touch.pysrc/krux/touchscreens/cst816.pysrc/krux/touchscreens/ft6x36.pysimulator/kruxsim/devices.pysimulator/kruxsim/mocks/cst816.pysimulator/simulator.pykrux build/flash scripttests/Inspect captured patch +1679 / −84
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 293c95a..dda7d10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog 25.XX.X - XXX 2025
+### New Device Support: Embed Fire
+This device shares similarities with the WonderMV but stands out with its larger 2.4" touchscreen.
+
### New Device Support: WonderK PRO
From the wonderful land of Korea, a new creation arrives: the WonderK PRO. Created by an entrepreneur who loves the Krux project, the WonderK follows in the footsteps of the WonderMV, but boasts a larger 2.8" display! Computer simulator for the WonderK device is also included.
diff --git a/docs/getting-started/features/tools.en.md b/docs/getting-started/features/tools.en.md
index ca8236a..48feb82 100644
--- a/docs/getting-started/features/tools.en.md
+++ b/docs/getting-started/features/tools.en.md
@@ -59,7 +59,7 @@ A suite of automated tests to verify important features on your device.
<div style="clear: both"></div>
-#### Touchscreen (Maix Amigo, Yahboom, WonderMV and TZT only)
+#### Touchscreen (Maix Amigo, Yahboom, WonderMV, TZT or Embed Fire only)
<img src="../../../img/maixpy_amigo/touch-test-300.png" align="right" class="amigo">
Confirm that touch input is accurately detected across the entire display surface.
diff --git a/docs/getting-started/installing/from-pre-built-release.en.md b/docs/getting-started/installing/from-pre-built-release.en.md
index da1b02c..96999ea 100644
--- a/docs/getting-started/installing/from-pre-built-release.en.md
+++ b/docs/getting-started/installing/from-pre-built-release.en.md
@@ -41,7 +41,7 @@ Connect the device to your computer via USB (for Maix Amigo, make sure you’re
./ktool -B goE -b 1500000 maixpy_DEVICE/kboot.kfpkg
```
-For `dock`, `wonder_mv` or `tzt` use the `-B dan` parameter:
+For `dock`, `wonder_mv`, `tzt` or `embed_fire` use the `-B dan` parameter:
```bash
./ktool -B dan -b 1500000 maixpy_DEVICE/kboot.kfpkg
```
diff --git a/docs/getting-started/installing/from-source.en.md b/docs/getting-started/installing/from-source.en.md
index b8c055c..bf16c67 100644
--- a/docs/getting-started/installing/from-source.en.md
+++ b/docs/getting-started/installing/from-source.en.md
@@ -65,7 +65,7 @@ unzip kboot.kfpkg -d ./kboot/
```
### Flash the firmware onto the device
-Connect the device to your computer via USB (for Maix Amigo, make sure you’re using bottom port), power it on, and run the following, replacing `DEVICE` with either `m5stickv`, `amigo`, `cube`, `dock`, `yahboom`, `wonder_mv` or `tzt`:
+Connect the device to your computer via USB (for Maix Amigo, make sure you’re using bottom port), power it on, and run the following, replacing `DEVICE` with either `m5stickv`, `amigo`, `cube`, `dock`, `yahboom`, `wonder_mv`, `tzt` or `embed_fire`:
```bash
# flash firmware to DEVICE
./krux flash maixpy_DEVICE
diff --git a/docs/getting-started/installing/from-test-release.en.md b/docs/getting-started/installing/from-test-release.en.md
index 5397feb..94b00d6 100644
--- a/docs/getting-started/installing/from-test-release.en.md
+++ b/docs/getting-started/installing/from-test-release.en.md
@@ -142,3 +142,21 @@ To Flash TZT you need to pass the `-B dan` parameter.
```pwsh
.\ktool-win.exe -B dan -b 1500000 maixpy_tzt\kboot.kfpkg
```
+
+#### Embed Fire
+To Flash Embed Fire you need to pass the `-B dan` parameter and `-b 400000` (maximum supported baud rate).
+
+##### Linux
+```bash
+./ktool-linux -B dan -b 400000 maixpy_embed_fire/kboot.kfpkg
+```
+
+##### Mac
+```bash
+./ktool-mac -B dan -b 400000 maixpy_embed_fire/kboot.kfpkg
+```
+
+##### Windows
+```pwsh
+.\ktool-win.exe -B dan -b 400000 maixpy_embed_fire\kboot.kfpkg
+```
diff --git a/docs/getting-started/settings.en.md b/docs/getting-started/settings.en.md
index 121a8b8..20c317a 100644
--- a/docs/getting-started/settings.en.md
+++ b/docs/getting-started/settings.en.md
@@ -119,7 +119,7 @@ Here you choose between `thermal/adafruit`, `cnc/file`, `cnc/grbl` or `none` (de
<div style="clear: both"></div>
-#### Touchscreen (Maix Amigo, Yahboom, WonderMV and TZT only)
+#### Touchscreen (Maix Amigo, Yahboom, WonderMV, TZT or Embed Fire only)
<img src="../../img/maixpy_amigo/touchscreen-300.png" align="right" class="amigo">
If your device has touchscreen you can change the touch detection threshold. If it is being too sensitive or detecting false or ghost touches, you should increase the threshold value, making it less sensitive. The other way is also valid, reduce the threshold to make the screen more sensitive to touches.
diff --git a/docs/img/maixpy_embed_fire/about-303.png b/docs/img/maixpy_embed_fire/about-303.png
new file mode 100644
index 0000000..53785b1
Binary files /dev/null and b/docs/img/maixpy_embed_fire/about-303.png differ
diff --git a/docs/img/maixpy_embed_fire/logo-303.en.png b/docs/img/maixpy_embed_fire/logo-303.en.png
new file mode 100644
index 0000000..11693b3
Binary files /dev/null and b/docs/img/maixpy_embed_fire/logo-303.en.png differ
diff --git a/docs/index.en.md b/docs/index.en.md
index 2e37b81..13e974c 100644
--- a/docs/index.en.md
+++ b/docs/index.en.md
@@ -14,7 +14,7 @@ hide:
Krux is an open-source firmware that transforms off-the-shelf Kendryte K210 devices, such as the Maix Amigo, M5StickV and [more](parts.md), into versatile bitcoin transaction signers. Beyond its core functionality, Krux is a flexible platform that can adapt to devices with different form factors, providing a suite of tools to assist with the creation and recovery of mnemonic backups, some of which include encryption options for enhanced security.
-Devices like the Maix Amigo, Yahboom, WonderMV or TZT come ready to use, with large touchscreens that make it easy and user-friendly to operate. These devices are ideal for those looking for a plug-and-play solution. On the other hand, Krux also supports development board kits, which are perfect for DIY enthusiasts who enjoy customizing and building their own hardware setups.
+Devices like the Maix Amigo, Yahboom, WonderMV, TZT or Embed Fire come ready to use, with large touchscreens that make it easy and user-friendly to operate. These devices are ideal for those looking for a plug-and-play solution. On the other hand, Krux also supports development board kits, which are perfect for DIY enthusiasts who enjoy customizing and building their own hardware setups.
Interacting seamlessly with leading coordinator wallets through QR codes, SD cards, and even thermal printers, the user-friendly firmware offers unique features to support transactions and mnemonic backups in an offline environment.
diff --git a/docs/parts.en.md b/docs/parts.en.md
index 8dbef3c..cc75e07 100644
--- a/docs/parts.en.md
+++ b/docs/parts.en.md
@@ -2,17 +2,17 @@
### Compatible Devices (comparative table)
-| Device | [M5StickV](#m5stickv) | [Maix Amigo](#maix-amigo) | [Maix Dock](#maix-dock) | [Yahboom k210 module](#yahboom-k210-module) | [Maix Cube](#maix-cube) | [WonderMV](#wondermv) | [TZT](#tzt) |
-| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
-| Price range | US$ 50-55 | US$ 50-85 | US$ 27-35 | US$ 45-61 | US$ 34-49 | US$ 58-86 | US$ 48 |
-| Screen size / resolution | 1.14" / 135*240 | 3.5" / 320*480 | 2.4" / 240*320 | 2" / 240*320 | 1.3" / 240*240 | 2" / 240*320 | 2" / 240*320 |
-| Brightness control | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: |
-| Device size | 48\*24\*22mm | 104\*63\*17mm | 98\*59\*18mm | 57\*41\*17mm | 40\*40\*16mm | 59\*41\*17mm | 57\*41\*16mm |
-| Touchscreen | :x: | Capacitive | :x: | Capacitive | :x: | Capacitive | Capacitive |
-| Camera | `OV7740` | `OV7740` rear<br>`GC0328` front | `GC0328` | `OV2640` <i style="font-size: 85%">(VER:1.0)</i> or<br>`GC2145` <i style="font-size: 85%">(VER:1.1)</i> | `OV7740` | `GC2145` | `GC2145` or<br>`GC0328` |
-| Battery | 200mAh | 520mAh | :x: | :x: | 200mAh | :x: | :x: |
-| Requirements | None | None | [Rotary encoder](https://duckduckgo.com/?q=ky-040)<br> [3D printed case](https://github.com/selfcustody/DockEncoderCase)<br> Soldering<br>Assembly | None | None | None | None |
-| Warnings | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) | [:material-numeric-2-circle:{ title="Maix Amigo screens" }](#amigo-info) | [:material-numeric-3-circle:{ title="Maix Dock and soldered pin" }](#dock-info) | Micro USB | 3-Way button | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) [:material-numeric-4-circle:{ title="WonderMV and SD card" }](#wondermv-info) | None |
+| Device | [M5StickV](#m5stickv) | [Maix Amigo](#maix-amigo) | [Maix Dock](#maix-dock) | [Yahboom k210 module](#yahboom-k210-module) | [Maix Cube](#maix-cube) | [WonderMV](#wondermv) | [TZT](#tzt) | [Embed Fire](#embed-fire) |
+| ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- |
+| Price range | US$ 50-55 | US$ 50-85 | US$ 27-35 | US$ 45-61 | US$ 34-49 | US$ 58-86 | US$ 48 | US$ 74 |
+| Screen size / resolution | 1.14" / 135*240 | 3.5" / 320*480 | 2.4" / 240*320 | 2" / 240*320 | 1.3" / 240*240 | 2" / 240*320 | 2" / 240*320 | 2.4" / 240*320 |
+| Brightness control | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :x: |
+| Device size | 48\*24\*22mm | 104\*63\*17mm | 98\*59\*18mm | 57\*41\*17mm | 40\*40\*16mm | 59\*41\*17mm | 57\*41\*16mm | 63\*45\*14mm |
+| Touchscreen | :x: | Capacitive | :x: | Capacitive | :x: | Capacitive | Capacitive | Capacitive |
+| Camera | `OV7740` | `OV7740` rear<br>`GC0328` front | `GC0328` | `OV2640` <i style="font-size: 85%">(VER:1.0)</i> or<br>`GC2145` <i style="font-size: 85%">(VER:1.1)</i> | `OV7740` | `GC2145` | `GC2145` or<br>`GC0328` | `OV2640` |
+| Battery | 200mAh | 520mAh | :x: | :x: | 200mAh | :x: | :x: | :x: |
+| Requirements | None | None | [Rotary encoder](https://duckduckgo.com/?q=ky-040)<br> [3D printed case](https://github.com/selfcustody/DockEncoderCase)<br> Soldering<br>Assembly | None | None | None | None | None |
+| Warnings | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) | [:material-numeric-2-circle:{ title="Maix Amigo screens" }](#amigo-info) | [:material-numeric-3-circle:{ title="Maix Dock and soldered pin" }](#dock-info) | Micro USB | 3-Way button | [:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info) [:material-numeric-4-circle:{ title="WonderMV and SD card" }](#wondermv-info) | None | None |
<i style="font-size: 85%">:material-numeric-1-circle:{id="pull-up-resistor-info"}:
----8<----
@@ -39,7 +39,7 @@ WonderMV may restart when certain SD cards are inserted. See [Why does my Wonder
</i>
### M5StickV
-<img src="../img/maixpy_m5stickv/logo-250.png" align="right" style="width: 15%;">
+<img src="../img/maixpy_m5stickv/logo-250.png" align="right" style="width: 10%;">
Krux was first ported to this device in March 2022. Featuring the smallest screen size and resolution, it also includes a built-in battery. Refer to the [USB-C recognition warning <i style="font-size: 85%">:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info)</i> above for important notes. Below is a list of distributors where it can be found:
@@ -57,7 +57,7 @@ Krux was first ported to this device in March 2022. Featuring the smallest scree
<div style="clear: both"></div>
### Maix Amigo
-<img src="../img/maixpy_amigo/logo-300.png" align="right" style="width: 16%;">
+<img src="../img/maixpy_amigo/logo-300.png" align="right" style="width: 20%;">
Krux has supported this device since its second version in August 2022. It offers the largest screen size and resolution, a touchscreen, and a built-in battery. Below is a list of distributors where it is available:
@@ -71,7 +71,7 @@ Krux has supported this device since its second version in August 2022. It offer
<div style="clear: both"></div>
### Yahboom k210 module
-<img src="../img/maixpy_yahboom/logo-312.png" align="right" style="width: 16%;">
+<img src="../img/maixpy_yahboom/logo-312.png" align="right" style="width: 15%;">
Supported by Krux since March 2024, this device features a touchscreen and comes with a compatible 32 GB card, a USB card reader, one PH2.0 4-pin male-to-male connector, and a PH2.0 female adapter for connecting to a [thermal printer](#optional-ttl-serial-thermal-printer). Below is a list of distributors where it is available:
@@ -83,7 +83,7 @@ Supported by Krux since March 2024, this device features a touchscreen and comes
<div style="clear: both"></div>
### Maix Cube
-<img src="../img/maixpy_cube/logo-400.png" align="right" style="width: 18%;">
+<img src="../img/maixpy_cube/logo-400.png" align="right" style="width: 12%;">
Supported by Krux since July 2024, this device features the second smallest screen size and resolution, a 3-way button, and a built-in battery. Below is a list of distributors where it is available:
@@ -97,7 +97,7 @@ Supported by Krux since July 2024, this device features the second smallest scre
<div style="clear: both"></div>
### WonderMV
-<img src="../img/maixpy_wonder_mv/logo-304.png" align="right" style="width: 16%;">
+<img src="../img/maixpy_wonder_mv/logo-304.png" align="right" style="width: 15%;">
Supported by Krux since September 2024, this touchscreen device features a metal backplate and includes a compatible 32 GB card, a USB card reader, and two 5264 4-pin connector male-to-male cables for connecting to a [thermal printer](#optional-ttl-serial-thermal-printer). Refer to the [USB-C recognition warning <i style="font-size: 85%">:material-numeric-1-circle:{ title="USB-C recognition" }](#pull-up-resistor-info)</i> and the [SD card warning <i style="font-size: 85%">:material-numeric-4-circle:{ title="WonderMV and SD card" }](#wondermv-info)</i> above for important notes. The following distributors offer this device:
@@ -111,7 +111,7 @@ Supported by Krux since September 2024, this touchscreen device features a metal
### TZT
-<img src="../img/maixpy_tzt/logo-314.png" align="right" style="width: 16%;">
+<img src="../img/maixpy_tzt/logo-314.png" align="right" style="width: 15%;">
Supported by Krux since October 2025, this touchscreen device comes in a premium milled aluminum housing and features five buttons. Available through the following distributors:
@@ -119,10 +119,19 @@ Supported by Krux since October 2025, this touchscreen device comes in a premium
<div style="clear: both"></div>
+### Embed Fire
+<img src="../img/maixpy_embed_fire/logo-303.png" align="right" style="width: 18%;">
+
+Supported by Krux since November 2025, this touchscreen device features a 2.4" display and a metal backplate. It is available through the following distributors:
+
+- [AliExpress](https://www.aliexpress.com/w/wholesale-k210-AI-python.html)
+
+<div style="clear: both"></div>
+
### Maix Dock
-<img src="../img/maixpy_dock/logo-302.png" align="right" style="width: 16%;">
+<img src="../img/maixpy_dock/logo-302.png" align="right" style="width: 18%;">
For DIY enthusiasts, Krux has supported the Maix Dock since August 2022. These kits include a board and screen but require you to source a rotary encoder or buttons separately and assemble the device yourself. Some Maix Dock boards also include Wi-Fi.
diff --git a/krux b/krux
index 0cbb2fc..cbcd04d 100755
--- a/krux
+++ b/krux
@@ -70,6 +70,7 @@ if [ "$1" == "build" ]; then
[ "$device" == "maixpy_yahboom_devkit" ]||
[ "$device" == "maixpy_tzt" ]||
[ "$device" == "maixpy_wonder_k" ]||
+ [ "$device" == "maixpy_embed_fire" ]||
[ "$allow_unsupported" ]; then
declare $(grep -m 1 version pyproject.toml | sed 's/ *= */=/g' | sed 's/"//g' | sed 's/\r//g')
sed -i -- 's/VERSION = ".*"/VERSION = "'"$version"'"/g' src/krux/metadata.py
@@ -96,7 +97,7 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == "
CMD_SUFFIX="-s firmware.bin"
elif [ "$1" == "boot-terminal" ]; then
CMD_SUFFIX="-s firmware.bin -t"
- else
+ elif [ "$1" == "erase" ]; then
CMD_SUFFIX="-E"
fi
device="$2"
@@ -105,6 +106,9 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == "
else
device_vendor_product_id=""
BAUDRATE=2000000
+ if [ "$device" == "maixpy_embed_fire" ]; then
+ BAUDRATE=400000
+ fi
if [ "$device" == "maixpy_m5stickv" ]; then
# https://devicehunt.com/view/type/usb/vendor/0403/device/6001
device_vendor_product_id="0403:6001"
@@ -118,7 +122,8 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == "
[ "$device" == "maixpy_yahboom" ]||
[ "$device" == "maixpy_wonder_mv" ]||
[ "$device" == "maixpy_yahboom_devkit" ]||
- [ "$device" == "maixpy_wonder_k" ]; then
+ [ "$device" == "maixpy_wonder_k" ]||
+ [ "$device" == "maixpy_embed_fire" ]; then
# https://devicehunt.com/view/type/usb/vendor/1a86/device/7523
device_vendor_product_id="1a86:7523"
elif [ "$device" == "maixpy_tzt" ]; then
@@ -138,7 +143,8 @@ elif [ "$1" == "flash" -o "$1" == "boot" -o "$1" == "boot-terminal" -o "$1" == "
if [ "$device" == "maixpy_dock" ]||
[ "$device" == "maixpy_wonder_mv" ]||
[ "$device" == "maixpy_tzt" ]||
- [ "$device" == "maixpy_wonder_k" ]; then
+ [ "$device" == "maixpy_wonder_k" ]||
+ [ "$device" == "maixpy_embed_fire" ]; then
BOARD="dan"
else
BOARD="goE"
diff --git a/pyproject.toml b/pyproject.toml
index 8fd159d..a402d54 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -130,6 +130,7 @@ simulator-cube = "python simulator/simulator.py --device maixpy_cube"
simulator-wonder-mv = "python simulator/simulator.py --device maixpy_wonder_mv"
simulator-tzt = "python simulator/simulator.py --device maixpy_tzt"
simulator-wonder-k = "python simulator/simulator.py --device maixpy_wonder_k"
+simulator-embed-fire = "python simulator/simulator.py --device maixpy_embed_fire"
# aliases
simulator.ref = "simulator-amigo"
simulator-m5.ref = "simulator-m5stickv"
@@ -138,6 +139,8 @@ simulator-wonder.ref = "simulator-wonder-mv"
simulator-wondermv.ref = "simulator-wonder-mv"
simulator-wonderk.ref = "simulator-wonder-k"
simulator-k.ref = "simulator-wonder-k"
+simulator-embed.ref = "simulator-embed-fire"
+simulator-embedfire.ref = "simulator-embed-fire"
# git tasks
git-update = "git submodule update --init --recursive"
diff --git a/simulator/assets/maixpy_embed_fire.png b/simulator/assets/maixpy_embed_fire.png
new file mode 100644
index 0000000..1b29911
Binary files /dev/null and b/simulator/assets/maixpy_embed_fire.png differ
diff --git a/simulator/assets/maixpy_embed_fire_mask.png b/simulator/assets/maixpy_embed_fire_mask.png
new file mode 100644
index 0000000..fb42bc7
Binary files /dev/null and b/simulator/assets/maixpy_embed_fire_mask.png differ
diff --git a/simulator/kruxsim/devices.py b/simulator/kruxsim/devices.py
index 31c2f6d..a27b0f6 100644
--- a/simulator/kruxsim/devices.py
+++ b/simulator/kruxsim/devices.py
@@ -31,6 +31,7 @@ CUBE = "maixpy_cube"
WONDER_MV = "maixpy_wonder_mv"
WONDER_K = "maixpy_wonder_k"
TZT = "maixpy_tzt"
+EMBEDFIRE = "maixpy_embed_fire"
WINDOW_SIZES = {
M5STICKV: (320, 640),
@@ -41,7 +42,8 @@ WINDOW_SIZES = {
CUBE: (484, 612),
WONDER_MV: (410, 590),
WONDER_K: (500, 650),
- TZT: (410,580)
+ TZT: (410,580),
+ EMBEDFIRE: (500, 671),
}
@@ -79,7 +81,7 @@ def load_font(device):
os.path.join("..", "firmware", "font", "FusionPixel-14.bdf"),
),
]
- elif device in (DOCK, YAHBOOM, WONDER_MV, TZT, WONDER_K):
+ elif device in (DOCK, YAHBOOM, WONDER_MV, TZT, WONDER_K, EMBEDFIRE):
fonts[device] = [
pg.freetype.Font(
os.path.join("..", "firmware", "font", "ter-u16n.bdf")
@@ -156,6 +158,13 @@ def screenshot_rect(device):
screen.get_rect().center[0] - 0,
screen.get_rect().center[1] + 28, #36 #20
)
+ elif device == EMBEDFIRE:
+ rect.width -= 10
+ rect.height -= 30
+ rect.center = (
+ screen.get_rect().center[0] + 5,
+ screen.get_rect().center[1] + 15,
+ )
# TODO: fix after WONDER_K img is ready
# elif device == WONDER_K:
# rect.width -= 0
diff --git a/simulator/kruxsim/mocks/cst816.py b/simulator/kruxsim/mocks/cst816.py
new file mode 100644
index 0000000..5e6fcb2
--- /dev/null
+++ b/simulator/kruxsim/mocks/cst816.py
@@ -0,0 +1,39 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2023 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+import sys
+from unittest import mock
+from kruxsim.mocks.touchscreen_common import TCOMMON, register_sequence_executor
+
+
+class CST816(TCOMMON):
+ def activate(self, irq_pin):
+ pass
+
+
+touch_control = CST816()
+
+
+if "krux.touchscreens.cst816" not in sys.modules:
+ sys.modules["krux.touchscreens.cst816"] = mock.MagicMock(
+ touch_control=touch_control,
+ register_sequence_executor=register_sequence_executor,
+ )
diff --git a/simulator/simulator.py b/simulator/simulator.py
index 3b5ba25..c0c0932 100644
--- a/simulator/simulator.py
+++ b/simulator/simulator.py
@@ -112,6 +112,7 @@ from kruxsim.mocks import sensor
from kruxsim.mocks import shannon
from kruxsim.mocks import ft6x36
from kruxsim.mocks import gt911
+from kruxsim.mocks import cst816
from kruxsim.mocks import buttons
from kruxsim.mocks import rotary
from kruxsim.sequence import SequenceExecutor
@@ -166,6 +167,7 @@ YAHBOOM_SIZE = (312, 440)
CUBE_SIZE = (400, 424)
WONDER_MV_SIZE = (304, 440)
TZT_SIZE = (314, 442)
+EMBEDFIRE_SIZE = (303,407)
# Handle screenshots scale and alpha bg
# When exporting the mask from GIMP uncheck "Save info about transparent pixels color"
@@ -204,6 +206,11 @@ elif (args.device == devices.TZT):
mask_img = pg.image.load(
os.path.join("assets", "maixpy_tzt_mask.png")
).convert_alpha()
+elif (args.device == devices.EMBEDFIRE):
+ device_screenshot_size = EMBEDFIRE_SIZE
+ mask_img = pg.image.load(
+ os.path.join("assets", "maixpy_embed_fire_mask.png")
+ ).convert_alpha()
# TODO: WONDER_K IMG
# elif (args.device == devices.WONDER_K):
# device_screenshot_size = WONDER_K_SIZE
@@ -300,6 +307,8 @@ try:
if event.type == pg.MOUSEBUTTONDOWN:
if args.device == devices.WONDER_K:
gt911.touch_control.trigger_event()
+ elif args.device == devices.EMBEDFIRE:
+ cst816.touch_control.trigger_event()
else:
ft6x36.touch_control.trigger_event()
if event.type == pg.ACTIVEEVENT and event.gain:
diff --git a/src/krux/camera.py b/src/krux/camera.py
index 840b88e..2780ef1 100644
--- a/src/krux/camera.py
+++ b/src/krux/camera.py
@@ -117,7 +117,10 @@ class Camera:
if self.cam_id == OV5642_ID:
sensor.set_hmirror(1)
if self.cam_id == OV2640_ID:
- sensor.set_vflip(1)
+ if kboard.is_embed_fire:
+ sensor.set_hmirror(0)
+ else:
+ sensor.set_vflip(1)
if kboard.is_bit:
# CIF mode will use central pixels and discard darker periphery
sensor.set_framesize(sensor.CIF)
diff --git a/src/krux/display.py b/src/krux/display.py
index 93e0c40..fac23bb 100644
--- a/src/krux/display.py
+++ b/src/krux/display.py
@@ -151,6 +151,16 @@ class Display:
ss=board.config["lcd"]["ss"],
clk=board.config["lcd"]["clk"],
)
+ elif kboard.is_embed_fire:
+ lcd.init(
+ type=6, # DEV_EMBED_FIRE
+ invert=False,
+ rst=board.config["lcd"]["rst"],
+ dcx=board.config["lcd"]["dcx"],
+ ss=board.config["lcd"]["ss"],
+ clk=board.config["lcd"]["clk"],
+ lcd_type=6, # ST7789V
+ )
elif kboard.is_cube:
lcd.init(
invert=True,
diff --git a/src/krux/kboard.py b/src/krux/kboard.py
index ebe4789..bf77ad9 100644
--- a/src/krux/kboard.py
+++ b/src/krux/kboard.py
@@ -30,6 +30,7 @@ class KBoard:
self.is_amigo = board.config["type"] == "amigo"
self.is_bit = board.config["type"] == "bit"
self.is_cube = board.config["type"] == "cube"
+ self.is_embed_fire = board.config["type"] == "embed_fire"
self.is_yahboom = board.config["type"] == "yahboom"
self.is_wonder_mv = board.config["type"] == "wonder_mv"
self.is_tzt = board.config["type"] == "tzt"
diff --git a/src/krux/touch.py b/src/krux/touch.py
index ee1435a..c7d5dc0 100644
--- a/src/krux/touch.py
+++ b/src/krux/touch.py
@@ -23,6 +23,7 @@
import time
+from .kboard import kboard
from .krux_settings import Settings
IDLE = 0
@@ -54,7 +55,12 @@ class Touch:
self.gesture = None
self.state = IDLE
self.width, self.height = width, height
- if res_pin is not None:
+ if kboard.is_embed_fire:
+ from .touchscreens.cst816 import touch_control
+
+ self.touch_driver = touch_control
+ self.touch_driver.activate(irq_pin)
+ elif res_pin is not None:
from .touchscreens.gt911 import touch_control
self.touch_driver = touch_control
diff --git a/src/krux/touchscreens/cst816.py b/src/krux/touchscreens/cst816.py
new file mode 100644
index 0000000..3497878
--- /dev/null
+++ b/src/krux/touchscreens/cst816.py
@@ -0,0 +1,150 @@
+# The MIT License (MIT)
+
+# Copyright (c) 2021-2024 Krux contributors
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+import time
+from Maix import GPIO
+from fpioa_manager import fm
+from . import Touchscreen
+from ..i2c import i2c_bus
+
+CST816S_TOUCH_DATA = 0x01
+CST816S_VERSION = 0x15
+CST816S_VERSION_INFO = 0xA7
+CST816S_STANDBY = 0xA5
+CST816S_AUTO_SLEEP_TIME = 0xF9
+CST816S_AUTO_SLEEP_MODE = 0xFE
+CST816S_DOUBLE_CLICK_EN = 0xEC
+
+CST816S_ADDR = 0x15
+ACTIVITY_THRESHOLD = 100
+SCREEN_WIDTH = 240
+EVENT_TOUCH_CONTACT = 0x02
+
+
+def __handler__(pin_num=None):
+ """GPIO interrupt handler for touch events"""
+ # pylint: disable=unused-argument
+ touch_control.trigger_event()
+
+
+class CST816(Touchscreen):
+ """CST816S capacitive touchscreen controller driver"""
+
+ def __init__(self):
+ self.irq_pin = None
+ self.event_flag = False
+ self.irq_point = None
+ self.last_touch_time = 0
+
+ def _init_pins(self, irq_pin):
+ fm.register(irq_pin, fm.fpioa.GPIOHS1)
+ self.irq_pin = GPIO(GPIO.GPIOHS1, GPIO.IN, GPIO.PULL_UP)
+
+ def _init_cst816(self):
+ try:
+ version = self._read_reg(CST816S_VERSION, 1)
+ if version is not None:
+ print("CST816S touchscreen found")
+
+ self._write_reg(CST816S_AUTO_SLEEP_MODE, bytearray([0x00]))
+
+ except Exception as e:
+ print("CST816S initialization error:", e)
+
+ def activate(self, irq_pin):
+ """Enable touchscreen with interrupt handling"""
+ self._init_pins(irq_pin)
+ self._init_cst816()
+ self.irq_pin.irq(__handler__, GPIO.IRQ_FALLING)
+
+ def _write_reg(self, reg_addr, buf):
+ if i2c_bus is not None:
+ try:
+ payload = bytearray([reg_addr])
+ payload.extend(bytearray(buf))
+ i2c_bus.writeto(CST816S_ADDR, payload)
+ except Exception as e:
+ print("CST816S write error:", e)
+
+ def _read_reg(self, reg_addr, buf_len):
+ if i2c_bus is not None:
+ try:
+ addr_bytes = bytearray([reg_addr])
+ i2c_bus.writeto(CST816S_ADDR, addr_bytes)
+ return i2c_bus.readfrom(CST816S_ADDR, buf_len)
+ except Exception as e:
+ print("CST816S read error:", e)
+ return None
+
+ def _read_touch_data(self):
+ try:
+ touch_data = self._read_reg(CST816S_TOUCH_DATA, 6)
+ if touch_data is not None and len(touch_data) >= 6:
+ points = touch_data[1]
+ event = touch_data[2] >> 6
+ x = ((touch_data[2] & 0x0F) << 8) | touch_data[3]
+ y = ((touch_data[4] & 0x0F) << 8) | touch_data[5]
+
+ if points > 0 and event == EVENT_TOUCH_CONTACT:
+ x = max(0, SCREEN_WIDTH - x)
+ return (x, y)
+
+ except Exception as e:
+ print("CST816S touch data read error:", e)
+ return None
+
+ def current_point_validate(self):
+ """Read and validate current touch point coordinates"""
+ return self._read_touch_data()
+
+ def current_point(self):
+ """Return touch coordinates if within activity threshold"""
+ if time.ticks_ms() - self.last_touch_time <= ACTIVITY_THRESHOLD:
+ return self._read_touch_data()
+ return None
+
+ def trigger_event(self):
+ """Process touch interrupt and update event state"""
+ current_time = time.ticks_ms()
+
+ if current_time - self.last_touch_time <= ACTIVITY_THRESHOLD:
+ self.last_touch_time = current_time
+ return
+
+ irq_point = self.current_point_validate()
+ if irq_point is not None:
+ self.irq_point = irq_point
+ self.event_flag = True
+ self.last_touch_time = current_time
+
+ def event(self):
+ """Return and clear touch event flag"""
+ flag = self.event_flag
+ self.event_flag = False
+ return flag
+
+ def threshold(self, _):
+ """Threshold configuration (not supported on CST816S)"""
+ print("CST816S: Threshold setting not supported")
+
+
+touch_control = CST816()
diff --git a/src/krux/touchscreens/ft6x36.py b/src/krux/touchscreens/ft6x36.py
index ec09321..5ce9eaa 100644
--- a/src/krux/touchscreens/ft6x36.py
+++ b/src/krux/touchscreens/ft6x36.py
@@ -93,7 +93,7 @@ class FT6X36(Touchscreen):
y = ((data_buf[2] & 0x0F) << 8) | (data_buf[3])
return (x, y)
except Exception as e:
- return e # debug
+ print("FT6X36 read error:", e)
return None
def trigger_event(self):
diff --git a/tests/conftest.py b/tests/conftest.py
index 1d6d456..0a1426d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,6 +10,7 @@ from .shared_mocks import (
board_yahboom,
board_bit,
board_wonder_k,
+ board_embed_fire,
encode_to_string,
encode,
statvfs,
@@ -175,6 +176,15 @@ def wonder_k(monkeypatch, mp_modules):
reset_krux_modules()
+@pytest.fixture
+def embed_fire(monkeypatch, mp_modules):
+ import sys
+
+ monkeypatch.setitem(sys.modules, "board", board_embed_fire())
+ monkeypatch.setitem(sys.modules, "pmu", None)
+ reset_krux_modules()
+
+
@pytest.fixture(
params=[
"amigo",
diff --git a/tests/shared_mocks.py b/tests/shared_mocks.py
index cadd019..74dd3a6 100644
--- a/tests/shared_mocks.py
+++ b/tests/shared_mocks.py
@@ -727,6 +727,54 @@ def board_wonder_k():
)
+def board_embed_fire():
+ return mock.MagicMock(
+ config={
+ "type": "embed_fire",
+ "lcd": {
+ "dcx": 27,
+ "ss": 28,
+ "rst": 29,
+ "clk": 26,
+ "height": 240,
+ "width": 320,
+ "invert": 0,
+ "dir": 96,
+ "lcd_type": 0,
+ },
+ "sensor": {
+ "pin_sda": 24,
+ "cmos_href": 20,
+ "cmos_pclk": 23,
+ "cmos_xclk": 22,
+ "cmos_pwdn": 21,
+ "cmos_vsync": 18,
+ "reg_width": 8,
+ "i2c_num": 2,
+ "pin_clk": 25,
+ "cmos_rst": 19,
+ },
+ "sdcard": {"sclk": 8, "mosi": 7, "miso": 9, "cs": 6},
+ "board_info": {
+ "BOOT_KEY": 16,
+ "LED_G": 17,
+ "CONNEXT_A": 1,
+ "CONNEXT_B": 0,
+ "I2C_SDA": 12,
+ "I2C_SCL": 13,
+ "SPI_SCLK": 8,
+ "SPI_MOSI": 7,
+ "SPI_MISO": 9,
+ "SPI_CS": 6,
+ },
+ "krux": {
+ "pins": {"BUTTON_B": 16, "TOUCH_IRQ": 15, "I2C_SDA": 12, "I2C_SCL": 13},
+ "display": {"touch": True, "font": [8, 16], "font_wide": [16, 16]},
+ },
+ }
+ )
+
+
def mock_context(mocker):
import board
diff --git a/tests/test_touch.py b/tests/test_touch.py
index f35aa2d..3f03ff7 100644
--- a/tests/test_touch.py
+++ b/tests/test_touch.py
@@ -1,8 +1,514 @@
+import pytest
import time
-from .test_input import mock_timer_ticks
-def test_touch_event(mocker, amigo):
+@pytest.fixture
+def mock_settings(mocker):
+ """Mock Settings to avoid dependency on hardware config"""
+ mock_settings_obj = mocker.MagicMock()
+ mock_settings_obj.hardware.touch.threshold = 40
+ # Ensure hardware doesn't have display attribute to avoid coordinate flipping
+ del mock_settings_obj.hardware.display
+ mock_settings_class = mocker.patch("krux.touch.Settings")
+ mock_settings_class.return_value = mock_settings_obj
+ return mock_settings_obj
+
+
+@pytest.fixture
+def mock_touch_driver(mocker):
+ """Mock a generic touch driver"""
+ driver = mocker.MagicMock()
+ driver.current_point.return_value = None
+ driver.event.return_value = False
+ driver.irq_point = None
+ return driver
+
+
+def test_touch_init_ft6x36(mocker, amigo, mock_settings):
+ """Test Touch initialization with FT6X36 driver (default case)"""
+ from krux.touch import Touch
+
+ mock_control = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_control)
+
+ # No res_pin = FT6X36
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ assert touch.width == 240
+ assert touch.height == 135
+ assert touch.state == 0 # IDLE
+ mock_control.activate_irq.assert_called_once_with(20)
+ mock_control.threshold.assert_called_once_with(40)
+
+
+def test_touch_init_cst816(mocker, embed_fire, mock_settings):
+ """Test Touch initialization with CST816 driver (embed_fire board)"""
+ from krux.touch import Touch
+
+ mock_control = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.cst816.touch_control", mock_control)
+
+ # embed_fire = CST816
+ touch = Touch(width=240, height=135, irq_pin=21)
+
+ assert touch.touch_driver == mock_control
+ mock_control.activate.assert_called_once_with(21)
+ mock_control.threshold.assert_called_once_with(40)
+
+
+def test_touch_init_gt911(mocker, wonder_k, mock_settings):
+ """Test Touch initialization with GT911 driver (res_pin provided)"""
+ from krux.touch import Touch
+
+ mock_control = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.gt911.touch_control", mock_control)
+
+ # res_pin != None = GT911
+ touch = Touch(width=240, height=135, irq_pin=21, res_pin=22)
+
+ assert touch.touch_driver == mock_control
+ mock_control.activate.assert_called_once_with(21, 22)
+ mock_control.threshold.assert_called_once_with(40)
+
+
+def test_clear_regions(mocker, amigo, mock_settings):
+ """Test clearing touch regions"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.y_regions = [50, 100, 150]
+ touch.x_regions = [60, 120]
+
+ touch.clear_regions()
+
+ assert touch.y_regions == []
+ assert touch.x_regions == []
+
+
+def test_add_y_delimiter(mocker, amigo, mock_settings):
+ """Test adding Y delimiters"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ touch.add_y_delimiter(50)
+ touch.add_y_delimiter(100)
+
+ assert touch.y_regions == [50, 100]
+
+
+def test_add_y_delimiter_out_of_bounds(mocker, amigo, mock_settings):
+ """Test adding Y delimiter outside display area raises error"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ with pytest.raises(ValueError, match="outside display area"):
+ touch.add_y_delimiter(250) # width = 240
+
+
+def test_add_x_delimiter(mocker, amigo, mock_settings):
+ """Test adding X delimiters"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ touch.add_x_delimiter(45)
+ touch.add_x_delimiter(90)
+
+ assert touch.x_regions == [45, 90]
+
+
+def test_add_x_delimiter_out_of_bounds(mocker, amigo, mock_settings):
+ """Test adding X delimiter outside display area raises error"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ with pytest.raises(ValueError, match="outside display area"):
+ touch.add_x_delimiter(140) # height = 135
+
+
+def test_set_regions(mocker, amigo, mock_settings):
+ """Test setting regions with lists"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ touch.set_regions(x_list=[30, 60, 90], y_list=[60, 120, 180])
+
+ assert touch.x_regions == [30, 60, 90]
+ assert touch.y_regions == [60, 120, 180]
+
+
+def test_set_regions_invalid_type(mocker, amigo, mock_settings):
+ """Test setting regions with non-list raises error"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ with pytest.raises(ValueError, match="must be a list"):
+ touch.set_regions(x_list="not a list")
+
+ with pytest.raises(ValueError, match="must be a list"):
+ touch.set_regions(y_list=123)
+
+
+@pytest.mark.parametrize(
+ "data,x_regions,y_regions,expected_valid",
+ [
+ ((100, 100), [], [], True), # No regions = always valid
+ ((100, 100), [], [50, 150], True), # Within y bounds
+ ((100, 100), [40, 120], [], True), # Within x bounds
+ ((100, 100), [40, 120], [50, 150], True), # Within both bounds
+ ((30, 100), [40, 120], [], False), # Below x lower bound
+ ((130, 100), [40, 120], [], False), # Above x upper bound
+ ((100, 30), [], [50, 150], False), # Below y lower bound
+ ((100, 160), [], [50, 150], False), # Above y upper bound
+ ],
+)
+def test_valid_position(
+ mocker, amigo, mock_settings, data, x_regions, y_regions, expected_valid
+):
+ """Test position validation against regions"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.x_regions = x_regions
+ touch.y_regions = y_regions
+
+ assert touch.valid_position(data) == expected_valid
+
+
+@pytest.mark.parametrize(
+ "y_regions,x_regions,touch_point,expected_index",
+ [
+ ([60, 120], [], (100, 50), 0), # y=50 < 60: index 0
+ ([60, 120], [], (100, 80), 0), # y=80 between 60 and 120: index 0
+ ([60, 120], [], (100, 130), 1), # y=130 > 120: index 1
+ ([40, 80, 120], [], (50, 30), 0), # y=30 < 40: index 0
+ ([40, 80, 120], [], (50, 50), 0), # y=50 between 40-80: index 0
+ ([40, 80, 120], [], (50, 90), 1), # y=90 between 80-120: index 1
+ ([40, 80, 120], [], (50, 130), 2), # y=130 > 120: index 2
+ ],
+)
+def test_extract_index(
+ mocker, amigo, mock_settings, y_regions, x_regions, touch_point, expected_index
+):
+ """Test index extraction from touch coordinates"""
+ from krux.touch import Touch
+
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mocker.MagicMock())
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.y_regions = y_regions
+ touch.x_regions = x_regions
+
+ index = touch._extract_index(touch_point)
+ assert index == expected_index
+
+
+def test_current_state_idle_to_pressed(mocker, amigo, mock_settings):
+ """Test state transition from IDLE to PRESSED"""
+ from krux.touch import Touch, IDLE, PRESSED
+
+ mock_driver = mocker.MagicMock()
+ mock_driver.current_point.return_value = (100, 100)
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.y_regions = [60, 120]
+
+ assert touch.state == IDLE
+
+ state = touch.current_state()
+
+ assert state == PRESSED
+ assert len(touch.press_point) == 1
+ assert touch.press_point[0] == (100, 100)
+
+
+def test_current_state_pressed_to_released(mocker, amigo, mock_settings):
+ """Test state transition from PRESSED to RELEASED"""
+ from krux.touch import Touch, PRESSED, RELEASED
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.state = PRESSED
+ touch.press_point = [(100, 100)]
+ touch.release_point = (105, 105)
+
+ # Return None to simulate touch release
+ mock_driver.current_point.return_value = None
+
+ state = touch.current_state()
+
+ assert state == RELEASED
+
+
+def test_current_state_released_to_idle(mocker, amigo, mock_settings):
+ """Test state transition from RELEASED to IDLE"""
+ from krux.touch import Touch, RELEASED, IDLE
+
+ mock_driver = mocker.MagicMock()
+ mock_driver.current_point.return_value = None
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.state = RELEASED
+
+ state = touch.current_state()
+
+ assert state == IDLE
+
+
+@pytest.mark.parametrize(
+ "press_point,release_point,expected_gesture",
+ [
+ ((100, 100), (160, 100), 1), # SWIPE_RIGHT (lateral = 60)
+ ((160, 100), (100, 100), 2), # SWIPE_LEFT (lateral = -60)
+ ((100, 100), (100, 160), 4), # SWIPE_DOWN (vertical = 60 > lateral)
+ ((100, 160), (100, 100), 3), # SWIPE_UP (vertical = -60 > lateral)
+ ((100, 100), (105, 105), None), # No gesture (< threshold)
+ ],
+)
+def test_gesture_detection(
+ mocker, amigo, mock_settings, press_point, release_point, expected_gesture
+):
+ """Test swipe gesture detection"""
+ from krux.touch import Touch, PRESSED
+
+ mock_driver = mocker.MagicMock()
+ mock_driver.current_point.return_value = None
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.state = PRESSED
+ touch.press_point = [press_point]
+ touch.release_point = release_point
+
+ touch.current_state()
+
+ assert touch.gesture == expected_gesture
+
+
+def test_event_with_validation(mocker, amigo, mock_settings):
+ """Test event detection with position validation"""
+ from krux.touch import Touch
+
+ mock_driver = mocker.MagicMock()
+ mock_driver.event.return_value = True
+ mock_driver.irq_point = (100, 100)
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.y_regions = [50, 150]
+ touch.sample_time = 900 # 100ms ago
+
+ result = touch.event(validate_position=True)
+
+ assert result is True
+
+
+def test_event_invalid_position(mocker, amigo, mock_settings):
+ """Test event detection with invalid position"""
+ from krux.touch import Touch
+
+ mock_driver = mocker.MagicMock()
+ mock_driver.event.return_value = True
+ mock_driver.irq_point = (100, 30) # Below y_regions[0]
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.y_regions = [50, 150]
+ touch.sample_time = 900
+
+ result = touch.event(validate_position=True)
+
+ assert result is False
+
+
+def test_event_without_validation(mocker, amigo, mock_settings):
+ """Test event detection without position validation"""
+ from krux.touch import Touch
+
+ mock_driver = mocker.MagicMock()
+ mock_driver.event.return_value = True
+ mock_driver.irq_point = (100, 30) # Would be invalid if validated
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.y_regions = [50, 150]
+ touch.sample_time = 900
+
+ result = touch.event(validate_position=False)
+
+ assert result is True
+
+
+def test_event_sample_period_not_elapsed(mocker, amigo, mock_settings):
+ """Test event not triggered if sample period hasn't elapsed"""
+ from krux.touch import Touch
+
+ mock_driver = mocker.MagicMock()
+ mock_driver.event.return_value = True
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1010)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.sample_time = 1000 # 10ms ago (< TOUCH_S_PERIOD=20)
+
+ result = touch.event()
+
+ assert result is False
+
+
+def test_value(mocker, amigo, mock_settings):
+ """Test value() method returns button-like behavior"""
+ from krux.touch import Touch, IDLE, PRESSED
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ # IDLE state returns 1
+ mock_driver.current_point.return_value = None
+ touch.state = IDLE
+ assert touch.value() == 1
+
+ # Non-IDLE state returns 0
+ # When touch is pressed, driver returns coordinates
+ mock_driver.current_point.return_value = (100, 100)
+ touch.state = IDLE # Start from IDLE
+ result = touch.value()
+ # State becomes PRESSED, so value returns 0
+ assert result == 0
+
+
+def test_swipe_right_value(mocker, amigo, mock_settings):
+ """Test swipe right detection and reset"""
+ from krux.touch import Touch, SWIPE_RIGHT
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ touch.gesture = SWIPE_RIGHT
+ assert touch.swipe_right_value() == 0
+ assert touch.gesture is None # Gesture cleared
+
+ assert touch.swipe_right_value() == 1 # No gesture
+
+
+def test_swipe_left_value(mocker, amigo, mock_settings):
+ """Test swipe left detection and reset"""
+ from krux.touch import Touch, SWIPE_LEFT
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ touch.gesture = SWIPE_LEFT
+ assert touch.swipe_left_value() == 0
+ assert touch.gesture is None
+
+
+def test_swipe_up_value(mocker, amigo, mock_settings):
+ """Test swipe up detection and reset"""
+ from krux.touch import Touch, SWIPE_UP
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ touch.gesture = SWIPE_UP
+ assert touch.swipe_up_value() == 0
+ assert touch.gesture is None
+
+
+def test_swipe_down_value(mocker, amigo, mock_settings):
+ """Test swipe down detection and reset"""
+ from krux.touch import Touch, SWIPE_DOWN
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+
+ touch.gesture = SWIPE_DOWN
+ assert touch.swipe_down_value() == 0
+ assert touch.gesture is None
+
+
+def test_current_index(mocker, amigo, mock_settings):
+ """Test getting current touch index"""
+ from krux.touch import Touch
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.index = 5
+
+ assert touch.current_index() == 5
+
+
+def test_press_point_averaging(mocker, amigo, mock_settings):
+ """Test that multiple touch samples are averaged"""
+ from krux.touch import Touch, PRESSED
+
+ mock_driver = mocker.MagicMock()
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
+ mocker.patch("time.ticks_ms", return_value=1000)
+
+ touch = Touch(width=240, height=135, irq_pin=20)
+ touch.state = PRESSED
+ touch.press_point = [(100, 100), (102, 102), (98, 98)]
+
+ # Add another point
+ mock_driver.current_point.return_value = (100, 100)
+ touch.current_state()
+
+ # Should have 4 points now
+ assert len(touch.press_point) == 4
+
+ # Index should be calculated from average
+ # Average = (100+102+98+100)/4, (100+102+98+100)/4 = (100, 100)
+ # (Note: actual index depends on regions set)
+
+
+def test_touch_event_integration(mocker, amigo, mock_settings):
+ """Integration test: full event sequence with touch point averaging and index calculation"""
from krux.touch import Touch
import board
@@ -15,47 +521,31 @@ def test_touch_event(mocker, amigo):
EVENTS = [False, True, False, False]
POINTS = [TOUCH_POINT_1, TOUCH_POINT_2, None, None]
- mocker.patch("krux.touchscreens.ft6x36.FT6X36.event", side_effect=EVENTS)
- mocker.patch("krux.touchscreens.ft6x36.FT6X36.current_point", side_effect=POINTS)
-
- mocker.patch.object(time, "ticks_ms", side_effect=mock_timer_ticks())
-
- touch = Touch(
- board.config["lcd"]["width"],
- board.config["lcd"]["height"],
- board.config["krux"]["pins"]["TOUCH_IRQ"],
- )
- touch.y_regions = Y_REGIONS
- touch.touch_driver.irq_point = TOUCH_POINT_1 # Simulate touch event
+ mock_driver = mocker.MagicMock()
+ mock_driver.event.side_effect = EVENTS
+ mock_driver.current_point.side_effect = POINTS
+ mocker.patch("krux.touchscreens.ft6x36.touch_control", mock_driver)
- for _ in range(len(EVENTS)):
- event = touch.event(validate_position=True)
- assert event == False
- assert touch.current_index() == 3
+ # Mock time advancing
+ tick_time = [10000]
+ def mock_time_advance():
+ tick_time[0] += 100
+ return tick_time[0] - 100
-def test_set_regions(mocker, amigo):
- from krux.touch import Touch
- import board
- import pytest
+ mocker.patch.object(time, "ticks_ms", side_effect=mock_time_advance)
touch = Touch(
board.config["lcd"]["width"],
board.config["lcd"]["height"],
board.config["krux"]["pins"]["TOUCH_IRQ"],
)
+ touch.y_regions = Y_REGIONS
+ mock_driver.irq_point = TOUCH_POINT_1 # Simulate touch event
- touch.set_regions()
-
- assert touch.x_regions == touch.y_regions == []
-
- with pytest.raises(ValueError, match="x_list must be a list"):
- touch.set_regions(1)
-
- with pytest.raises(ValueError, match="y_list must be a list"):
- touch.set_regions(None, 1)
-
- touch.set_regions([1, 2, 3], [4, 5, 6])
+ event_result = None
+ for _ in range(len(EVENTS)):
+ event_result = touch.event(validate_position=True)
- assert touch.x_regions == [1, 2, 3]
- assert touch.y_regions == [4, 5, 6]
+ assert event_result is False
+ assert touch.current_index() == 3
diff --git a/tests/touchscreens/helpers.py b/tests/touchscreens/helpers.py
new file mode 100644
index 0000000..7eff6be
--- /dev/null
+++ b/tests/touchscreens/helpers.py
@@ -0,0 +1,46 @@
+# Test helpers for touchscreen tests
+
+
+def create_cst816_touch_data(points, event, x_coord, y_coord):
+ """Helper to construct CST816 touch data bytes.
+
+ Args:
+ points: Number of touch points (0x00 or 0x01)
+ event: Event type (e.g., EVENT_TOUCH_CONTACT)
+ x_coord: X coordinate (0-4095)
+ y_coord: Y coordinate (0-4095)
+
+ Returns:
+ list: 6-byte touch data array
+ """
+ x_high = (x_coord >> 8) & 0x0F
+ x_low = x_coord & 0xFF
+ y_high = (y_coord >> 8) & 0x0F
+ y_low = y_coord & 0xFF
+
+ return [
+ 0x00, # gesture
+ points,
+ (event << 6) | x_high,
+ x_low,
+ y_high,
+ y_low,
+ ]
+
+
+def create_ft6x36_touch_data(x_coord, y_coord):
+ """Helper to construct FT6X36 touch data bytes.
+
+ Args:
+ x_coord: X coordinate (0-4095)
+ y_coord: Y coordinate (0-4095)
+
+ Returns:
+ list: 4-byte touch data array [x_high, x_low, y_high, y_low]
+ """
+ x_high = (x_coord >> 8) & 0x0F
+ x_low = x_coord & 0xFF
+ y_high = (y_coord >> 8) & 0x0F
+ y_low = y_coord & 0xFF
+
+ return [x_high, x_low, y_high, y_low]
diff --git a/tests/touchscreens/test_cst816.py b/tests/touchscreens/test_cst816.py
new file mode 100644
index 0000000..1e33c51
--- /dev/null
+++ b/tests/touchscreens/test_cst816.py
@@ -0,0 +1,326 @@
+import pytest
+
+
+@pytest.fixture
+def mock_cst816(mocker):
+ from krux.touchscreens.cst816 import (
+ fm,
+ touch_control,
+ CST816S_TOUCH_DATA,
+ CST816S_VERSION,
+ EVENT_TOUCH_CONTACT,
+ SCREEN_WIDTH,
+ )
+
+ mocker.patch.object(fm, "register")
+ mock_gpio = mocker.MagicMock()
+ mocker.patch("Maix.GPIO", return_value=mock_gpio)
+
+ def fake_activate(irq_pin):
+ touch_control.irq_pin = mock_gpio
+
+ def fake_write_reg(reg_addr, buf):
+ pass
+
+ def fake_read_reg(reg_addr, buf_len):
+ if reg_addr == CST816S_VERSION:
+ return [0x01]
+ elif reg_addr == CST816S_TOUCH_DATA:
+ from .helpers import create_cst816_touch_data
+
+ return create_cst816_touch_data(
+ points=0x01, event=EVENT_TOUCH_CONTACT, x_coord=0x0050, y_coord=0x00A0
+ )
+ return [0x00]
+
+ mocker.patch.object(touch_control, "_write_reg", fake_write_reg)
+ mocker.patch.object(touch_control, "_read_reg", fake_read_reg)
+ mocker.patch.object(touch_control, "activate", fake_activate)
+
+ touch_control.event_flag = False
+ touch_control.irq_point = None
+ touch_control.last_touch_time = 0
+ mocker.spy(touch_control, "trigger_event")
+
+ return touch_control
+
+
+def test_handler(mocker, embed_fire, mock_cst816):
+ from krux.touchscreens.cst816 import __handler__, ACTIVITY_THRESHOLD
+
+ mocker.patch("time.ticks_ms", return_value=ACTIVITY_THRESHOLD + 100)
+ __handler__(10)
+
+ mock_cst816.trigger_event.assert_called_once()
+ assert mock_cst816.event_flag
+ assert mock_cst816.irq_point is not None
+ assert mock_cst816.event()
+ assert not mock_cst816.event_flag
+
+
+def test_debounce_trigger_event_too_soon(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816, ACTIVITY_THRESHOLD
+
+ touch = CST816()
+ touch.last_touch_time = 1000
+
+ mocker.patch("time.ticks_ms", return_value=1000 + ACTIVITY_THRESHOLD - 10)
+ mock_validate = mocker.patch.object(touch, "current_point_validate")
+
+ touch.trigger_event()
+
+ assert not touch.event_flag
+ assert touch.irq_point is None
+ mock_validate.assert_not_called()
+ assert touch.last_touch_time == 1000 + ACTIVITY_THRESHOLD - 10
+
+
+def test_init(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816
+
+ t = CST816()
+
+ assert t.irq_pin is None
+ assert t.event_flag is False
+ assert t.irq_point is None
+ assert t.last_touch_time == 0
+
+
+def test_point_not_read(mocker, dock):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ data = touch.current_point()
+
+ assert not data
+
+
+def test_point_read(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT, SCREEN_WIDTH
+ from .helpers import create_cst816_touch_data
+
+ touch = CST816()
+ mocker.patch("time.ticks_ms", return_value=1000)
+ touch.last_touch_time = 950
+
+ x_coord = 0x0050
+ y_coord = 0x00A0
+ touch_data = create_cst816_touch_data(0x01, EVENT_TOUCH_CONTACT, x_coord, y_coord)
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data)
+
+ data = touch.current_point()
+ expected = (SCREEN_WIDTH - x_coord, y_coord)
+ assert data == expected
+
+
+def test_point_validate_read(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT, SCREEN_WIDTH
+ from .helpers import create_cst816_touch_data
+
+ touch = CST816()
+ x_coord = 0x0028
+ y_coord = 0x0078
+ touch_data = create_cst816_touch_data(0x01, EVENT_TOUCH_CONTACT, x_coord, y_coord)
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data)
+
+ data = touch.current_point_validate()
+ expected = (SCREEN_WIDTH - x_coord, y_coord)
+ assert data == expected
+
+
+def test_no_touch_points(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT
+ from .helpers import create_cst816_touch_data
+
+ touch = CST816()
+ touch_data = create_cst816_touch_data(0x00, EVENT_TOUCH_CONTACT, 0x00, 0x00)
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data)
+
+ data = touch.current_point_validate()
+ assert data is None
+
+
+def test_wrong_event_type(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ points = 0x01
+ event = 0x01 << 6
+ touch_data = [0x00, points, event, 0x50, 0x00, 0xA0]
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data)
+
+ data = touch.current_point_validate()
+ assert data is None
+
+
+def test_point_read_error(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ mocker.patch("time.ticks_ms", return_value=1000)
+ touch.last_touch_time = 950
+
+ e = Exception("mocked error")
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", side_effect=e)
+
+ data = touch.current_point()
+ assert data is None
+
+
+def test_read_touch_data_parsing_error(mocker, embed_fire, capsys):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ mocker.patch.object(
+ touch, "_read_reg", side_effect=Exception("Touch data parsing error")
+ )
+
+ data = touch._read_touch_data()
+
+ assert data is None
+ captured = capsys.readouterr()
+ assert "CST816S touch data read error:" in captured.out
+ assert "Touch data parsing error" in captured.out
+
+
+def test_point_outside_threshold(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816, ACTIVITY_THRESHOLD
+
+ touch = CST816()
+ mocker.patch("time.ticks_ms", return_value=1000)
+ touch.last_touch_time = 1000 - ACTIVITY_THRESHOLD - 10
+
+ data = touch.current_point()
+ assert data is None
+
+
+def test_activate(mocker, embed_fire):
+ from krux.touchscreens.cst816 import CST816, fm
+
+ mock_fm_register = mocker.patch.object(fm, "register")
+ mock_gpio = mocker.MagicMock()
+ mocker.patch("Maix.GPIO", return_value=mock_gpio)
+
+ touch = CST816()
+ test_pin = 21
+
+ assert touch.irq_pin is None
+
+ touch.activate(test_pin)
+
+ mock_fm_register.assert_called_with(test_pin, fm.fpioa.GPIOHS1)
+ assert touch.irq_pin is not None
+
+
+def test_init_cst816_read_error(mocker, embed_fire, capsys):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ mocker.patch.object(touch, "_read_reg", side_effect=Exception("I2C read error"))
+ mock_write = mocker.patch.object(touch, "_write_reg")
+
+ touch._init_cst816()
+
+ captured = capsys.readouterr()
+ assert "CST816S initialization error:" in captured.out
+ assert "I2C read error" in captured.out
+ mock_write.assert_not_called()
+
+
+def test_init_cst816_write_error(mocker, embed_fire, capsys):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ mocker.patch.object(touch, "_read_reg", return_value=[0x01])
+ mocker.patch.object(touch, "_write_reg", side_effect=Exception("I2C write error"))
+
+ touch._init_cst816()
+
+ captured = capsys.readouterr()
+ assert "CST816S initialization error:" in captured.out
+ assert "I2C write error" in captured.out
+
+
+def test_write_reg_exception(mocker, embed_fire, capsys):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ mocker.patch("krux.i2c.i2c_bus.writeto", side_effect=Exception("I2C write failed"))
+
+ touch._write_reg(0x00, bytearray([0x01]))
+
+ captured = capsys.readouterr()
+ assert "CST816S write error:" in captured.out
+ assert "I2C write failed" in captured.out
+
+
+def test_write_reg_no_i2c_bus(dock):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ touch._write_reg(0x00, bytearray([0x01]))
+
+
+def test_read_reg_exception(mocker, embed_fire, capsys):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ mocker.patch("krux.i2c.i2c_bus.writeto", side_effect=Exception("I2C read failed"))
+
+ result = touch._read_reg(0x00, 1)
+
+ captured = capsys.readouterr()
+ assert "CST816S read error:" in captured.out
+ assert "I2C read failed" in captured.out
+ assert result is None
+
+
+def test_read_reg_no_i2c_bus(dock):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ result = touch._read_reg(0x00, 1)
+
+ assert result is None
+
+
+def test_threshold(mocker, embed_fire, capsys):
+ from krux.touchscreens.cst816 import CST816
+
+ touch = CST816()
+ touch.threshold(30)
+
+ captured = capsys.readouterr()
+ assert "Threshold setting not supported" in captured.out
+
+
+@pytest.mark.parametrize(
+ "x_coord,y_coord,expected_x",
+ [
+ (0x0000, 0x0000, 240),
+ (0x0050, 0x00A0, 160),
+ (0x00F0, 0x0140, 0),
+ (0x0078, 0x0140, 120),
+ ],
+)
+def test_coordinate_mirroring(mocker, embed_fire, x_coord, y_coord, expected_x):
+ from krux.touchscreens.cst816 import CST816, EVENT_TOUCH_CONTACT
+ from .helpers import create_cst816_touch_data
+
+ touch = CST816()
+ touch_data = create_cst816_touch_data(0x01, EVENT_TOUCH_CONTACT, x_coord, y_coord)
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=touch_data)
+
+ data = touch.current_point_validate()
+ assert data == (expected_x, y_coord)
diff --git a/tests/touchscreens/test_ft6x36.py b/tests/touchscreens/test_ft6x36.py
index 8929ab2..757f112 100644
--- a/tests/touchscreens/test_ft6x36.py
+++ b/tests/touchscreens/test_ft6x36.py
@@ -40,28 +40,31 @@ def mock_ft6x36(mocker):
def test_handler(mocker, amigo, mock_ft6x36):
from krux.touchscreens.ft6x36 import __handler__
- # The pin numbers are arbitrary, just for testing
- # and they will be ignored in the handler
- calls = []
- for pin in [10, 11, 12]:
- __handler__(pin)
- calls.append(mocker.call())
-
- mock_ft6x36.trigger_event.assert_has_calls(calls)
+ # Trigger the handler (pin number is arbitrary and ignored)
+ __handler__(10)
+
+ # Verify trigger_event was called
+ mock_ft6x36.trigger_event.assert_called_once()
+
+ # Verify event flag was set and point was captured
assert mock_ft6x36.event_flag
assert mock_ft6x36.irq_point is not None
- # get the event event flag and cleans it
+ # Verify event() returns flag and clears it
assert mock_ft6x36.event()
assert not mock_ft6x36.event_flag
def test_init(mocker, amigo):
- from krux.touchscreens.ft6x36 import FT6X36
+ from krux.touchscreens.ft6x36 import FT6X36, FT6X36_ADDR
t = FT6X36()
- assert isinstance(t, FT6X36)
+ # Verify initial state
+ assert t.touch_irq_pin is None
+ assert t.event_flag is False
+ assert t.irq_point is None
+ assert t.addr == FT6X36_ADDR
def test_point_not_read(mocker, dock):
@@ -87,23 +90,77 @@ def test_point_read(mocker, amigo):
assert data == point
-def test_gesture_discarded(mocker, amigo):
+@pytest.mark.parametrize(
+ "touch_points,expected_result",
+ [
+ (0x00, None), # No touch
+ (0x01, (0x09, 0x09)), # Single touch (valid)
+ (0x02, None), # Two touches (not supported)
+ (0x03, None), # Three touches (not supported)
+ ],
+)
+def test_touch_point_counts(mocker, amigo, touch_points, expected_result):
from krux.touchscreens.ft6x36 import FT6X36
touch = FT6X36()
- # four bytes
reg_point = [0x00, 0x09, 0x00, 0x09]
- mocker.patch("krux.i2c.i2c_bus.readfrom_mem", side_effect=([0x02], reg_point))
+ mocker.patch(
+ "krux.i2c.i2c_bus.readfrom_mem", side_effect=([touch_points], reg_point)
+ )
data = touch.current_point()
- assert data is None
+ assert data == expected_result
def test_point_read_error(mocker, amigo):
from krux.touchscreens.ft6x36 import FT6X36
touch = FT6X36()
- # four bytes
e = Exception("mocked error")
mocker.patch("krux.i2c.i2c_bus.readfrom_mem", side_effect=([0x01], e))
data = touch.current_point()
- assert data == e
+ assert data is None
+
+
+def test_activate_irq(mocker, amigo):
+ from krux.touchscreens.ft6x36 import FT6X36, fm
+
+ mock_fm_register = mocker.patch.object(fm, "register")
+ mock_gpio = mocker.MagicMock()
+ mocker.patch("Maix.GPIO", return_value=mock_gpio)
+
+ touch = FT6X36()
+ test_pin = 20
+
+ # Before activation, IRQ pin should be None
+ assert touch.touch_irq_pin is None
+
+ touch.activate_irq(test_pin)
+
+ # After activation, verify:
+ # 1. Pin was registered with fpioa manager
+ mock_fm_register.assert_called_with(test_pin, fm.fpioa.GPIOHS1)
+
+ # 2. IRQ pin was configured
+ assert touch.touch_irq_pin is not None
+
+
+def test_threshold(mocker, amigo):
+ from krux.touchscreens.ft6x36 import FT6X36, FT_ID_G_THGROUP
+
+ mock_writeto = mocker.patch("krux.i2c.i2c_bus.writeto_mem")
+
+ touch = FT6X36()
+ test_threshold = 30
+
+ touch.threshold(test_threshold)
+
+ # Verify write_reg was called with correct threshold value
+ # write_reg should have been called during __init__ and when setting threshold
+ calls = mock_writeto.call_args_list
+ # Find the call that sets our test threshold
+ threshold_calls = [
+ call
+ for call in calls
+ if call[0][1] == FT_ID_G_THGROUP and call[0][2] == test_threshold
+ ]
+ assert len(threshold_calls) >= 1
diff --git a/tests/touchscreens/test_gt911.py b/tests/touchscreens/test_gt911.py
new file mode 100644
index 0000000..2c5c315
--- /dev/null
+++ b/tests/touchscreens/test_gt911.py
@@ -0,0 +1,352 @@
+import pytest
+
+
+@pytest.fixture
+def mock_gt911(mocker):
+ from krux.touchscreens.gt911 import (
+ fm,
+ touch_control,
+ GT911_COORD_ADDR,
+ GT911_PRODUCT_ID,
+ )
+
+ mocker.patch.object(fm, "register")
+ mock_gpio = mocker.MagicMock()
+ mocker.patch("Maix.GPIO", return_value=mock_gpio)
+ mocker.patch("time.sleep_ms") # Mock sleep to avoid delays in tests
+
+ def fake_activate(irq_pin, res_pin):
+ touch_control.irq_pin = mock_gpio
+ touch_control.reset_pin = mock_gpio
+
+ def fake_write_reg(reg_addr, buf):
+ pass
+
+ def fake_read_reg(reg_addr, buf_len):
+ if reg_addr == GT911_PRODUCT_ID:
+ # Mock product ID (4 bytes: "911")
+ return [0x39, 0x31, 0x31, 0x00] # "911" in ASCII
+
+ elif reg_addr == GT911_COORD_ADDR:
+ # Mock status byte: 0x81 = buffer ready (0x80) + 1 touch point (0x01)
+ return [0x81]
+
+ elif reg_addr == GT911_COORD_ADDR + 1:
+ # Mock touch data: 8 bytes
+ # [track_id, x_low, x_high, y_low, y_high, size_low, size_high, reserved]
+ x_coord = 0x0064 # X = 100
+ y_coord = 0x00C8 # Y = 200
+ return [
+ 0x00, # track_id
+ x_coord & 0xFF, # x_low
+ (x_coord >> 8) & 0xFF, # x_high
+ y_coord & 0xFF, # y_low
+ (y_coord >> 8) & 0xFF, # y_high
+ 0x00, # size_low
+ 0x00, # size_high
+ 0x00, # reserved
+ ]
+
+ return [0x00]
+
+ mocker.patch.object(touch_control, "write_reg", fake_write_reg)
+ mocker.patch.object(touch_control, "read_reg", fake_read_reg)
+ mocker.patch.object(touch_control, "activate", fake_activate)
+
+ touch_control.event_flag = False
+ touch_control.irq_point = None
+ touch_control.last_touch_time = 0
+ mocker.spy(touch_control, "trigger_event")
+
+ return touch_control
+
+
+def test_handler(mocker, wonder_k, mock_gt911):
+ from krux.touchscreens.gt911 import __handler__, ACTIVITY_THRESHOLD
+
+ # Mock time to ensure debounce criteria is met
+ mocker.patch("time.ticks_ms", return_value=ACTIVITY_THRESHOLD + 100)
+
+ # Trigger the handler (pin number is arbitrary and ignored)
+ __handler__(10)
+
+ # Verify trigger_event was called
+ mock_gt911.trigger_event.assert_called_once()
+
+ # Verify event flag was set and point was captured
+ assert mock_gt911.event_flag
+ assert mock_gt911.irq_point is not None
+
+ # Verify event() returns flag and clears it
+ assert mock_gt911.event()
+ assert not mock_gt911.event_flag
+
+
+def test_init(mocker, wonder_k):
+ from krux.touchscreens.gt911 import GT911, GT911_ADDR
+
+ t = GT911()
+
+ # Verify initial state
+ assert t.irq_pin is None
+ assert t.reset_pin is None
+ assert t.event_flag is False
+ assert t.irq_point is None
+ assert t.addr == GT911_ADDR
+ assert t.last_touch_time == 0
+
+
+def test_point_not_read(mocker, dock):
+ # Dock does not set i2c_bus because it does not
+ # use I2C_SCL or I2C_SDA pins
+ from krux.touchscreens.gt911 import GT911
+
+ touch = GT911()
+ data = touch.current_point()
+
+ assert not data
+
+
+def test_point_read(mocker, wonder_k):
+ from krux.touchscreens.gt911 import GT911
+
+ touch = GT911()
+
+ # Mock time to ensure we're within activity threshold
+ mocker.patch("time.ticks_ms", return_value=1000)
+ touch.last_touch_time = 950 # 50ms ago, within threshold
+
+ # Mock coordinate data: X=100, Y=200
+ x_coord = 0x0064
+ y_coord = 0x00C8
+ coord_data = [
+ 0x00,
+ x_coord & 0xFF,
+ (x_coord >> 8) & 0xFF,
+ y_coord & 0xFF,
+ (y_coord >> 8) & 0xFF,
+ 0x00,
+ 0x00,
+ 0x00,
+ ]
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", return_value=coord_data)
+
+ data = touch.current_point()
+ assert data == (x_coord, y_coord)
+
+
+def test_point_validate_read(mocker, wonder_k):
+ from krux.touchscreens.gt911 import GT911
+
+ touch = GT911()
+
+ # Mock status: 0x81 (buffer ready + 1 touch point)
+ # Mock coordinates: X=150, Y=250
+ x_coord = 0x0096
+ y_coord = 0x00FA
+ coord_data = [
+ 0x00,
+ x_coord & 0xFF,
+ (x_coord >> 8) & 0xFF,
+ y_coord & 0xFF,
+ (y_coord >> 8) & 0xFF,
+ 0x00,
+ 0x00,
+ 0x00,
+ ]
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch(
+ "krux.i2c.i2c_bus.readfrom",
+ side_effect=[[0x81], coord_data], # First call: status, second: coords
+ )
+
+ data = touch.current_point_validate()
+ assert data == (x_coord, y_coord)
+
+
+@pytest.mark.parametrize(
+ "status,expected",
+ [
+ (0x00, None), # No touch, buffer not ready
+ (0x80, None), # Buffer ready but no touch points
+ (0x01, None), # Touch point but buffer not ready
+ (0x81, (100, 200)), # Valid: buffer ready + touch point
+ (
+ 0x82,
+ (100, 200),
+ ), # Valid: buffer ready + 2 touch points (first point returned)
+ ],
+)
+def test_status_validation(mocker, wonder_k, status, expected):
+ """Test different status register values"""
+ from krux.touchscreens.gt911 import GT911
+
+ touch = GT911()
+
+ # Mock coordinates: X=100, Y=200
+ coord_data = [0x00, 0x64, 0x00, 0xC8, 0x00, 0x00, 0x00, 0x00]
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch(
+ "krux.i2c.i2c_bus.readfrom",
+ side_effect=[[status], coord_data],
+ )
+
+ data = touch.current_point_validate()
+ assert data == expected
+
+
+def test_point_read_error(mocker, wonder_k):
+ from krux.touchscreens.gt911 import GT911
+
+ touch = GT911()
+
+ # Mock time to ensure we're within activity threshold
+ mocker.patch("time.ticks_ms", return_value=1000)
+ touch.last_touch_time = 950
+
+ # Simulate error
+ e = Exception("mocked error")
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("krux.i2c.i2c_bus.readfrom", side_effect=e)
+
+ data = touch.current_point()
+ assert data is None
+
+
+def test_point_outside_threshold(mocker, wonder_k):
+ from krux.touchscreens.gt911 import GT911, ACTIVITY_THRESHOLD
+
+ touch = GT911()
+
+ # Mock time to ensure we're outside activity threshold
+ mocker.patch("time.ticks_ms", return_value=1000)
+ touch.last_touch_time = 1000 - ACTIVITY_THRESHOLD - 10 # Outside threshold
+
+ data = touch.current_point()
+ assert data is None
+
+
+def test_activate(mocker, wonder_k):
+ from krux.touchscreens.gt911 import GT911, fm
+
+ mock_fm_register = mocker.patch.object(fm, "register")
+ mock_gpio = mocker.MagicMock()
+ mocker.patch("Maix.GPIO", return_value=mock_gpio)
+ mocker.patch("time.sleep_ms")
+
+ touch = GT911()
+ test_irq_pin = 21
+ test_reset_pin = 22
+
+ # Before activation
+ assert touch.irq_pin is None
+ assert touch.reset_pin is None
+
+ touch.activate(test_irq_pin, test_reset_pin)
+
+ # After activation, verify pins were registered
+ assert mock_fm_register.call_count == 2
+ calls = mock_fm_register.call_args_list
+ assert calls[0][0] == (test_irq_pin, fm.fpioa.GPIOHS1)
+ assert calls[1][0] == (test_reset_pin, fm.fpioa.GPIOHS2)
+
+ # Verify pins were configured
+ assert touch.irq_pin is not None
+ assert touch.reset_pin is not None
+
+
+def test_threshold(mocker, wonder_k):
+ from krux.touchscreens.gt911 import GT911, GT911_CONFIG_DATA
+
+ mock_writeto = mocker.patch("krux.i2c.i2c_bus.writeto")
+
+ touch = GT911()
+ test_threshold = 50
+
+ touch.threshold(test_threshold)
+
+ # Verify write_reg was called with correct threshold value
+ calls = mock_writeto.call_args_list
+ # Find the call that sets our test threshold
+ # GT911 uses 16-bit addresses, so payload is [addr_high, addr_low, value]
+ threshold_calls = [
+ call
+ for call in calls
+ if len(call[0]) >= 2
+ and len(call[0][1]) >= 3
+ and call[0][1][0] == (GT911_CONFIG_DATA >> 8)
+ and call[0][1][1] == (GT911_CONFIG_DATA & 0xFF)
+ and call[0][1][2] == test_threshold
+ ]
+ assert len(threshold_calls) >= 1
+
+
+@pytest.mark.parametrize(
+ "x_coord,y_coord",
+ [
+ (0x0000, 0x0000), # Min coordinates
+ (0x0064, 0x00C8), # Mid coordinates: (100, 200)
+ (0x01E0, 0x0320), # Large coordinates: (480, 800)
+ (0x00FF, 0x00FF), # Equal X,Y: (255, 255)
+ ],
+)
+def test_coordinate_reading(mocker, wonder_k, x_coord, y_coord):
+ """Test coordinate extraction from touch data"""
+ from krux.touchscreens.gt911 import GT911
+
+ touch = GT911()
+
+ coord_data = [
+ 0x00,
+ x_coord & 0xFF,
+ (x_coord >> 8) & 0xFF,
+ y_coord & 0xFF,
+ (y_coord >> 8) & 0xFF,
+ 0x00,
+ 0x00,
+ 0x00,
+ ]
+
+ mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch(
+ "krux.i2c.i2c_bus.readfrom",
+ side_effect=[[0x81], coord_data],
+ )
+
+ data = touch.current_point_validate()
+ assert data == (x_coord, y_coord)
+
+
+def test_trigger_event_clears_irq_flag(mocker, wonder_k):
+ """Verify that trigger_event clears the interrupt flag"""
+ from krux.touchscreens.gt911 import GT911, ACTIVITY_THRESHOLD, GT911_COORD_ADDR
+
+ mock_writeto = mocker.patch("krux.i2c.i2c_bus.writeto")
+ mocker.patch("time.ticks_ms", return_value=ACTIVITY_THRESHOLD + 100)
+
+ touch = GT911()
+
+ # Mock valid touch data
+ mocker.patch(
+ "krux.i2c.i2c_bus.readfrom",
+ side_effect=[[0x81], [0x00, 0x64, 0x00, 0xC8, 0x00, 0x00, 0x00, 0x00]],
+ )
+
+ touch.trigger_event()
+
+ # Verify interrupt flag was cleared
+ # Should write 0x00 to GT911_COORD_ADDR
+ clear_calls = [
+ call
+ for call in mock_writeto.call_args_list
+ if len(call[0]) >= 2
+ and len(call[0][1]) >= 3
+ and call[0][1][0] == (GT911_COORD_ADDR >> 8)
+ and call[0][1][1] == (GT911_COORD_ADDR & 0xFF)
+ and call[0][1][2] == 0x00
+ ]
+ assert len(clear_calls) >= 1
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.