diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl new file mode 100644 index 00000000..235dcd3f --- /dev/null +++ b/.beads/issues.jsonl @@ -0,0 +1,8 @@ +{"id":"specter-diy.new-6bg","title":"Fix C: Remove /qspi from sys.path in main.c","description":"Comment out /qspi and /qspi/lib from sys.path in stm32/main.c. Eliminates shadowing but users cant add modules to QSPI.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T18:52:46.875253+01:00","updated_at":"2025-12-19T19:27:55.312347+01:00","closed_at":"2025-12-19T19:27:55.312347+01:00","close_reason":"FAILED - CWD still /qspi, '' in sys.path still shadows"} +{"id":"specter-diy.new-9qr","title":"QSPI dirs shadow frozen modules after MicroPython upgrade","description":"New MicroPython uses .frozen as sys.path entry instead of absolute priority. CWD defaults to /qspi, so '' in sys.path finds /qspi/hosts before .frozen/hosts. Settings dirs shadow frozen modules. See .history/lvgl9-migration-lessons.md for details. Recommended fix: rename /qspi/hosts to /qspi/settings/hosts","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-19T18:47:45.871674+01:00","updated_at":"2025-12-19T19:30:24.589192+01:00","closed_at":"2025-12-19T19:30:24.589192+01:00","close_reason":"RESOLVED: Fix D implemented - .frozen first in sys.path. Tested all 4 fixes: A(fail), B(works+migration), C(fail), D(works)","dependencies":[{"issue_id":"specter-diy.new-9qr","depends_on_id":"specter-diy.new-gxv","type":"blocks","created_at":"2025-12-19T18:52:53.130203+01:00","created_by":"daemon"},{"issue_id":"specter-diy.new-9qr","depends_on_id":"specter-diy.new-bkl","type":"blocks","created_at":"2025-12-19T18:52:53.240199+01:00","created_by":"daemon"},{"issue_id":"specter-diy.new-9qr","depends_on_id":"specter-diy.new-6bg","type":"blocks","created_at":"2025-12-19T18:52:53.350989+01:00","created_by":"daemon"},{"issue_id":"specter-diy.new-9qr","depends_on_id":"specter-diy.new-kp3","type":"blocks","created_at":"2025-12-19T18:52:53.460851+01:00","created_by":"daemon"}]} +{"id":"specter-diy.new-bkl","title":"Fix B: Rename settings dirs to /qspi/settings/*","description":"Change Host.SETTINGS_DIR and app paths from /qspi/hosts to /qspi/settings/hosts etc. Cleanest long-term fix.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T18:52:46.610554+01:00","updated_at":"2025-12-19T19:23:08.013163+01:00","closed_at":"2025-12-19T19:23:08.013163+01:00","close_reason":"SUCCESS - works but requires migrating old /qspi dirs"} +{"id":"specter-diy.new-dz2","title":"VFS filesystems not mounting (/flash, /qspi)","description":"sdram.RAMDevice.ioctl() returns None instead of 0 for init/erase. VfsFat.mkfs fails with 'can't convert NoneType to int'. Bug in f469-disco/usermods/sdram module.","status":"open","priority":1,"issue_type":"bug","created_at":"2025-12-19T19:49:19.007935+01:00","updated_at":"2025-12-19T19:53:28.074456+01:00"} +{"id":"specter-diy.new-gxv","title":"Fix A: Change CWD in boot.py to /flash","description":"Change os.chdir('/flash') in boot.py before pyb.main() to avoid /qspi being current dir. Already attempted, needs re-test.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T18:52:46.357046+01:00","updated_at":"2025-12-19T18:54:29.409761+01:00","closed_at":"2025-12-19T18:54:29.409761+01:00","close_reason":"Failed: moving CWD to /flash still shadows because /flash/keystore exists. The '' in sys.path always shadows if CWD has matching dirs."} +{"id":"specter-diy.new-kp3","title":"Fix D: Move .frozen first in sys.path in runtime.c","description":"Insert .frozen at index 0 instead of append in py/runtime.c. Frozen modules always win regardless of CWD.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-19T18:52:47.141162+01:00","updated_at":"2025-12-19T19:15:17.619462+01:00","closed_at":"2025-12-19T19:15:17.619462+01:00","close_reason":"SUCCESS: Moving .frozen first in sys.path in runtime.c fixes the shadowing issue. Frozen modules now always take priority."} +{"id":"specter-diy.new-lp1","title":"QR scanner returns garbage data","description":"Scanner triggers/beeps/stops but data is binary garbage. PIN trigger mode at 9600 baud. May need different baud rate or scanner config.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-19T16:24:58.042665+01:00","updated_at":"2025-12-19T17:02:06.665561+01:00","closed_at":"2025-12-19T17:02:06.665561+01:00","close_reason":"Scanner works"} +{"id":"specter-diy.new-ok7","title":"Python code migration after MicroPython/LVGL 9.3.0 upgrade","description":"","status":"open","priority":1,"issue_type":"epic","created_at":"2025-12-19T16:24:40.788371+01:00","updated_at":"2025-12-19T16:24:40.788371+01:00","dependencies":[{"issue_id":"specter-diy.new-ok7","depends_on_id":"specter-diy.new-lp1","type":"blocks","created_at":"2025-12-19T16:25:03.594054+01:00","created_by":"daemon"}]} diff --git a/.gitmodules b/.gitmodules index 1066f1d7..b615da07 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "f469-disco"] path = f469-disco - url = https://github.com/diybitcoinhardware/f469-disco + url = https://github.com/diybitcoinhardware/f469-disco.git + branch = micropython-upgrade [submodule "bootloader"] path = bootloader url = https://github.com/cryptoadvance/specter-bootloader \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 16ec1063..689cf916 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,50 @@ -FROM python:3.9.15@sha256:b5f024fa682187ef9305a2b5d2c4bb583bef83356259669fc80273bb2222f5ed -ENV LANG=C.UTF-8 +# syntax=docker/dockerfile:1.6 +FROM python:3.9.23-bookworm@sha256:dc01447eea126f97459cbcb0e52a5863fcc84ff53462650ae5a28277c175f49d +ENV LANG=C.UTF-8 ARG DEBIAN_FRONTEND=noninteractive +ARG TOOLCHAIN_VER=14.3.rel1 +ARG TARGETARCH +ARG TARGET_DIR="/opt/arm-toolchain" + +# Minimal deps for download/verify/extract +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl xz-utils grep coreutils \ + && rm -rf /var/lib/apt/lists/* -# ARM Embedded Toolchain -# Integrity is checked using the MD5 checksum provided by ARM at https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm/downloads -RUN curl -sSfL -o arm-toolchain.tar.bz2 "https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2020q2/gcc-arm-none-eabi-9-2020-q2-update-x86_64-linux.tar.bz2?revision=05382cca-1721-44e1-ae19-1e7c3dc96118&rev=05382cca172144e1ae191e7c3dc96118&hash=3ACFE672E449EBA7A21773EE284A88BC7DFA5044" && \ - echo 2b9eeccc33470f9d3cda26983b9d2dc6 arm-toolchain.tar.bz2 > /tmp/arm-toolchain.md5 && \ - md5sum --check /tmp/arm-toolchain.md5 && rm /tmp/arm-toolchain.md5 && \ - tar xf arm-toolchain.tar.bz2 -C /opt && \ - rm arm-toolchain.tar.bz2 +# Default to Arm's blob storage mirror +ARG TOOLCHAIN_MIRROR=armkeil.blob.core.windows.net/developer/Files/downloads/gnu -# Adding GCC to PATH and defining rustup/cargo home directories -ENV PATH=/opt/gcc-arm-none-eabi-9-2020-q2-update/bin:$PATH +# Pin Arm GNU Toolchain per-arch with SHA256 sums +ENV TOOLCHAIN_SHA256_AMD64=8f6903f8ceb084d9227b9ef991490413014d991874a1e34074443c2a72b14dbd +ENV TOOLCHAIN_SHA256_ARM64=2d465847eb1d05f876270494f51034de9ace9abe87a4222d079f3360240184d3 -# Installing python requirements +# Arm GNU Toolchain (arm-none-eabi), host-aware, SHA-256 verified, cached download +RUN --mount=type=cache,target=/root/.cache \ + set -eux; \ + case "$TARGETARCH" in \ + amd64) host="x86_64"; TOOLCHAIN_SHA256="$TOOLCHAIN_SHA256_AMD64" ;; \ + arm64) host="aarch64"; TOOLCHAIN_SHA256="$TOOLCHAIN_SHA256_ARM64" ;; \ + *) echo "Unsupported arch: $TARGETARCH" && exit 1 ;; \ + esac; \ + file="arm-gnu-toolchain-${TOOLCHAIN_VER}-${host}-arm-none-eabi.tar.xz"; \ + url="https://${TOOLCHAIN_MIRROR}/${TOOLCHAIN_VER}/binrel/${file}"; \ + dest="/root/.cache/$file"; \ + if [ ! -f "$dest" ]; then \ + echo "Downloading $url"; \ + curl -fSL --retry 5 --retry-all-errors -C - -o "$dest" "$url"; \ + else \ + echo "Using cached $dest"; \ + fi; \ + echo "${TOOLCHAIN_SHA256} $dest" | sha256sum -c -; \ + tar -xJf "$dest" -C /opt; \ + ln -s /opt/arm-gnu-toolchain-${TOOLCHAIN_VER}-${host}-arm-none-eabi ${TARGET_DIR} + +ENV PATH="${TARGET_DIR}/bin:${PATH}" + +# Python deps COPY bootloader/tools/requirements.txt . -RUN pip3 install -r requirements.txt +RUN python -m pip install --no-cache-dir -r requirements.txt WORKDIR /app - CMD ["/usr/bin/env", "bash", "./build_firmware.sh"] diff --git a/Makefile b/Makefile index 18a6a8f2..eb147e48 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ mpy-cross: $(TARGET_DIR) $(MPY_DIR)/mpy-cross/Makefile make -C $(MPY_DIR)/mpy-cross \ DEBUG=$(DEBUG) \ CFLAGS_EXTRA="$(MPY_CFLAGS)" && \ - cp $(MPY_DIR)/mpy-cross/mpy-cross $(TARGET_DIR) + cp $(MPY_DIR)/mpy-cross/build/mpy-cross $(TARGET_DIR) # embed git metadata for firmware builds .PHONY: git-info @@ -39,13 +39,18 @@ git-info: disco: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/stm32 git-info @echo Building firmware make -C $(MPY_DIR)/ports/stm32 \ - BOARD=$(BOARD) \ - FLAVOR=$(FLAVOR) \ - USE_DBOOT=$(USE_DBOOT) \ - USER_C_MODULES=$(USER_C_MODULES) \ - FROZEN_MANIFEST=$(FROZEN_MANIFEST_DISCO) \ - DEBUG=$(DEBUG) \ - CFLAGS_EXTRA="$(MPY_CFLAGS)" && \ + BOARD=$(BOARD) \ + FLAVOR=$(FLAVOR) \ + USE_DBOOT=$(USE_DBOOT) \ + USER_C_MODULES=$(USER_C_MODULES) \ + FROZEN_MANIFEST=$(FROZEN_MANIFEST_DISCO) \ + CFLAGS_EXTRA="-DMP_CONFIGFILE=\"\" $(MPY_CFLAGS)" + DEBUG=$(DEBUG) && \ + arm-none-eabi-objcopy -O binary \ + $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \ + $(TARGET_DIR)/specter-diy.bin && \ + cp $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.hex \ + $(TARGET_DIR)/specter-diy.hex arm-none-eabi-objcopy -O binary \ $(MPY_DIR)/ports/stm32/build-STM32F469DISC/firmware.elf \ $(TARGET_DIR)/specter-diy.bin && \ @@ -76,8 +81,8 @@ unix: $(TARGET_DIR) mpy-cross $(MPY_DIR)/ports/unix git-info make -C $(MPY_DIR)/ports/unix \ USER_C_MODULES=$(USER_C_MODULES) \ FROZEN_MANIFEST=$(FROZEN_MANIFEST_UNIX) \ - CFLAGS_EXTRA="$(MPY_CFLAGS)" && \ - cp $(MPY_DIR)/ports/unix/micropython $(TARGET_DIR)/micropython_unix + CFLAGS_EXTRA="-DMP_CONFIGFILE=\"\" $(MPY_CFLAGS)" && \ + cp $(MPY_DIR)/ports/unix/build-standard/micropython $(TARGET_DIR)/micropython_unix simulate: unix $(TARGET_DIR)/micropython_unix simulate.py @@ -90,6 +95,7 @@ all: mpy-cross disco unix clean: rm -rf $(TARGET_DIR) make -C $(MPY_DIR)/mpy-cross clean + rm -rf $(MPY_DIR)/mpy-cross/build make -C $(MPY_DIR)/ports/unix \ USER_C_MODULES=$(USER_C_MODULES) \ FROZEN_MANIFEST=$(FROZEN_MANIFEST_UNIX) clean diff --git a/boot/debug/hardwaretest.py b/boot/debug/hardwaretest.py index 6a80af2d..003801c8 100644 --- a/boot/debug/hardwaretest.py +++ b/boot/debug/hardwaretest.py @@ -52,7 +52,11 @@ async def main(self): await self.qr.enable() s = await self.qr.get_data() if s: - data = s.read().decode() + raw = s.read() + try: + data = raw.decode() + except: + data = repr(raw) await self.gui.alert("Here's what we scanned:", data) else: conn = get_connection() diff --git a/docs/build.md b/docs/build.md index 7e71241b..fef821c6 100644 --- a/docs/build.md +++ b/docs/build.md @@ -61,45 +61,87 @@ You'll need to have [Nix](https://nixos.org/) installed as well. To compile the firmware for the board you will need `arm-none-eabi-gcc` compiler. -**Debian/Ubuntu**: +#### Debian/Ubuntu + ```sh sudo apt-get install build-essential gcc-arm-none-eabi binutils-arm-none-eabi gdb-multiarch openocd ``` -**Archlinux**: +#### Archlinux + ```sh sudo pacman -S arm-none-eabi-gcc arm-none-eabi-binutils openocd base-devel python-case ``` You might need change default gcc flag settings with `CFLAGS_EXTRA="-w"`. Export it or set the variable before of `make` to avoid warnings being raised as errors. -**MacOS**: +#### MacOS + ```sh -brew tap ArmMbed/homebrew-formulae brew install arm-none-eabi-gcc ``` -On **Windows**: Install linux subsystem and follow Linux instructions. +#### Windows + +Install the Linux subsystem and follow Linux instructions. The recommended distribution as of this writing is **Ubuntu 22.04 LTS**. It can be installed using the following command in the **administrator's PowerShell**: + +```sh +# Install Ubuntu 22.04 as WSL Linux environment +wsl --install -d Ubuntu-22.04 +# Start new instance of Ubuntu 22.04 (if multiple distros are present in system) +wsl -d Ubuntu-22.04 +``` + +To install all the dependencies on Windows, make sure the **universe** repository is enabled and the package list is up to date. If not, here is a quick fix: + +```sh +# Uncomment all lines in /etc/apt/sources.list that begin with # deb +sudo sed -i 's/^# deb/deb/' /etc/apt/sources.list +# Enable universe repository +sudo add-apt-repository universe +# Update package list +sudo apt update +``` ### Prerequisities (Manually): Simulator You may need to install SDL2 library to simulate the screen of the device. -**Linux**: +#### Linux + ```sh sudo apt install libsdl2-dev ``` -**MacOS**: +#### MacOS + ```sh brew install sdl2 ``` -**Windows**: +To make the SDL2 library available to your C/C++ toolchain, ensure that Homebrew’s include and library paths are added to the compiler and linker flags. If they are not already set, you can add the following lines to your shell profile (commonly `~/.zprofile` or `~/.bash_profile`) or configure them in another way before making the Unix build: + +```sh +export LDFLAGS="-L/opt/homebrew/lib $LDFLAGS" +export CPPFLAGS="-idirafter /opt/homebrew/include $CPPFLAGS" +export CFLAGS="-idirafter /opt/homebrew/include $CFLAGS" +``` + +#### Windows + - `sudo apt install libsdl2-dev` on Linux side. - install and launch [Xming](https://sourceforge.net/projects/xming/) on Windows side - set `export DISPLAY=:0` on linux part +[VcXsrv](https://github.com/marchaesen/vcxsrv) is another viable option for running X11 applications on Windows. Here is an example environment that should be configured on the WSL side: + +```sh +export LIBGL_ALWAYS_INDIRECT=0 +export LIBGL_ALWAYS_SOFTWARE=true +export DISPLAY=127.0.0.1:0 +export XDG_RUNTIME_DIR=/mnt/wslg/runtime-dir +``` + ## Build All build commands might need a prefix like `nix develop -c` or need to be run from `nix develop` shell. If you use direnv, you don't need to do anything, apart from an initial `direnv allow`. diff --git a/docs/reproducible-build.md b/docs/reproducible-build.md index facc29ec..0028bcd9 100644 --- a/docs/reproducible-build.md +++ b/docs/reproducible-build.md @@ -45,3 +45,18 @@ For Apple M1 add a platform flag to the docker commands: docker build -t diy . --platform linux/x86_64 docker run --platform linux/amd64 -ti -v `pwd`:/app -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) diy ``` + +# Simplified build without signing + +If you just need a HEX file without bootloader and signatures you can use this command: + +```sh +docker run -ti -v `pwd`:/app diy bash -c "make clean && make disco" +``` + +This will generate the binaries of the main firmware, which can be flashed into the Discovery board via ST-LINK or ROM bootloader. + +```txt +bin/specter-diy.bin +bin/specter-diy.hex +``` diff --git a/f469-disco b/f469-disco index db3ce3e9..2ccc2af1 160000 --- a/f469-disco +++ b/f469-disco @@ -1 +1 @@ -Subproject commit db3ce3e918cf0fd36f076ecd86ef05240d7c3cef +Subproject commit 2ccc2af1f8f0f015a064967a4628309f8c880d04 diff --git a/src/gui/async_gui.py b/src/gui/async_gui.py index 5d71d010..6a4cd282 100644 --- a/src/gui/async_gui.py +++ b/src/gui/async_gui.py @@ -46,10 +46,10 @@ def hide_loader(self): async def load_screen(self, scr): while self.background is not None: await asyncio.sleep_ms(10) - old_scr = lv.scr_act() - lv.scr_load(scr) + old_scr = lv.screen_active() + lv.screen_load(scr) self.scr = scr - old_scr.del_async() + old_scr.delete_async() async def open_popup(self, scr): # wait for another popup to finish @@ -57,7 +57,7 @@ async def open_popup(self, scr): await asyncio.sleep_ms(10) self.background = self.scr self.scr = scr - lv.scr_load(scr) + lv.screen_load(scr) async def close_popup(self): scr = self.background diff --git a/src/gui/common.py b/src/gui/common.py index 753d32b6..6b7a990f 100644 --- a/src/gui/common.py +++ b/src/gui/common.py @@ -1,6 +1,6 @@ """Some commonly used functions, like helpers""" import lvgl as lv -import qrcode +#import qrcode import math from micropython import const import gc @@ -14,125 +14,129 @@ def init_styles(dark=True): + # LVGL 9.x theme and style initialization + disp = lv.display_get_default() + if dark: - # Set theme - th = lv.theme_night_init(210, lv.font_roboto_22) - # adjusting theme - # background color cbg = lv.color_hex(0x192432) - # ctxt = lv.color_hex(0x7f8fa4) ctxt = lv.color_hex(0xFFFFFF) cbtnrel = lv.color_hex(0x506072) cbtnpr = lv.color_hex(0x405062) chl = lv.color_hex(0x313E50) + cprimary = lv.palette_main(lv.PALETTE.BLUE) + csecondary = lv.palette_main(lv.PALETTE.GREY) else: - # Set theme to light - # TODO: work in progress... - th = lv.theme_material_init(210, lv.font_roboto_22) - # adjusting theme - # background color cbg = lv.color_hex(0xEEEEEE) - # ctxt = lv.color_hex(0x7f8fa4) - ctxt = lv.color_hex(0) + ctxt = lv.color_hex(0x000000) cbtnrel = lv.color_hex(0x506072) cbtnpr = lv.color_hex(0x405062) chl = lv.color_hex(0x313E50) - th.style.label.sec.text.color = cbtnrel - th.style.scr.body.main_color = cbg - th.style.scr.body.grad_color = cbg - # text color - th.style.scr.text.color = ctxt - # buttons - # btn released - th.style.btn.rel.body.main_color = cbtnrel - th.style.btn.rel.body.grad_color = cbtnrel - th.style.btn.rel.body.shadow.width = 0 - th.style.btn.rel.body.border.width = 0 - th.style.btn.rel.body.radius = 10 - # btn pressed - lv.style_copy(th.style.btn.pr, th.style.btn.rel) - th.style.btn.pr.body.main_color = cbtnpr - th.style.btn.pr.body.grad_color = cbtnpr - # button map released - th.style.btnm.btn.rel.body.main_color = cbg - th.style.btnm.btn.rel.body.grad_color = cbg - th.style.btnm.btn.rel.body.radius = 0 - th.style.btnm.btn.rel.body.border.width = 0 - th.style.btnm.btn.rel.body.shadow.width = 0 - th.style.btnm.btn.rel.text.color = ctxt - # button map pressed - lv.style_copy(th.style.btnm.btn.pr, th.style.btnm.btn.rel) - th.style.btnm.btn.pr.body.main_color = chl - th.style.btnm.btn.pr.body.grad_color = chl - # button map toggled - lv.style_copy(th.style.btnm.btn.tgl_pr, th.style.btnm.btn.pr) - lv.style_copy(th.style.btnm.btn.tgl_rel, th.style.btnm.btn.pr) - # button map inactive - lv.style_copy(th.style.btnm.btn.ina, th.style.btnm.btn.rel) - th.style.btnm.btn.ina.text.opa = 80 - # button map background - th.style.btnm.bg.body.opa = 0 - th.style.btnm.bg.body.border.width = 0 - th.style.btnm.bg.body.shadow.width = 0 - # textarea - th.style.ta.oneline.body.opa = 0 - th.style.ta.oneline.body.border.width = 0 - th.style.ta.oneline.text.font = lv.font_roboto_28 - th.style.ta.oneline.text.color = ctxt - # slider - th.style.slider.knob.body.main_color = cbtnrel - th.style.slider.knob.body.grad_color = cbtnrel - th.style.slider.knob.body.radius = 5 - th.style.slider.knob.body.border.width = 0 - # page - th.style.page.bg.body.opa = 0 - th.style.page.scrl.body.opa = 0 - th.style.page.bg.body.border.width = 0 - th.style.page.bg.body.padding.left = 0 - th.style.page.bg.body.padding.right = 0 - th.style.page.bg.body.padding.top = 0 - th.style.page.bg.body.padding.bottom = 0 - th.style.page.scrl.body.border.width = 0 - th.style.page.scrl.body.padding.left = 0 - th.style.page.scrl.body.padding.right = 0 - th.style.page.scrl.body.padding.top = 0 - th.style.page.scrl.body.padding.bottom = 0 - - lv.theme_set_current(th) + cprimary = lv.palette_main(lv.PALETTE.BLUE) + csecondary = lv.palette_main(lv.PALETTE.GREY) + + # Initialize default theme + th = lv.theme_default_init(disp, cprimary, csecondary, dark, lv.font_montserrat_22) + disp.set_theme(th) + + # Store colors for later use + styles["cbg"] = cbg + styles["ctxt"] = ctxt + styles["cbtnrel"] = cbtnrel + styles["cbtnpr"] = cbtnpr + styles["chl"] = chl + + # Screen style + styles["scr"] = lv.style_t() + styles["scr"].init() + styles["scr"].set_bg_color(cbg) + styles["scr"].set_text_color(ctxt) + + # Button style + styles["btn"] = lv.style_t() + styles["btn"].init() + styles["btn"].set_bg_color(cbtnrel) + styles["btn"].set_shadow_width(0) + styles["btn"].set_border_width(0) + styles["btn"].set_radius(10) + + # Button pressed style + styles["btn_pressed"] = lv.style_t() + styles["btn_pressed"].init() + styles["btn_pressed"].set_bg_color(cbtnpr) + + # Button matrix styles + styles["btnm"] = lv.style_t() + styles["btnm"].init() + styles["btnm"].set_bg_color(cbg) + styles["btnm"].set_radius(0) + styles["btnm"].set_border_width(0) + styles["btnm"].set_shadow_width(0) + styles["btnm"].set_text_color(ctxt) + + styles["btnm_pressed"] = lv.style_t() + styles["btnm_pressed"].init() + styles["btnm_pressed"].set_bg_color(chl) + + styles["btnm_bg"] = lv.style_t() + styles["btnm_bg"].init() + styles["btnm_bg"].set_bg_opa(0) + styles["btnm_bg"].set_border_width(0) + styles["btnm_bg"].set_shadow_width(0) + + # Textarea style + styles["ta"] = lv.style_t() + styles["ta"].init() + styles["ta"].set_bg_opa(0) + styles["ta"].set_border_width(0) + styles["ta"].set_text_font(lv.font_montserrat_28) + styles["ta"].set_text_color(ctxt) + + # Slider knob style + styles["slider_knob"] = lv.style_t() + styles["slider_knob"].init() + styles["slider_knob"].set_bg_color(cbtnrel) + styles["slider_knob"].set_radius(5) + styles["slider_knob"].set_border_width(0) styles["theme"] = th - # Title style - just a default style with larger font + + # Title style styles["title"] = lv.style_t() - lv.style_copy(styles["title"], th.style.label.prim) - styles["title"].text.font = lv.font_roboto_28 - styles["title"].text.color = ctxt + styles["title"].init() + styles["title"].set_text_font(lv.font_montserrat_28) + styles["title"].set_text_color(ctxt) + # Hint style styles["hint"] = lv.style_t() - lv.style_copy(styles["hint"], th.style.label.sec) - styles["hint"].text.font = lv.font_roboto_16 + styles["hint"].init() + styles["hint"].set_text_font(lv.font_montserrat_16) + styles["hint"].set_text_color(csecondary) + # Small style styles["small"] = lv.style_t() - lv.style_copy(styles["small"], styles["hint"]) - styles["small"].text.color = ctxt + styles["small"].init() + styles["small"].set_text_font(lv.font_montserrat_16) + styles["small"].set_text_color(ctxt) + # Warning style styles["warning"] = lv.style_t() - lv.style_copy(styles["warning"], th.style.label.prim) - styles["warning"].text.color = lv.color_hex(0xFF9A00) + styles["warning"].init() + styles["warning"].set_text_color(lv.color_hex(0xFF9A00)) def add_label(text, y=PADDING, scr=None, style=None, width=None): """Helper functions that creates a title-styled label""" if width is None: width = HOR_RES - 2 * PADDING if scr is None: - scr = lv.scr_act() + scr = lv.screen_active() lbl = lv.label(scr) lbl.set_text(text) if style in styles: - lbl.set_style(0, styles[style]) - lbl.set_long_mode(lv.label.LONG.BREAK) + lbl.add_style(styles[style], 0) + lbl.set_long_mode(lv.label.LONG_MODE.WRAP) lbl.set_width(width) lbl.set_x((HOR_RES - width) // 2) - lbl.set_align(lv.label.ALIGN.CENTER) + lbl.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) lbl.set_y(y) return lbl @@ -140,21 +144,21 @@ def add_label(text, y=PADDING, scr=None, style=None, width=None): def add_button(text=None, callback=None, scr=None, y=700): """Helper function that creates a button with a text label""" if scr is None: - scr = lv.scr_act() - btn = lv.btn(scr) + scr = lv.screen_active() + btn = lv.button(scr) btn.set_width(HOR_RES - 2 * PADDING) btn.set_height(BTN_HEIGHT) if text is not None: lbl = lv.label(btn) lbl.set_text(text) - lbl.set_align(lv.label.ALIGN.CENTER) + lbl.center() - btn.align(scr, lv.ALIGN.IN_TOP_MID, 0, 0) + btn.align(lv.ALIGN.TOP_MID, 0, 0) btn.set_y(y) if callback is not None: - btn.set_event_cb(callback) + btn.add_event_cb(callback, lv.EVENT.CLICKED, None) return btn @@ -172,14 +176,17 @@ def align_button_pair(btn1, btn2): w = (HOR_RES - 3 * PADDING) // 2 btn1.set_width(w) btn2.set_width(w) + # Clear alignment and set explicit x positions + btn1.set_align(lv.ALIGN.DEFAULT) + btn2.set_align(lv.ALIGN.DEFAULT) btn1.set_x(PADDING) - btn2.set_x(HOR_RES // 2 + PADDING // 2) + btn2.set_x(PADDING + w + PADDING) def add_qrcode(text, y=QR_PADDING, scr=None, style=None, width=None): """Helper functions that creates a title-styled label""" if scr is None: - scr = lv.scr_act() + scr = lv.screen_active() if width is None: width = 350 @@ -188,7 +195,7 @@ def add_qrcode(text, y=QR_PADDING, scr=None, style=None, width=None): qr.set_text(text) qr.set_size(width) qr.set_text(text) - qr.align(scr, lv.ALIGN.IN_TOP_MID, 0, y) + qr.align_to(scr, lv.ALIGN.TOP_MID, 0, y) return qr diff --git a/src/gui/components/battery.py b/src/gui/components/battery.py index 29ca66e8..c16a367d 100644 --- a/src/gui/components/battery.py +++ b/src/gui/components/battery.py @@ -14,12 +14,15 @@ class Battery(lv.obj): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.set_style(lv.style_transp_tight) + # Make background transparent in LVGL 9.x + self.set_style_bg_opa(0, 0) + self.set_style_border_width(0, 0) + self.set_style_pad_all(0, 0) self.level = lv.label(self) self.level.set_recolor(True) self.icon = lv.label(self) self.charge = lv.label(self) - self.set_size(30,20) + self.set_size(30, 20) # self.bar = lv.bar(self) self.update() @@ -39,6 +42,6 @@ def update(self): self.icon.set_text(lv.SYMBOL.BATTERY_EMPTY) if self.CHARGING: self.charge.set_text(lv.SYMBOL.CHARGE) - self.charge.align(self.icon, lv.ALIGN.CENTER, 0, 0) + self.charge.align_to(self.icon, lv.ALIGN.CENTER, 0, 0) else: self.charge.set_text("") diff --git a/src/gui/components/keyboard.py b/src/gui/components/keyboard.py index fc6a0ac9..d8e1b725 100644 --- a/src/gui/components/keyboard.py +++ b/src/gui/components/keyboard.py @@ -4,18 +4,18 @@ from .theme import styles -class HintKeyboard(lv.btnm): +class HintKeyboard(lv.buttonmatrix): def __init__(self, scr, *args, **kwargs): super().__init__(scr, *args, **kwargs) - self.hint = lv.btn(scr) + self.hint = lv.button(scr) self.hint.set_size(50, 60) self.hint_lbl = lv.label(self.hint) self.hint_lbl.set_text(" ") - self.hint_lbl.set_style(0, styles["title"]) + self.hint_lbl.add_style(styles["title"], 0) self.hint_lbl.set_size(50, 60) - self.hint.set_hidden(True) + self.hint.add_flag(lv.obj.FLAG.HIDDEN) self.callback = None - super().set_event_cb(self.cb) + super().add_event_cb(self.cb, lv.EVENT.ALL, None) def set_event_cb(self, callback): self.callback = callback @@ -23,20 +23,21 @@ def set_event_cb(self, callback): def get_event_cb(self): return self.callback - def cb(self, obj, event): - if event == lv.EVENT.PRESSING: + def cb(self, event): + code = event.get_code() + obj = event.get_target() + if code == lv.EVENT.PRESSING: feed_touch() - c = obj.get_active_btn_text() + c = obj.get_selected_button_text() if c is not None and len(c) <= 2: - self.hint.set_hidden(False) + self.hint.clear_flag(lv.obj.FLAG.HIDDEN) self.hint_lbl.set_text(c) - point = lv.point_t() - indev = lv.indev_get_act() - lv.indev_get_point(indev, point) + indev = lv.indev_active() + point = indev.get_point() self.hint.set_pos(point.x - 25, point.y - 130) - elif event == lv.EVENT.RELEASED: - self.hint.set_hidden(True) + elif code == lv.EVENT.RELEASED: + self.hint.add_flag(lv.obj.FLAG.HIDDEN) if self.callback is not None: - self.callback(obj, event) + self.callback(obj, code) diff --git a/src/gui/components/modal.py b/src/gui/components/modal.py index 71193ddf..43d2b7ee 100644 --- a/src/gui/components/modal.py +++ b/src/gui/components/modal.py @@ -19,7 +19,7 @@ def __init__(self, parent, *args, **kwargs): self.mbox = lv.mbox(self) self.mbox.set_width(400) - self.mbox.align(None, lv.ALIGN.IN_TOP_MID, 0, 200) + self.mbox.align(lv.ALIGN.TOP_MID, 0, 200) def set_text(self, text): self.mbox.set_text(text) diff --git a/src/gui/components/qrcode.py b/src/gui/components/qrcode.py index 504a769d..ef6bd06a 100644 --- a/src/gui/components/qrcode.py +++ b/src/gui/components/qrcode.py @@ -1,6 +1,5 @@ import lvgl as lv import lvqr -import qrcode import math import gc import asyncio @@ -10,14 +9,22 @@ from qrencoder import QREncoder qr_style = lv.style_t() -qr_style.body.main_color = lv.color_hex(0xFFFFFF) -qr_style.body.grad_color = lv.color_hex(0xFFFFFF) -qr_style.body.opa = 255 -qr_style.text.opa = 255 -qr_style.text.color = lv.color_hex(0) -qr_style.text.line_space = 0 -qr_style.text.letter_space = 0 -qr_style.body.radius = 10 +qr_style.init() +qr_style.set_bg_color(lv.color_hex(0xFFFFFF)) +qr_style.set_bg_grad_color(lv.color_hex(0xFFFFFF)) +qr_style.set_bg_opa(255) +qr_style.set_text_opa(255) +qr_style.set_text_color(lv.color_hex(0)) +qr_style.set_text_line_space(0) +qr_style.set_text_letter_space(0) +qr_style.set_radius(10) + +# Transparent style (replacement for lv.style_transp_tight) +style_transp = lv.style_t() +style_transp.init() +style_transp.set_bg_opa(0) +style_transp.set_border_width(0) +style_transp.set_pad_all(0) QR_SIZES = [17, 32, 53, 78, 106, 154, 192, 230, 271, 367, 458, 586, 718, 858] BTNSIZE = 70 @@ -32,9 +39,11 @@ class QRCode(lv.obj): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) style = lv.style_t() - lv.style_copy(style, qr_style) - style.text.font = lv.font_roboto_16 - style.text.color = lv.color_hex(0x192432) + style.init() + style.set_bg_color(lv.color_hex(0xFFFFFF)) + style.set_bg_opa(255) + style.set_text_font(lv.font_roboto_16) + style.set_text_color(lv.color_hex(0x192432)) self.encoder = None self._autoplay = True @@ -50,9 +59,9 @@ def __init__(self, *args, **kwargs): self.create_playback_controls(style) self.note = lv.label(self) - self.note.set_style(0, style) + self.note.add_style(style, 0) self.note.set_text("") - self.note.set_align(lv.label.ALIGN.CENTER) + self.note.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) self.set_text(self._text) self.task = asyncio.create_task(self.animate()) @@ -66,14 +75,15 @@ def spacing(self): @spacing.setter def spacing(self, spacing): - qr_style = lv.style_t() - qr_style.body.border.width = spacing - self.qr.set_style(0, qr_style) + sp_style = lv.style_t() + sp_style.init() + sp_style.set_border_width(spacing) + self.qr.add_style(sp_style, 0) self._spacing = spacing def create_playback_controls(self, style): self.playback = lv.obj(self) - self.playback.set_style(lv.style_transp_tight) + self.playback.add_style(style_transp, 0) self.playback.set_size(480, BTNSIZE) self.playback.set_y(640) @@ -117,7 +127,7 @@ def create_playback_controls(self, style): def create_density_controls(self, style): self.controls = lv.obj(self) - self.controls.set_style(lv.style_transp_tight) + self.controls.add_style(style_transp, 0) self.controls.set_size(480, BTNSIZE) self.controls.set_y(740) plus = lv.btn(self.controls) @@ -136,8 +146,8 @@ def create_density_controls(self, style): lbl = lv.label(self.controls) lbl.set_text("QR code density") - lbl.set_style(0, style) - lbl.set_align(lv.label.ALIGN.CENTER) + lbl.add_style(style, 0) + lbl.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) lbl.align(self.controls, lv.ALIGN.CENTER, 0, 0) self.controls.set_hidden(True) @@ -290,7 +300,7 @@ def check_controls(self): def _set_text(self, text): # one bcur frame doesn't require checksum print(text) - self.set_style(qr_style) + self.add_style(qr_style, 0) self.qr.set_text(text) self.qr.align(self, lv.ALIGN.CENTER, 0, -100 if self.is_fullscreen else 0) self.note.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, 0) diff --git a/src/gui/core.py b/src/gui/core.py index bba3fea3..4f68a21f 100644 --- a/src/gui/core.py +++ b/src/gui/core.py @@ -7,13 +7,14 @@ def init(blocking=True, dark=True): - # display.init(not blocking) + # Ensure display is initialized first (for LVGL 9.x) + display.init(not blocking) # Initialize the styles init_styles(dark=dark) scr = lv.obj() - lv.scr_load(scr) + lv.screen_load(scr) update() diff --git a/src/gui/decorators.py b/src/gui/decorators.py index 6cf54576..d441bdaa 100644 --- a/src/gui/decorators.py +++ b/src/gui/decorators.py @@ -9,33 +9,37 @@ def feed_touch(): and feeds it to random number pool """ point = lv.point_t() - indev = lv.indev_get_act() - lv.indev_get_point(indev, point) - # now we can take bytes([point.x % 256, point.y % 256]) - # and feed it into hash digest - t = time.ticks_cpu() - random_data = t.to_bytes(4, "big") + bytes([point.x % 256, point.y % 256]) - rng.feed(random_data) + indev = lv.indev_active() + if indev: + indev.get_point(point) + # now we can take bytes([point.x % 256, point.y % 256]) + # and feed it into hash digest + t = time.ticks_cpu() + random_data = t.to_bytes(4, "big") + bytes([point.x % 256, point.y % 256]) + rng.feed(random_data) def feed_rng(func): """Any callback will contribute to random number pool""" - - def wrapper(o, e): - if e == lv.EVENT.PRESSING: + # LVGL 9.x: callback receives event object + def wrapper(event): + code = event.get_code() + if code == lv.EVENT.PRESSING: feed_touch() - func(o, e) + func(event) return wrapper def on_release(func): """Handy decorator if you only care about click event""" - - def wrapper(o, e): - if e == lv.EVENT.PRESSING: + # LVGL 9.x: callback receives event object + # CLICKED = complete press+release cycle + def wrapper(event): + code = event.get_code() + if code == lv.EVENT.PRESSING: feed_touch() - elif e == lv.EVENT.RELEASED and func is not None: + elif code == lv.EVENT.CLICKED and func is not None: func() return wrapper diff --git a/src/gui/screens/alert.py b/src/gui/screens/alert.py index fd9f0fda..6cea561a 100644 --- a/src/gui/screens/alert.py +++ b/src/gui/screens/alert.py @@ -13,12 +13,13 @@ def __init__( obj = self.title if note is not None: self.note = add_label(note, scr=self, style="hint") - self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.note.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) obj = self.note - self.page = lv.page(self) + # LVGL 9.x: page replaced with scrollable obj + self.page = lv.obj(self) self.page.set_size(480, 600) self.message = add_label(message, scr=self.page) - self.page.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 0) + self.page.align_to(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 0) if button_text is not None: self.close_button = add_button(scr=self, callback=on_release(self.release)) diff --git a/src/gui/screens/input.py b/src/gui/screens/input.py index f28e5c34..a53975a7 100644 --- a/src/gui/screens/input.py +++ b/src/gui/screens/input.py @@ -123,13 +123,13 @@ def __init__( if note is not None: self.note = add_label(note, scr=self, style="hint") - self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.note.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) self.kb = HintKeyboard(self) self.kb.set_map(self.CHARSET) self.kb.set_width(HOR_RES) self.kb.set_height(int(VER_RES / 2.5)) - self.kb.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, 0) + self.kb.align(lv.ALIGN.BOTTOM_MID, 0, 0) self.ta = lv.ta(self) self.ta.set_text(suggestion) @@ -145,7 +145,7 @@ def __init__( self.kb.set_event_cb(self.cb) self.warning = add_label("", scr=self, style="hint") - self.warning.align(self.ta, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + self.warning.align_to(self.ta, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) def cb(self, obj, event): if event == lv.EVENT.RELEASED: @@ -215,14 +215,14 @@ def __init__(self, title="Enter your PIN code", note=None, get_word=None, subtit if subtitle is not None: lbl = add_label(subtitle, scr=self, style="hint") lbl.set_recolor(True) - lbl.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + lbl.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) if note is not None: lbl = add_label(note, scr=self, style="hint") - lbl.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 90) + lbl.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 90) self.get_word = get_word if get_word is not None: self.words = add_label(get_word(b""), scr=self) - self.words.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 120) + self.words.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 120) btnm = lv.btnm(self) # shuffle numbers to make sure # no constant fingerprints left on screen @@ -237,7 +237,7 @@ def __init__(self, title="Enter your PIN code", note=None, get_word=None, subtit btnm.set_map(btnmap) btnm.set_width(HOR_RES) btnm.set_height(HOR_RES) - btnm.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, -100) + btnm.align(lv.ALIGN.BOTTOM_MID, 0, -100) # increase font size style = lv.style_t() lv.style_copy(style, btnm.get_style(lv.btnm.STYLE.BTN_REL)) @@ -262,7 +262,7 @@ def __init__(self, title="Enter your PIN code", note=None, get_word=None, subtit self.pin.set_one_line(True) self.pin.set_text_align(lv.label.ALIGN.CENTER) self.pin.set_pwd_show_time(0) - self.pin.align(btnm, lv.ALIGN.OUT_TOP_MID, 0, -80) + self.pin.align_to(btnm, lv.ALIGN.OUT_TOP_MID, 0, -80) self.next_button = add_button(scr=self, callback=on_release(self.submit)) @@ -343,7 +343,7 @@ def __init__(self, title="Enter derivation path"): self.kb.set_map(self.PATH_CHARSET) self.kb.set_width(HOR_RES) self.kb.set_height(VER_RES // 2) - self.kb.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, 0) + self.kb.align(lv.ALIGN.BOTTOM_MID, 0, 0) lbl = add_label("m/", style="title", scr=self) lbl.set_y(PADDING + 150) @@ -423,13 +423,13 @@ def __init__( self.title = add_label(title, scr=self, y=PADDING, style="title") self.note = add_label(note, scr=self, style="hint") - self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.note.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) self.kb = lv.btnm(self) self.kb.set_map(self.NUMERIC_CHARSET) self.kb.set_width(HOR_RES) self.kb.set_height(VER_RES // 2) - self.kb.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, 0) + self.kb.align(lv.ALIGN.BOTTOM_MID, 0, 0) lbl = add_label('', style="title", scr=self) lbl.set_y(PADDING + 150) diff --git a/src/gui/screens/menu.py b/src/gui/screens/menu.py index e1e64363..8d9a89f2 100644 --- a/src/gui/screens/menu.py +++ b/src/gui/screens/menu.py @@ -13,9 +13,10 @@ def __init__( self.title = add_label(title, style="title", scr=self) if note is not None: self.note = add_label(note, style="hint", scr=self) - self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.note.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) y += self.note.get_height() - self.page = lv.page(self) + # LVGL 9.x: page replaced with scrollable obj + self.page = lv.obj(self) h = 800 - y - 20 self.page.set_size(480, h) self.page.set_y(y) @@ -32,15 +33,14 @@ def __init__( cb = None btn = add_button(text, cb, y=y, scr=self.page) if not enable: - btn.set_state(lv.btn.STATE.INA) - # color + btn.add_state(lv.STATE.DISABLED) + # color (LVGL 9.x style) if len(args) > 1: color = args[1] style = lv.style_t() - lv.style_copy(style, btn.get_style(lv.btn.STYLE.REL)) - style.body.main_color = lv.color_hex(color) - style.body.grad_color = lv.color_hex(color) - btn.set_style(lv.btn.STYLE.REL, style) + style.init() + style.set_bg_color(lv.color_hex(color)) + btn.add_style(style, 0) self.buttons.append(btn) y += 85 diff --git a/src/gui/screens/mnemonic.py b/src/gui/screens/mnemonic.py index d47eb7e6..12b3f363 100644 --- a/src/gui/screens/mnemonic.py +++ b/src/gui/screens/mnemonic.py @@ -14,10 +14,10 @@ def __init__(self, mnemonic="", title="Your recovery phrase:", note=None): self.title = add_label(title, scr=self, style="title") if note is not None: lbl = add_label(note, scr=self, style="hint") - lbl.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + lbl.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) self.table = MnemonicTable(self) self.table.set_mnemonic(mnemonic) - self.table.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + self.table.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) self.close_button = add_button(scr=self, callback=on_release(self.release)) @@ -29,16 +29,16 @@ def __init__(self, mnemonic="", title="Your recovery phrase:", note=None): super().__init__(title, message="", note=note) table = MnemonicTable(self) table.set_mnemonic(mnemonic) - table.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 50) + table.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 50) class ExportMnemonicScreen(MnemonicScreen): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.show_qr_btn = add_button(text="Show as QR code", scr=self, callback=on_release(self.select_qr)) - self.show_qr_btn.align(self.table, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + self.show_qr_btn.align_to(self.table, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) self.save_sd_btn = add_button(text="Save to SD card (plaintext)", scr=self, callback=on_release(self.select_sd)) - self.save_sd_btn.align(self.show_qr_btn, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + self.save_sd_btn.align_to(self.show_qr_btn, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) def select_sd(self): self.set_value(self.SD) @@ -59,7 +59,7 @@ def __init__( self.wordlist = wordlist mnemonic = generator(12) super().__init__(mnemonic, title, note) - self.table.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 50) + self.table.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 50) self.table.set_event_cb(self.on_word_click) # enable callbacks self.table.set_click(True) @@ -74,13 +74,13 @@ def __init__( # toggle switch 12-24 words lbl = lv.label(self) lbl.set_text("Use 24 words") - lbl.align(self.table, lv.ALIGN.OUT_BOTTOM_MID, 0, 40) + lbl.align_to(self.table, lv.ALIGN.OUT_BOTTOM_MID, 0, 40) lbl.set_x(120) self.switch_lbl = lbl self.switch = lv.sw(self) self.switch.off(lv.ANIM.OFF) - self.switch.align(lbl, lv.ALIGN.OUT_RIGHT_MID, 20, 0) + self.switch.align_to(lbl, lv.ALIGN.OUT_RIGHT_MID, 20, 0) def cb(): wordcount = 24 if self.switch.get_state() else 12 @@ -95,11 +95,11 @@ def cb(): self.kb.set_ctrl_map([lv.btnm.CTRL.TGL_ENABLE for i in range(11)]) self.kb.set_width(HOR_RES) self.kb.set_height(100) - self.kb.align(self.table, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.kb.align_to(self.table, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) self.kb.set_hidden(True) self.instruction = add_label("Hint: click on any word above to edit it.", scr=self, style="hint") - self.instruction.align(self.kb, lv.ALIGN.OUT_BOTTOM_MID, 0, 15) + self.instruction.align_to(self.kb, lv.ALIGN.OUT_BOTTOM_MID, 0, 15) def on_word_click(self, obj, evt): @@ -173,7 +173,7 @@ def __init__( self, checker=None, lookup=None, fixer=None, title="Enter your recovery phrase" ): super().__init__("", title) - self.table.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + self.table.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) self.checker = checker self.lookup = lookup @@ -231,7 +231,7 @@ def __init__( self.kb.set_btn_ctrl(self.BTN_DONE, lv.btnm.CTRL.INACTIVE) self.kb.set_width(HOR_RES) self.kb.set_height(260) - self.kb.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, 0) + self.kb.align(lv.ALIGN.BOTTOM_MID, 0, 0) self.kb.set_event_cb(self.callback) self.fixer = fixer @@ -239,12 +239,12 @@ def __init__( self.fix_button = add_button("fix", on_release(self.fix_cb), self) self.fix_button.set_size(55, 30) # position it out of the screen but on correct y - self.fix_button.align(self.table, lv.ALIGN.OUT_BOTTOM_MID, -400, -38) + self.fix_button.align_to(self.table, lv.ALIGN.OUT_BOTTOM_MID, -400, -38) if lookup is not None: self.autocomplete.set_width(HOR_RES) self.autocomplete.set_height(50) - self.autocomplete.align(self.kb, lv.ALIGN.OUT_TOP_MID, 0, 0) + self.autocomplete.align_to(self.kb, lv.ALIGN.OUT_TOP_MID, 0, 0) words = lookup("", 4) + [""] self.autocomplete.set_map(words) self.autocomplete.set_event_cb(self.select_word) @@ -304,13 +304,13 @@ def check_buttons(self): # check if we can fix the mnemonic try: self.fixer(mnemonic) - self.fix_button.align(self.table, lv.ALIGN.OUT_BOTTOM_MID, x, y) + self.fix_button.align_to(self.table, lv.ALIGN.OUT_BOTTOM_MID, x, y) except: - self.fix_button.align( + self.fix_button.align_to( self.table, lv.ALIGN.OUT_BOTTOM_MID, -400, -38 ) else: - self.fix_button.align(self.table, lv.ALIGN.OUT_BOTTOM_MID, -400, -38) + self.fix_button.align_to(self.table, lv.ALIGN.OUT_BOTTOM_MID, -400, -38) def callback(self, obj, event): if event != lv.EVENT.RELEASED: @@ -383,4 +383,4 @@ def event_handler(obj, event): mbox.add_btns(btns) mbox.set_width(400) mbox.set_event_cb(event_handler) - mbox.align(None, lv.ALIGN.CENTER, 0, 0) + mbox.align(lv.ALIGN.CENTER, 0, 0) diff --git a/src/gui/screens/progress.py b/src/gui/screens/progress.py index c86800aa..3c369020 100644 --- a/src/gui/screens/progress.py +++ b/src/gui/screens/progress.py @@ -16,10 +16,10 @@ def __init__(self, title, message, button_text="Cancel"): self.start = 0 self.end = 30 self.arc.set_angles(self.start, self.end) - self.arc.align(self, lv.ALIGN.CENTER, 0, -150) - self.message.align(self.arc, lv.ALIGN.OUT_BOTTOM_MID, 0, 120) + self.arc.align(lv.ALIGN.CENTER, 0, -150) + self.message.align_to(self.arc, lv.ALIGN.OUT_BOTTOM_MID, 0, 120) self.progress = add_label("", scr=self, style="title") - self.progress.align(self.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + self.progress.align_to(self.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) self.progress.set_recolor(True) def tick(self, d: int = 10): diff --git a/src/gui/screens/prompt.py b/src/gui/screens/prompt.py index 206bc83a..f9fd70b1 100644 --- a/src/gui/screens/prompt.py +++ b/src/gui/screens/prompt.py @@ -11,12 +11,13 @@ def __init__(self, title="Are you sure?", message="Make a choice", self.title = add_label(title, scr=self, style="title") if note is not None: self.note = add_label(note, scr=self, style="hint") - self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.note.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) obj = self.note - self.page = lv.page(self) + # LVGL 9.x: page replaced with scrollable obj + self.page = lv.obj(self) self.page.set_size(480, 600) self.message = add_label(message, scr=self.page) - self.page.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 0) + self.page.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 0) # Initialize an empty icon label. It will display nothing until a symbol is set. self.icon = lv.label(self) self.icon.set_text("") @@ -31,14 +32,14 @@ def __init__(self, title="Are you sure?", message="Make a choice", if warning: self.warning = add_label(warning, scr=self, style="warning") - # Display warning symbol in the icon label + # Display warning symbol in the icon label self.icon.set_text(lv.SYMBOL.WARNING) - + # Align warning text y_pos = self.cancel_button.get_y() - 60 # above the buttons x_pos = self.get_width() // 2 - self.warning.get_width() // 2 # in the center of the prompt self.warning.set_pos(x_pos, y_pos) - + # Align warning icon to the left of the title - self.icon.align(self.title, lv.ALIGN.IN_LEFT_MID, 90, 0) + self.icon.align_to(self.title, lv.ALIGN.LEFT_MID, 90, 0) diff --git a/src/gui/screens/qralert.py b/src/gui/screens/qralert.py index 18777665..550a4245 100644 --- a/src/gui/screens/qralert.py +++ b/src/gui/screens/qralert.py @@ -19,11 +19,11 @@ def __init__( qr_message = message super().__init__(title, message, button_text, note=note) self.qr = add_qrcode(qr_message, scr=self, width=qr_width) - self.qr.align(self.page, lv.ALIGN.IN_TOP_MID, 0, 20) - self.message.align(self.qr, lv.ALIGN.OUT_BOTTOM_MID, 0, 20) + self.qr.align_to(self.page, lv.ALIGN.TOP_MID, 0, 20) + self.message.align_to(self.qr, lv.ALIGN.OUT_BOTTOM_MID, 0, 20) if transcribe: btn = add_button("Toggle transcribe", on_release(self.toggle_transcribe), scr=self) - btn.align(self.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 20) + btn.align_to(self.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 20) def toggle_transcribe(self): self.qr.spacing = 0 if self.qr.spacing else 3 diff --git a/src/gui/screens/screen.py b/src/gui/screens/screen.py index 20e993b9..1a46b19e 100644 --- a/src/gui/screens/screen.py +++ b/src/gui/screens/screen.py @@ -22,18 +22,17 @@ def __init__(self): self.waiting = True self._value = None self.battery = Battery(self) - self.battery.align(self, lv.ALIGN.IN_TOP_RIGHT, -20, 10) + self.battery.align(lv.ALIGN.TOP_RIGHT, -20, 10) if type(self).network in type(self).COLORS: self.topbar = lv.obj(self) s = lv.style_t() - lv.style_copy(s, styles["theme"].style.btn.rel) - s.body.main_color = type(self).COLORS[type(self).network] - s.body.grad_color = type(self).COLORS[type(self).network] - s.body.opa = 200 - s.body.radius = 0 - s.body.border.width = 0 - self.topbar.set_style(s) + s.init() + s.set_bg_color(type(self).COLORS[type(self).network]) + s.set_bg_opa(200) + s.set_radius(0) + s.set_border_width(0) + self.topbar.add_style(s, 0) self.topbar.set_size(HOR_RES, 5) self.topbar.set_pos(0, 0) @@ -69,6 +68,6 @@ def show_loader(self, def hide_loader(self): if self.mbox is None: return - self.mbox.del_async() + self.mbox.delete_async() self.mbox = None update() diff --git a/src/gui/screens/settings.py b/src/gui/screens/settings.py index fe74796c..21ef3e48 100644 --- a/src/gui/screens/settings.py +++ b/src/gui/screens/settings.py @@ -9,7 +9,7 @@ def __init__(self, controls, title="Host setttings", note=None, controls_empty_t y = 40 if note is not None: self.note = add_label(note, style="hint", scr=self) - self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.note.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) y += self.note.get_height() self.controls = controls self.switches = [] @@ -22,9 +22,9 @@ def __init__(self, controls, title="Host setttings", note=None, controls_empty_t style="hint", ) switch = lv.sw(self.page) - switch.align(hint, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + switch.align_to(hint, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) lbl = add_label(" OFF ON ", scr=self.page) - lbl.align(switch, lv.ALIGN.CENTER, 0, 0) + lbl.align_to(switch, lv.ALIGN.CENTER, 0, 0) if control.get("value", False): switch.on(lv.ANIM.OFF) self.switches.append(switch) @@ -44,7 +44,7 @@ def __init__(self, dev=False, usb=False, note=None): super().__init__("Device settings", "") if note is not None: self.note = add_label(note, style="hint", scr=self) - self.note.align(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) + self.note.align_to(self.title, lv.ALIGN.OUT_BOTTOM_MID, 0, 5) y = 70 usb_label = add_label("USB communication", y, scr=self.page) usb_hint = add_label( @@ -57,9 +57,9 @@ def __init__(self, dev=False, usb=False, note=None): style="hint", ) self.usb_switch = lv.sw(self.page) - self.usb_switch.align(usb_hint, lv.ALIGN.OUT_BOTTOM_MID, 0, 20) + self.usb_switch.align_to(usb_hint, lv.ALIGN.OUT_BOTTOM_MID, 0, 20) lbl = add_label(" OFF ON ", scr=self.page) - lbl.align(self.usb_switch, lv.ALIGN.CENTER, 0, 0) + lbl.align_to(self.usb_switch, lv.ALIGN.CENTER, 0, 0) if usb: self.usb_switch.on(lv.ANIM.OFF) @@ -86,7 +86,7 @@ def __init__(self, dev=False, usb=False, note=None): self.wipebtn = add_button( lv.SYMBOL.TRASH + " Wipe device", on_release(self.wipe), scr=self ) - self.wipebtn.align(self, lv.ALIGN.IN_BOTTOM_MID, 0, -140) + self.wipebtn.align(lv.ALIGN.BOTTOM_MID, 0, -140) style = lv.style_t() lv.style_copy(style, self.wipebtn.get_style(lv.btn.STYLE.REL)) style.body.main_color = lv.color_hex(0x951E2D) diff --git a/src/gui/screens/transaction.py b/src/gui/screens/transaction.py index 3a630c04..83364376 100644 --- a/src/gui/screens/transaction.py +++ b/src/gui/screens/transaction.py @@ -20,9 +20,9 @@ def __init__(self, title, meta): enable_inputs = enable_inputs or meta.get("issuance", False) or meta.get("reissuance", False) lbl = add_label("Show detailed information ", scr=self) - lbl.align(obj, lv.ALIGN.CENTER, 0, 0) + lbl.align_to(obj, lv.ALIGN.CENTER, 0, 0) self.details_sw = lv.sw(self) - self.details_sw.align(obj, lv.ALIGN.CENTER, 130, 0) + self.details_sw.align_to(obj, lv.ALIGN.CENTER, 130, 0) self.details_sw.set_event_cb(on_release(self.toggle_details)) if enable_inputs: self.details_sw.on(lv.ANIM.OFF) @@ -75,7 +75,7 @@ def __init__(self, title, meta): fee_txt = "%d satoshi" % (meta["fee"]) fee = add_label("Fee: " + fee_txt, scr=self.page) fee.set_style(0, style) - fee.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + fee.align_to(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) obj = fee @@ -83,22 +83,22 @@ def __init__(self, title, meta): text = "WARNING!\n" + "\n".join(meta["warnings"]) self.warning = add_label(text, scr=self.page) self.warning.set_style(0, style_warning) - self.warning.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + self.warning.align_to(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) lbl = add_label("%d INPUTS" % len(meta["inputs"]), scr=self.page2) - lbl.align(self.page2, lv.ALIGN.IN_TOP_MID, 0, 30) + lbl.align(lv.ALIGN.TOP_MID, 0, 30) obj = lbl for i, inp in enumerate(meta["inputs"]): idxlbl = lv.label(self.page2) idxlbl.set_text("%d:" % i) - idxlbl.align(lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + idxlbl.align_to(lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) idxlbl.set_x(30) lbl = lv.label(self.page2) lbl.set_long_mode(lv.label.LONG.BREAK) lbl.set_width(380) valuetxt = "???" if inp["value"] == -1 else "%.8f" % (inp["value"]/1e8) lbl.set_text("%s %s from %s" % (valuetxt, inp.get("asset", self.default_asset), inp.get("label", "Unknown wallet"))) - lbl.align(idxlbl, lv.ALIGN.IN_TOP_LEFT, 0, 0) + lbl.align_to(idxlbl, lv.ALIGN.TOP_LEFT, 0, 0) lbl.set_x(60) if inp.get("sighash", ""): @@ -106,33 +106,33 @@ def __init__(self, title, meta): shlbl.set_long_mode(lv.label.LONG.BREAK) shlbl.set_width(380) shlbl.set_text(inp.get("sighash", "")) - shlbl.align(lbl, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5) + shlbl.align_to(lbl, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5) shlbl.set_x(60) shlbl.set_style(0, style_warning) lbl = shlbl obj = lbl lbl = add_label("%d OUTPUTS" % len(meta["outputs"]), scr=self.page2) - lbl.align(self.page2, lv.ALIGN.IN_TOP_MID, 0, 0) + lbl.align(lv.ALIGN.TOP_MID, 0, 0) lbl.set_y(obj.get_y() + obj.get_height() + 30) for i, out in enumerate(meta["outputs"]): idxlbl = lv.label(self.page2) idxlbl.set_text("%d:" % i) - idxlbl.align(lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + idxlbl.align_to(lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) idxlbl.set_x(30) lbl = lv.label(self.page2) lbl.set_long_mode(lv.label.LONG.BREAK) lbl.set_width(380) valuetxt = "???" if out["value"] == -1 else "%.8f" % (out["value"]/1e8) lbl.set_text("%s %s to %s" % (valuetxt, out.get("asset", self.default_asset), out.get("label", ""))) - lbl.align(idxlbl, lv.ALIGN.IN_TOP_LEFT, 0, 0) + lbl.align_to(idxlbl, lv.ALIGN.TOP_LEFT, 0, 0) lbl.set_x(60) addrlbl = lv.label(self.page2) addrlbl.set_long_mode(lv.label.LONG.BREAK) addrlbl.set_width(380) addrlbl.set_text(format_addr(out["address"], words=4)) - addrlbl.align(lbl, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5) + addrlbl.align_to(lbl, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 5) addrlbl.set_x(60) if out.get("label", ""): addrlbl.set_style(0, style_secondary) @@ -145,14 +145,14 @@ def __init__(self, title, meta): warning.set_align(lv.label.ALIGN.LEFT) warning.set_width(380) warning.set_style(0, self.style_warning) - warning.align(addrlbl, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) + warning.align_to(addrlbl, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) warning.set_x(60) lbl = warning if meta.get("fee"): idxlbl = lv.label(self.page2) idxlbl.set_text("Fee: " + fee_txt) - idxlbl.align(lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + idxlbl.align_to(lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) idxlbl.set_x(30) self.toggle_details() @@ -171,11 +171,11 @@ def show_output(self, out, obj): lbl = add_label( "%s %s to" % (valuetxt, out.get("asset", self.default_asset)), style="title", scr=self.page ) - lbl.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) + lbl.align_to(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) obj = lbl if out.get("label", ""): lbl = add_label(out["label"], style="title", scr=self.page) - lbl.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + lbl.align_to(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) obj = lbl if out.get("label", ""): txt = format_addr(out["address"], words=4) @@ -186,12 +186,12 @@ def show_output(self, out, obj): addr.set_style(0, self.style_secondary) else: addr.set_style(0, self.style) - addr.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + addr.align_to(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) obj = addr if "warning" in out: text = "WARNING! %s" % out["warning"] warning = add_label(text, scr=self.page) warning.set_style(0, self.style_warning) - warning.align(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + warning.align_to(obj, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) obj = warning return obj \ No newline at end of file diff --git a/src/gui/specter.py b/src/gui/specter.py index 6f22a601..70d28632 100644 --- a/src/gui/specter.py +++ b/src/gui/specter.py @@ -60,6 +60,10 @@ async def coro(self, host, scr): - or host finishes processing Also updates progress screen """ + # Wait for scanning to start (or user cancel) + while not host.in_progress and scr.waiting: + await asyncio.sleep_ms(10) + # Wait for scanning to finish (or user cancel) while host.in_progress and scr.waiting: await asyncio.sleep_ms(30) scr.tick(5) diff --git a/src/hosts/qr.py b/src/hosts/qr.py index f4ab3728..2df2b7fb 100644 --- a/src/hosts/qr.py +++ b/src/hosts/qr.py @@ -826,6 +826,10 @@ async def get_data(self, raw=True, chunk_timeout=0.5): self, "Scanning...", "Point scanner to the QR code" ) stream = await self.scan(raw=raw, chunk_timeout=chunk_timeout) + # Wait for progress popup to close before returning + if self.manager is not None: + while self.manager.gui.background is not None: + await asyncio.sleep_ms(10) if stream is not None: return stream