From df7d0739abce7e3d34bebb0ee43ad040ee1c9970 Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 3 May 2026 16:46:43 +0100 Subject: [PATCH 01/32] Add .gitattributes to ignore specific files and folders --- .gitattributes | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e28543c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +.gitignore export-ignore +download.bat export-ignore +download.sh export-ignore +pyproject.toml export-ignore +README.md export-ignore +.github/ export-ignore +.vscode/ export-ignore +dev/ export-ignore +tests/ export-ignore +typings/ expot-ignore From 48bf98174357da109517641dab38858df64d7482 Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 3 May 2026 16:57:04 +0100 Subject: [PATCH 02/32] Upgrade setup-python action to version 5 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ca787a..458a635 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: 3.10 - name: Run release setup script From 370c59816b955b39d35db3c01cc73ffd7a72ef80 Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 3 May 2026 17:00:13 +0100 Subject: [PATCH 03/32] Fix python-version formatting in release.yml --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 458a635..21bcd4a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.10 + python-version: "3.10" - name: Run release setup script run: | python3 ./dev/build_release.py -f From e5ec3ee08bb2cae61c1629e5fba5aebcba40f756 Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 18:14:38 +0100 Subject: [PATCH 04/32] eliminate pylance issues --- hexpansion_mgr.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/hexpansion_mgr.py b/hexpansion_mgr.py index c89109d..9a89b3b 100644 --- a/hexpansion_mgr.py +++ b/hexpansion_mgr.py @@ -1,4 +1,5 @@ """ Hexpansion & EEPROM Management Module for HexManager""" +# pyright: reportAttributeAccessIssue=false # # Handles detection, initialisation, programming, upgrading and erasure of hexpansion EEPROMs. # @@ -101,7 +102,7 @@ class HexpansionMgr: "App OK" ] - _LFS_META = 2 # Number of free blocks to reserve for LittleFS metadata when calculating how much space we have to write an app to the EEPROM (keep 1 block in hand for file create/sync, and 1 block in hand to ensure we don't fill the EEPROM completely and cause weird LittleFS behaviour) + _LFS_META = 2 # Number of free blocks to reserve for LittleFS metadata when calculating how much space we have to write an app to the EEPROM # Sub-states are defined at module level (_SUB_*); app-level state # routing is handled by the dispatch tables in app.py. @@ -337,7 +338,7 @@ def _read_port_header(self, port: int): """Read the EEPROM header for the given port and set the default detail page.""" try: self._port_selected_header = self._read_header(port) - except (OSError, RuntimeError, Exception) as e: + except (OSError, RuntimeError, Exception) as e: # pylint: disable=broad-except print(f"H:Error reading header for port {port}: {e}") self._port_selected_header = None self._update_detail_page_count() @@ -568,8 +569,9 @@ def _update_state_erase(self, delta): # pylint: disable=unused-argument return if self._logging: print(f"H:Erasing EEPROM on port {erase_port}") - if self._hexpansion_type_by_slot[erase_port - 1] is not None: - self._hexpansion_init_type = self._hexpansion_type_by_slot[erase_port - 1] + erase_type = self._hexpansion_type_by_slot[erase_port - 1] + if erase_type is not None: + self._hexpansion_init_type = erase_type eeprom_page_size=app.HEXPANSION_TYPES[self._hexpansion_init_type].eeprom_page_size if self._hexpansion_init_type > 0 else _DEFAULT_EEPROM_PAGE_SIZE eeprom_total_size=app.HEXPANSION_TYPES[self._hexpansion_init_type].eeprom_total_size if self._hexpansion_init_type > 0 else _DEFAULT_EEPROM_TOTAL_SIZE erase_addr_len = self._hexpansion_eeprom_addr_len[erase_port - 1] @@ -695,7 +697,9 @@ def _update_state_port_select(self, delta: int): # pylint: disable=unused-argu elif self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_RECOGNISED_OLD_APP: # The selected port has an old app, so we can upgrade it. self._upgrade_port = self._port_selected - self._hexpansion_init_type = self._hexpansion_type_by_slot[self._upgrade_port - 1] if self._hexpansion_type_by_slot[self._upgrade_port - 1] is not None else 0 + upgrade_type = self._hexpansion_type_by_slot[self._upgrade_port - 1] + if upgrade_type is not None: + self._hexpansion_init_type = upgrade_type app.notification = Notification("Upgrade?", port=self._upgrade_port) self._sub_state = _SUB_UPGRADE_CONFIRM elif self._hexpansion_state_by_slot[self._port_selected - 1] >= self.HEXPANSION_STATE_FAULTY: @@ -1068,7 +1072,7 @@ def _update_app_in_eeprom(self, port, type_index: int | None = None) -> int: # Check how much free space we have after deleting the existing app, to try to give a more informative error if the new app doesn't fit. try: - statvfs = os.statvfs(mountpoint) + statvfs = os.statvfs(mountpoint) # pylint: disable=no-member fs_block_size = statvfs[1] if len(statvfs) > 1 and statvfs[1] else statvfs[0] free_blocks = statvfs[4] if len(statvfs) > 4 else statvfs[3] except Exception as e: # pylint: disable=broad-except @@ -1085,9 +1089,7 @@ def _update_app_in_eeprom(self, port, type_index: int | None = None) -> int: max_payload = self._lfs_max_payload(free_blocks, fs_block_size) if app_mpy_size > max_payload: print( - f"H:Not enough free space to write app.mpy for {app.HEXPANSION_TYPES[selected_type].name}" - f" on port {port}: largest writable file is {max_payload}bytes and the app needs {app_mpy_size}bytes" - ) + f"H:app.mpy for {app.HEXPANSION_TYPES[selected_type].name} on port {port} needs {app_mpy_size}bytes: largest writable file is {max_payload}bytes") return _APP_EEPROM_RESULT_FAILURE if self._logging: From c5e3554ee32654db7905abcb17363fa5f3225c86 Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 18:42:37 +0100 Subject: [PATCH 05/32] allow .mpy files into repo --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index d8ccc59..d1e0dba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,7 @@ *.pyc *.pyo -*.mpy HexManager.code-workspace .deploy_state/test_device_download_state.json .editorconfig .venv/ .venv-wsl*/ - From 72383a7e060e8c8c376af87c0b1b90de98843cc9 Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 18:45:44 +0100 Subject: [PATCH 06/32] export-ignore entries in .gitattributes for release --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index e28543c..1b9fc37 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,6 @@ README.md export-ignore dev/ export-ignore tests/ export-ignore typings/ expot-ignore +*.py export-ignore +EEPROM/*.py export-ignore +pyproject.toml export-ignore From 4756c451e9234968967384e379141b0239ff90c4 Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 18:46:40 +0100 Subject: [PATCH 07/32] mpy files for release --- EEPROM/caffeine.mpy | Bin 0 -> 5228 bytes EEPROM/gps.mpy | Bin 0 -> 1470 bytes EEPROM/hexdrive.mpy | Bin 0 -> 5914 bytes app.mpy | Bin 0 -> 10043 bytes hexpansion_mgr.mpy | Bin 0 -> 18022 bytes settings_mgr.mpy | Bin 0 -> 2799 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 EEPROM/caffeine.mpy create mode 100644 EEPROM/gps.mpy create mode 100644 EEPROM/hexdrive.mpy create mode 100644 app.mpy create mode 100644 hexpansion_mgr.mpy create mode 100644 settings_mgr.mpy diff --git a/EEPROM/caffeine.mpy b/EEPROM/caffeine.mpy new file mode 100644 index 0000000000000000000000000000000000000000..fd6945f632d672d87523aff1052a56a606ef6d30 GIT binary patch literal 5228 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 G(E5M*@&y6_ literal 0 HcmV?d00001 diff --git a/EEPROM/gps.mpy b/EEPROM/gps.mpy new file mode 100644 index 0000000000000000000000000000000000000000..5f3d9dda6cac3bddbaecc36a57f3a97ebac38838 GIT binary patch literal 1470 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 literal 0 HcmV?d00001 diff --git a/app.mpy b/app.mpy new file mode 100644 index 0000000000000000000000000000000000000000..6ac5801712aeedd73872a6d9f1150ca22f2d30df GIT binary patch literal 10043 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 literal 0 HcmV?d00001 diff --git a/hexpansion_mgr.mpy b/hexpansion_mgr.mpy new file mode 100644 index 0000000000000000000000000000000000000000..961757160f0c17133b56235f4c4be00a7f35fcf6 GIT binary patch literal 18022 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 literal 0 HcmV?d00001 diff --git a/settings_mgr.mpy b/settings_mgr.mpy new file mode 100644 index 0000000000000000000000000000000000000000..48d7bfcfa52d1a1656c5717b8eba4a96a83b2fbb GIT binary patch literal 2799 zcmZ`&-*ej76}|#P6q{fnDO%Ykj$*(+u#Ihk*{-whI#@V%u!*r@9omf*t&nZHAP9*3 z+R2PylZ@Bx^r_RAK6Ix22Lx$f_KnR$r!#%&4iV8(psl67ds*0*yMfG#;io92ms|RRA ztf?iTB9~=R6{LbJR#4-k`gBib^NpheS^Dg>sQ`^BAe_IklSI8+Yu8rO2w#t{q>-DK z_rzL3MeHeIUCURK%kg#Ot;lMvq6m=JB zNVy=YdnJgobgZJ`jd(I6#FOc*tdP9A5l1uA^Nk6jTq!{-)CZ_Rk^gVGJ7?ADhNkC> zwIXsu-_3=~8pp;={e${WeX$X@6;OToQSV-%B&w)>ud%r1G&+jsRk?_Ib8)CXmGfdeHR{i_QcjK>MfxdZurQB**%O33gSSzTBW z^94D_sU^-DBF-ujAUZJbflTHVgqh}`05vZb@>My992epW%r``LJvd{&U@*C2DJLVx zfMqvuxbeve?qg%(I;f}5rca%9Gn#SMkaVKXpG55R3fLn@_cEFhi)QsC*QK?tGySC*uSQ4yR|mID==2UM8LnBcd;2WBSrzjBvrim6B{vdT_4qkUGS3 zx!Z?l4{je091eI^m}^8IvziKG+m{jJ4r9#uu#I8Ny`5n_YvG;<#tdl2xq}%rzfR^t z_zW|glJLgOuN+=?VtpfdV^gF3#;>Wf{Vkm(;O}{+v*hn|mioQU(kZFz!Otf&FPYLQ zPXv=1#8o$w&@P4oex2&4--{w4ac=M9s8o*F{Ht8r=@)h*80ENvGt?Ych8&8{!>4G_Nv{~De5o$rd1EG zwPQS3+Oa=qo!BhI?&|bFGm80a$CkV4(lNGUyC5Agl%YI)n*5Q^PJ788oApGo2$qd4 zHKm`i!1;o~c>_340q5DV)%EY;{EHKu+nPoC-|bl1B>m-4tN!x4SI0p9WrsZZ4S&-j zzi!D}e`yv$eoqlEjUpfniXhwXE~4k{BAzb&(MAp@v^{QQVrO@C9!k_H;&1#8$+p_-ANC$B{bcE}=kc_8MO@74rgcR;*TWz#Gvpbf&V?bI84BAOf4GMUh8@iNDaqNm z{acK2CvFXC4~+{EfE&TVZMc-u*%A2rbm|g+4M?0ZQcE|__6{YqCmdL%gF#qlv?GYP z1Y9o`6*s7YAHG7->(ApENli7g7#~U4^2b@b7SWF+USSlQ-L}ERA@X9nw%LA%uP*Q?j!;9NzzVC zWuh4WutiTdH={OQ0Z5%1$h^+h7=_3bDwksI7 literal 0 HcmV?d00001 From cc04f401882bfab91905cdd5732b2e90b8c30cda Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 18:51:12 +0100 Subject: [PATCH 08/32] Fix typo in .gitattributes for typings export-ignore --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 1b9fc37..fa3716d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,7 +7,7 @@ README.md export-ignore .vscode/ export-ignore dev/ export-ignore tests/ export-ignore -typings/ expot-ignore +typings/ export-ignore *.py export-ignore EEPROM/*.py export-ignore pyproject.toml export-ignore From cf3b278502fb14fd3b8da399317bb1fccde8cb5b Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 18:56:26 +0100 Subject: [PATCH 09/32] Add .gitattributes to export-ignore list in .gitattributes --- .gitattributes | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitattributes b/.gitattributes index fa3716d..a51ba5a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ .gitignore export-ignore +.gitattributes export-ignore download.bat export-ignore download.sh export-ignore pyproject.toml export-ignore From bf1e315c3f76fd64c751c70cf2894508bbcb03c3 Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 20:29:23 +0100 Subject: [PATCH 10/32] added missing serialise_mgr.mpy --- serialise_mgr.mpy | Bin 0 -> 7048 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 serialise_mgr.mpy diff --git a/serialise_mgr.mpy b/serialise_mgr.mpy new file mode 100644 index 0000000000000000000000000000000000000000..42a3cf293456ff69af3a9157f7367ece0edb55a9 GIT binary patch literal 7048 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 Date: Sun, 3 May 2026 20:30:19 +0100 Subject: [PATCH 11/32] Bump version number to 0.3 --- tildagon.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tildagon.toml b/tildagon.toml index 5c80cb2..49550be 100644 --- a/tildagon.toml +++ b/tildagon.toml @@ -37,4 +37,4 @@ description = "tildagon app for managing hexpansion EEPROMs" # increased, we interpret this as a new version being released. # # Version number must be a string in major.minor format (e.g. "1.3"). -version = "0.2" +version = "0.3" From 4b47e8e6ebea15e398217748c9504a8fd6e15172 Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 20:49:52 +0100 Subject: [PATCH 12/32] Update .gitattributes to change export-ignore settings and adjust binary handling for .mpy files --- .gitattributes | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index a51ba5a..6e44dde 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,5 +10,4 @@ dev/ export-ignore tests/ export-ignore typings/ export-ignore *.py export-ignore -EEPROM/*.py export-ignore -pyproject.toml export-ignore +*.mpy binary -diff From 6c5499b44a2a5f2def9f8f5c816c0802276605cd Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 3 May 2026 20:51:11 +0100 Subject: [PATCH 13/32] Bump version number to 0.4 --- tildagon.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tildagon.toml b/tildagon.toml index 49550be..169222a 100644 --- a/tildagon.toml +++ b/tildagon.toml @@ -37,4 +37,4 @@ description = "tildagon app for managing hexpansion EEPROMs" # increased, we interpret this as a new version being released. # # Version number must be a string in major.minor format (e.g. "1.3"). -version = "0.3" +version = "0.4" From 564c3ecf31a9d8f664e0fdb841f95da37e7994bf Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 3 May 2026 20:51:33 +0100 Subject: [PATCH 14/32] Update app version to 0.4 --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 97f5a5b..84bf696 100644 --- a/app.py +++ b/app.py @@ -15,7 +15,7 @@ RequestStopAppEvent) import app -APP_VERSION = "0.2" # HexManager App Version Number +APP_VERSION = "0.4" # HexManager App Version Number _HEXPANSIONS_JSON = "hexpansions.json" # Name of the hexpansion type definitions file From 40473bc2eb13ec3bc06c1c292b777451da67da37 Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 20:52:30 +0100 Subject: [PATCH 15/32] v0.4 to be consistent with versions --- app.mpy | Bin 10043 -> 10043 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app.mpy b/app.mpy index 6ac5801712aeedd73872a6d9f1150ca22f2d30df..fd69a1d502eadd7a1832ab8c2a4eff3abb3fd008 100644 GIT binary patch delta 14 Wcmdn(x7%;S97aZy&2t&&sR95mRt2d5 delta 14 Wcmdn(x7%;S97aZ?&2t&&sR95mN(H9? From 677fb29f23c722b2fd167443f884292436fef4de Mon Sep 17 00:00:00 2001 From: robotmad Date: Sun, 3 May 2026 20:52:53 +0100 Subject: [PATCH 16/32] Add 'serialise_mgr' to runtime modules list --- dev/build_release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dev/build_release.py b/dev/build_release.py index e3e22ff..1c3e66a 100644 --- a/dev/build_release.py +++ b/dev/build_release.py @@ -12,6 +12,7 @@ "EEPROM/gps", "EEPROM/caffeine", "settings_mgr", + "serialise_mgr", "hexpansion_mgr", } From 6b5ff017f18d818899cfd74eee80668f457d99ba Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sat, 9 May 2026 01:47:19 +0100 Subject: [PATCH 17/32] Refactor minimum BadgeOS version check to use a constant for better maintainability --- EEPROM/hexdrive.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/EEPROM/hexdrive.py b/EEPROM/hexdrive.py index 5405f29..47dff2d 100644 --- a/EEPROM/hexdrive.py +++ b/EEPROM/hexdrive.py @@ -12,6 +12,9 @@ import app +# Define the minimum BadgeOS version required to run this app (e.g. if we need features that are only available in a certain version of BadgeOS) +_MIN_BADGEOS_VERSION = [1, 9, 0] # v1.9.0 is required to be able to read the EEPROM with 16-bit addressing + # HexDrive Hexpansion constants # Hardware defintions: _ENABLE_PIN = 0 # First LS pin used to enable the SMPSU @@ -92,7 +95,7 @@ def __init__(self, config: HexpansionConfig | None = None): ver = self._parse_version(ota.get_version()) #print(f"D:S/W {ver}") # e.g. v1.9.0-beta.1 - if ver >= [1, 9, 0]: + if ver >= _MIN_BADGEOS_VERSION: # we need v1.9.0+ to be able to read the EEPROM with 16-bit addressing, so if we are running on an older version then we cannot continue pass else: From 68f1413701010a7c6dafd2dff3e27a90c281e077 Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sat, 9 May 2026 10:17:15 +0100 Subject: [PATCH 18/32] Use HexDrive2 for V2 EEPROM deployment --- .gitmodules | 3 +++ dev/build_release.py | 35 +++++++++++++++++++++++++++++++++-- dev/download_to_device.py | 1 + hexpansions.json | 6 +++--- tests/test_smoke.py | 31 ++++++++++++++++++++++++------- vendor/HexDrive2 | 1 + 6 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 .gitmodules create mode 160000 vendor/HexDrive2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..5ea844f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/HexDrive2"] + path = vendor/HexDrive2 + url = https://github.com/TeamRobotmad/HexDrive2.git diff --git a/dev/build_release.py b/dev/build_release.py index 1c3e66a..8688aae 100644 --- a/dev/build_release.py +++ b/dev/build_release.py @@ -2,10 +2,17 @@ import os import subprocess import sys +from dataclasses import dataclass from pathlib import Path import mpy_cross + +@dataclass(frozen=True) +class ModuleSpec: + source: Path + artifact: Path + RUNTIME_MODULES = { "app", "EEPROM/hexdrive", @@ -16,6 +23,10 @@ "hexpansion_mgr", } +EXTERNAL_MODULES = ( + ModuleSpec(Path("vendor/HexDrive2/hexdrive2.py"), Path("EEPROM/hexdrive2.mpy")), +) + files_to_mpy = {Path(f"{module}.py") for module in RUNTIME_MODULES} files_to_keep = { @@ -25,10 +36,26 @@ Path("hexpansions.json"), } files_to_keep.update({Path(f"{module}.mpy") for module in RUNTIME_MODULES}) +files_to_keep.update({spec.artifact for spec in EXTERNAL_MODULES}) + +IGNORED_SOURCE_DIRS = (Path("vendor/HexDrive2"),) def _construct_filepaths(dirname, filenames): return [Path(dirname, filename) for filename in filenames] +def _normalise_parts(path: Path) -> tuple[str, ...]: + return tuple(part for part in path.parts if part not in (".", "")) + +def _is_ignored_dir(dirname: str) -> bool: + parts = _normalise_parts(Path(dirname)) + if ".git" in parts: + return True + for ignored_dir in IGNORED_SOURCE_DIRS: + ignored_parts = _normalise_parts(ignored_dir) + if parts[: len(ignored_parts)] == ignored_parts: + return True + return False + def find_files(top_level_dir): walkerator = iter(os.walk(top_level_dir)) dirname, _, filenames = next(walkerator) @@ -36,8 +63,7 @@ def find_files(top_level_dir): all_files = _construct_filepaths(dirname, filenames) for dirname, _, filenames in walkerator: - # if dirname not in dirs_to_keep: - if dirname != "./.git" and ".git/" not in dirname: + if not _is_ignored_dir(dirname): all_files.extend(_construct_filepaths(dirname, filenames)) return all_files @@ -56,6 +82,11 @@ def find_files(top_level_dir): print(f"Mpy-ing file: {file}") mpy_cross.run(file, "-v") + for spec in EXTERNAL_MODULES: + print(f"Mpy-ing file: {spec.source} -> {spec.artifact}") + spec.artifact.parent.mkdir(parents=True, exist_ok=True) + mpy_cross.run(str(spec.source), "-v", "-o", str(spec.artifact)) + if not files_to_keep.issubset(found_files): raise FileNotFoundError(f"Some of {files_to_keep} are not found so assuming wrong directory. " "Please run this script from HexManager dir.") diff --git a/dev/download_to_device.py b/dev/download_to_device.py index e2ec229..f75fa52 100644 --- a/dev/download_to_device.py +++ b/dev/download_to_device.py @@ -42,6 +42,7 @@ class ModuleSpec: ModuleSpec(Path("hexpansion_mgr.py"), Path("hexpansion_mgr.mpy")), ModuleSpec(Path("serialise_mgr.py"), Path("serialise_mgr.mpy")), ModuleSpec(Path("EEPROM/hexdrive.py"), Path("EEPROM/hexdrive.mpy")), + ModuleSpec(Path("vendor/HexDrive2/hexdrive2.py"), Path("EEPROM/hexdrive2.mpy")), ModuleSpec(Path("EEPROM/gps.py"), Path("EEPROM/gps.mpy")), ModuleSpec(Path("EEPROM/caffeine.py"), Path("EEPROM/caffeine.mpy")) ) diff --git a/hexpansions.json b/hexpansions.json index 0203a09..3525bb1 100644 --- a/hexpansions.json +++ b/hexpansions.json @@ -49,17 +49,17 @@ { "pid": "0x10CB", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "Uncommitted", "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive", "app_mpy_version": 6, "app_name": "HexDriveApp" + "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" }, { "pid": "0x10CA", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "2 Motor", "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive", "app_mpy_version": 6, "app_name": "HexDriveApp" + "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" }, { "pid": "0x10CC", "vid": "0xCBCB", "name": "HexDriveV2", "sub_type": "2 Servo", "eeprom_total_size": 32768, "eeprom_page_size": 64, - "app_mpy_name": "hexdrive", "app_mpy_version": 6, "app_name": "HexDriveApp" + "app_mpy_name": "hexdrive2", "app_mpy_version": 7, "app_name": "HexDriveApp" }, { "pid": "0x3000", "vid": "0xCBCB", "name": "HexTest", diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 761f340..92053f5 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -1,6 +1,8 @@ import json +import re import sys import tempfile +from pathlib import Path import pytest @@ -57,28 +59,43 @@ def test_hexdrive_app_init(port): HexDriveApp(config) def test_app_versions_match(): - """Verify that the HexDrive app_mpy_version recorded in hexpansions.json matches - the HexDriveApp.VERSION constant in EEPROM/hexdrive.py. + """Verify that hexpansions.json records the correct source version per app artifact. hexpansions.json is the authoritative record of which .mpy version should be - programmed onto the EEPROM. If someone bumps hexdrive.py VERSION without - updating hexpansions.json (or vice-versa) this test will catch the mismatch. + programmed onto the EEPROM. If someone bumps hexdrive.py or hexdrive2.py + without updating hexpansions.json (or vice-versa) this test will catch it. """ - import json import os from sim.apps.HexManager.EEPROM.hexdrive import HexDriveApp + def extract_version(path: Path) -> int: + content = path.read_text(encoding="utf-8") + match = re.search(r"^\s*VERSION\s*=\s*(\d+)", content, re.MULTILINE) + assert match is not None, f"Could not find VERSION in {path}" + return int(match.group(1)) + json_path = os.path.join(os.path.dirname(__file__), "..", "hexpansions.json") with open(json_path) as f: data = json.load(f) + expected_versions = { + "hexdrive": HexDriveApp.VERSION, + "hexdrive2": extract_version( + Path(__file__).resolve().parents[1] / "vendor" / "HexDrive2" / "hexdrive2.py" + ), + } + hexdrive_entries = [h for h in data["hexpansions"] if h.get("app_name") == "HexDriveApp" and h.get("app_mpy_version") is not None] assert hexdrive_entries, "No HexDriveApp entries with app_mpy_version found in hexpansions.json" for entry in hexdrive_entries: - assert entry["app_mpy_version"] == HexDriveApp.VERSION, ( + app_mpy_name = entry.get("app_mpy_name") + assert app_mpy_name in expected_versions, ( + f"Unexpected app_mpy_name for HexDriveApp entry pid={entry['pid']}: {app_mpy_name}" + ) + assert entry["app_mpy_version"] == expected_versions[app_mpy_name], ( f"hexpansions.json entry pid={entry['pid']} has app_mpy_version=" - f"{entry['app_mpy_version']} but EEPROM/hexdrive.py VERSION={HexDriveApp.VERSION}" + f"{entry['app_mpy_version']} but {app_mpy_name}.py VERSION={expected_versions[app_mpy_name]}" ) def test_hexdrive_type_pids_consistent(): diff --git a/vendor/HexDrive2 b/vendor/HexDrive2 new file mode 160000 index 0000000..2aaca58 --- /dev/null +++ b/vendor/HexDrive2 @@ -0,0 +1 @@ +Subproject commit 2aaca588d3863b114b90c4938c7b35918c90ed08 From bc9c7fe285d094045b1590b4ea28b0ac7ca29ba5 Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 10 May 2026 12:26:40 +0100 Subject: [PATCH 19/32] Vendor HexCurrent for EEPROM deployment --- .gitmodules | 3 +++ dev/build_release.py | 3 ++- dev/download_to_device.py | 1 + hexpansions.json | 5 +++++ tests/test_smoke.py | 19 ++++++++++++------- vendor/HexCurrent | 1 + 6 files changed, 24 insertions(+), 8 deletions(-) create mode 160000 vendor/HexCurrent diff --git a/.gitmodules b/.gitmodules index 5ea844f..a6b30ab 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "vendor/HexDrive2"] path = vendor/HexDrive2 url = https://github.com/TeamRobotmad/HexDrive2.git +[submodule "vendor/HexCurrent"] + path = vendor/HexCurrent + url = https://github.com/TeamRobotmad/HexCurrent.git diff --git a/dev/build_release.py b/dev/build_release.py index 8688aae..397fe60 100644 --- a/dev/build_release.py +++ b/dev/build_release.py @@ -25,6 +25,7 @@ class ModuleSpec: EXTERNAL_MODULES = ( ModuleSpec(Path("vendor/HexDrive2/hexdrive2.py"), Path("EEPROM/hexdrive2.mpy")), + ModuleSpec(Path("vendor/HexCurrent/hexcurrent.py"), Path("EEPROM/hexcurrent.mpy")), ) files_to_mpy = {Path(f"{module}.py") for module in RUNTIME_MODULES} @@ -38,7 +39,7 @@ class ModuleSpec: files_to_keep.update({Path(f"{module}.mpy") for module in RUNTIME_MODULES}) files_to_keep.update({spec.artifact for spec in EXTERNAL_MODULES}) -IGNORED_SOURCE_DIRS = (Path("vendor/HexDrive2"),) +IGNORED_SOURCE_DIRS = (Path("vendor/HexDrive2"), Path("vendor/HexCurrent")) def _construct_filepaths(dirname, filenames): return [Path(dirname, filename) for filename in filenames] diff --git a/dev/download_to_device.py b/dev/download_to_device.py index f75fa52..e219f7f 100644 --- a/dev/download_to_device.py +++ b/dev/download_to_device.py @@ -43,6 +43,7 @@ class ModuleSpec: ModuleSpec(Path("serialise_mgr.py"), Path("serialise_mgr.mpy")), ModuleSpec(Path("EEPROM/hexdrive.py"), Path("EEPROM/hexdrive.mpy")), ModuleSpec(Path("vendor/HexDrive2/hexdrive2.py"), Path("EEPROM/hexdrive2.mpy")), + ModuleSpec(Path("vendor/HexCurrent/hexcurrent.py"), Path("EEPROM/hexcurrent.mpy")), ModuleSpec(Path("EEPROM/gps.py"), Path("EEPROM/gps.mpy")), ModuleSpec(Path("EEPROM/caffeine.py"), Path("EEPROM/caffeine.mpy")) ) diff --git a/hexpansions.json b/hexpansions.json index 3525bb1..88063ac 100644 --- a/hexpansions.json +++ b/hexpansions.json @@ -65,6 +65,11 @@ "pid": "0x3000", "vid": "0xCBCB", "name": "HexTest", "eeprom_total_size": 65536, "eeprom_page_size": 128 }, + { + "pid": "0x5000", "vid": "0xCBCB", "name": "HexCurrent", + "eeprom_total_size": 65536, "eeprom_page_size": 128, + "app_mpy_name": "hexcurrent", "app_mpy_version": 1, "app_name": "HexCurrentApp" + }, { "pid": "0x4000", "vid": "0xCBCB", "name": "HexDiag", "eeprom_total_size": 65536, "eeprom_page_size": 128 diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 92053f5..4580320 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -59,10 +59,10 @@ def test_hexdrive_app_init(port): HexDriveApp(config) def test_app_versions_match(): - """Verify that hexpansions.json records the correct source version per app artifact. + """Verify that hexpansions.json records the correct source version per vendored artifact. hexpansions.json is the authoritative record of which .mpy version should be - programmed onto the EEPROM. If someone bumps hexdrive.py or hexdrive2.py + programmed onto the EEPROM. If someone bumps a vendored app source file without updating hexpansions.json (or vice-versa) this test will catch it. """ import os @@ -83,15 +83,20 @@ def extract_version(path: Path) -> int: "hexdrive2": extract_version( Path(__file__).resolve().parents[1] / "vendor" / "HexDrive2" / "hexdrive2.py" ), + "hexcurrent": extract_version( + Path(__file__).resolve().parents[1] / "vendor" / "HexCurrent" / "hexcurrent.py" + ), } - hexdrive_entries = [h for h in data["hexpansions"] - if h.get("app_name") == "HexDriveApp" and h.get("app_mpy_version") is not None] - assert hexdrive_entries, "No HexDriveApp entries with app_mpy_version found in hexpansions.json" - for entry in hexdrive_entries: + versioned_entries = [ + entry for entry in data["hexpansions"] + if entry.get("app_mpy_name") in expected_versions and entry.get("app_mpy_version") is not None + ] + assert versioned_entries, "No recognised versioned app entries found in hexpansions.json" + for entry in versioned_entries: app_mpy_name = entry.get("app_mpy_name") assert app_mpy_name in expected_versions, ( - f"Unexpected app_mpy_name for HexDriveApp entry pid={entry['pid']}: {app_mpy_name}" + f"Unexpected app_mpy_name for versioned entry pid={entry['pid']}: {app_mpy_name}" ) assert entry["app_mpy_version"] == expected_versions[app_mpy_name], ( f"hexpansions.json entry pid={entry['pid']} has app_mpy_version=" diff --git a/vendor/HexCurrent b/vendor/HexCurrent new file mode 160000 index 0000000..a0c8bec --- /dev/null +++ b/vendor/HexCurrent @@ -0,0 +1 @@ +Subproject commit a0c8beca00816643091c0ee3d907cc92203f48ed From ec894ab2cc8f46d4631ad6d359064a6ccdf5e7f8 Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 10 May 2026 12:32:43 +0100 Subject: [PATCH 20/32] Bump vendored HexDrive2 submodule --- vendor/HexDrive2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/HexDrive2 b/vendor/HexDrive2 index 2aaca58..080b31a 160000 --- a/vendor/HexDrive2 +++ b/vendor/HexDrive2 @@ -1 +1 @@ -Subproject commit 2aaca588d3863b114b90c4938c7b35918c90ed08 +Subproject commit 080b31afa10eae7551300fecf19bbc6fafdadd45 From 772035f82c08d351cd86cc5a97c2eb1f1fe23a1d Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 10 May 2026 13:32:28 +0100 Subject: [PATCH 21/32] Bump vendored HexCurrent submodule --- vendor/HexCurrent | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/HexCurrent b/vendor/HexCurrent index a0c8bec..8cfe77b 160000 --- a/vendor/HexCurrent +++ b/vendor/HexCurrent @@ -1 +1 @@ -Subproject commit a0c8beca00816643091c0ee3d907cc92203f48ed +Subproject commit 8cfe77b662d570cc851a5bfab17bc917a5f76588 From 36b0e01ad715626ebc611b53431f0140ff96cf53 Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 10 May 2026 13:44:03 +0100 Subject: [PATCH 22/32] Refresh compiled EEPROM artifacts --- EEPROM/hexcurrent.mpy | Bin 0 -> 15582 bytes EEPROM/hexdrive.mpy | Bin 5914 -> 5942 bytes EEPROM/hexdrive2.mpy | Bin 0 -> 5756 bytes settings_mgr.mpy | Bin 2799 -> 2804 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 EEPROM/hexcurrent.mpy create mode 100644 EEPROM/hexdrive2.mpy diff --git a/EEPROM/hexcurrent.mpy b/EEPROM/hexcurrent.mpy new file mode 100644 index 0000000000000000000000000000000000000000..36f7d9ded80865f3b89e9ddd26b73312ad121dc9 GIT binary patch literal 15582 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 k#&^(QM`tYJ+ScDf+Ro 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 literal 0 HcmV?d00001 diff --git a/settings_mgr.mpy b/settings_mgr.mpy index 48d7bfcfa52d1a1656c5717b8eba4a96a83b2fbb..8c14810b37820bec1a646339755016ff365edcad 100644 GIT binary patch delta 202 zcmV;*05$*b74#JbO$GoTVHB|jAp-#|vmyi60w4$}3>j5`FCa7$RIrNluz=e{L?Zw~ zLL&k4Gy%b}BLb5e27dtqlbQx2lY$2mlh6i^0Rxj?2O|LmlY<8^7zPv^uoM=llnFrr z43pLgI{^)o5(+c{4wFX;C;<h74H=WO$GoTVH2?iAp^551JVK^5Gf29Re&!bH4;>?iuAC6+eAbo0763} z0MYU^0l~2&0h1pFe*prMp9UiV1CzrBpaBAtWCtUY!UhwQng<>j1r!{x6c#oBHUY2` z5jFx@LQ@h%6dxl6lh6kn0S1%o2ZR9!lW_=F0SJ@H2zCJplRybalRpU)lbQ)YlZOcs zlk5p90SuED3N!%?lS~RI0S=RO3PKqV5IGbauoEIS0T4PA9J4k7D+41CBM~DKlh6tz D0i`qR From 4ce2d0bb2df4f831c56524b9b577179683e45e1c Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 10 May 2026 14:24:17 +0100 Subject: [PATCH 23/32] Auto-detect blank EEPROM geometry --- hexpansion_mgr.py | 194 ++++++++++++++++++++++++++++++++++++---- tests/test_serialise.py | 143 +++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+), 15 deletions(-) diff --git a/hexpansion_mgr.py b/hexpansion_mgr.py index 9a89b3b..67e3dde 100644 --- a/hexpansion_mgr.py +++ b/hexpansion_mgr.py @@ -30,6 +30,8 @@ # EEPROM Constants _DEFAULT_EEPROM_PAGE_SIZE = 32 _DEFAULT_EEPROM_TOTAL_SIZE = 64 * 1024 // 8 +_EEPROM_PAGE_SIZE_CANDIDATES = (16, 32, 64, 128, 256) +_EEPROM_TOTAL_SIZE_CANDIDATES = (256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536) _IS_SIMULATOR = sys.platform != "esp32" @@ -122,6 +124,8 @@ def __init__(self, app, logging: bool = False): self._hexpansion_state_by_slot: list[int] = [self.HEXPANSION_STATE_UNKNOWN]*_NUM_HEXPANSION_SLOTS self._hexpansion_eeprom_addr_len: list[int | None] = [None]*_NUM_HEXPANSION_SLOTS self._hexpansion_eeprom_addr: list[int | None] = [None]*_NUM_HEXPANSION_SLOTS + self._hexpansion_eeprom_total_size: list[int | None] = [None]*_NUM_HEXPANSION_SLOTS + self._hexpansion_eeprom_page_size: list[int | None] = [None]*_NUM_HEXPANSION_SLOTS self._hexpansion_init_type: int = 0 self._detected_port: int | None = None self._waiting_app_port: int | None = None @@ -166,6 +170,129 @@ def logging(self, value: bool): """Set the logging state.""" self._logging = value + @staticmethod + def _mem_addr_bytes(mem_addr: int, addr_len: int) -> bytes: + return bytes((mem_addr >> shift) & 0xFF for shift in range((addr_len - 1) * 8, -1, -8)) + + @staticmethod + def _mem_target(addr: int, addr_len: int, mem_addr: int) -> tuple[int, int]: + mem_addr_mask = (1 << (addr_len * 8)) - 1 + return addr | (mem_addr >> (8 * addr_len)), mem_addr & mem_addr_mask + + @classmethod + def _write_eeprom_bytes(cls, i2c, addr: int, addr_len: int, mem_addr: int, data: bytes): + device_addr, masked_mem_addr = cls._mem_target(addr, addr_len, mem_addr) + i2c.writeto_mem(device_addr, masked_mem_addr, data, addrsize=8 * addr_len) + while True: + try: + if i2c.writeto(device_addr, cls._mem_addr_bytes(masked_mem_addr, addr_len)): + return + except OSError: + pass + finally: + time.sleep_ms(1) + + @classmethod + def _read_eeprom_bytes(cls, i2c, addr: int, addr_len: int, mem_addr: int, size: int) -> bytes: + device_addr, masked_mem_addr = cls._mem_target(addr, addr_len, mem_addr) + return i2c.readfrom_mem(device_addr, masked_mem_addr, size, addrsize=8 * addr_len) + + def _clear_eeprom_geometry(self, port: int): + self._hexpansion_eeprom_total_size[port - 1] = None + self._hexpansion_eeprom_page_size[port - 1] = None + + def _cache_eeprom_geometry(self, port: int, total_size: int, page_size: int): + self._hexpansion_eeprom_total_size[port - 1] = total_size + self._hexpansion_eeprom_page_size[port - 1] = page_size + + def _get_eeprom_geometry(self, port: int) -> tuple[int | None, int | None]: + if port not in range(1, _NUM_HEXPANSION_SLOTS + 1): + return None, None + return self._hexpansion_eeprom_total_size[port - 1], self._hexpansion_eeprom_page_size[port - 1] + + def _has_eeprom_geometry(self, port: int) -> bool: + total_size, page_size = self._get_eeprom_geometry(port) + return total_size is not None and page_size is not None + + def _geometry_for_type(self, type_index: int | None) -> tuple[int | None, int | None]: + if type_index is None or type_index < 0 or type_index >= len(self._app.HEXPANSION_TYPES): + return None, None + hexpansion_type = self._app.HEXPANSION_TYPES[type_index] + return hexpansion_type.eeprom_total_size, hexpansion_type.eeprom_page_size + + def _geometry_for_port(self, port: int, fallback_type_index: int | None = None) -> tuple[int | None, int | None]: + total_size, page_size = self._get_eeprom_geometry(port) + if total_size is not None and page_size is not None: + return total_size, page_size + return self._geometry_for_type(fallback_type_index) + + def _probe_eeprom_page_size(self, i2c, addr: int, addr_len: int) -> int | None: + for page_size in _EEPROM_PAGE_SIZE_CANDIDATES: + pattern = bytes(((page_size + index + 1) & 0xFF) for index in range(page_size + 1)) + try: + self._write_eeprom_bytes(i2c, addr, addr_len, 0, pattern) + readback = self._read_eeprom_bytes(i2c, addr, addr_len, 0, len(pattern)) + except OSError as e: + print(f"H:Error probing EEPROM page size: {e}") + return None + if readback != pattern: + return page_size + return _EEPROM_PAGE_SIZE_CANDIDATES[-1] + + def _probe_eeprom_total_size(self, i2c, addr: int, addr_len: int) -> int | None: + baseline = b"\xA5" + probe = b"\x5A" + for total_size in _EEPROM_TOTAL_SIZE_CANDIDATES: + try: + self._write_eeprom_bytes(i2c, addr, addr_len, 0, baseline) + self._write_eeprom_bytes(i2c, addr, addr_len, total_size, probe) + except OSError: + return total_size + try: + if self._read_eeprom_bytes(i2c, addr, addr_len, 0, 1) == probe: + return total_size + except OSError as e: + print(f"H:Error probing EEPROM total size: {e}") + return None + return None + + def _detect_eeprom_geometry(self, port: int, force: bool = False) -> tuple[int | None, int | None]: + if not force and self._has_eeprom_geometry(port): + return self._get_eeprom_geometry(port) + try: + i2c = I2C(port) + except Exception as e: # pylint: disable=broad-except + print(f"H:Error opening I2C port {port}: {e}") + return None, None + try: + header = self._read_header(port, i2c=i2c) + except RuntimeError: + header = None + except OSError as e: + print(f"H:Error probing EEPROM geometry on port {port}: {e}") + return None, None + except Exception as e: # pylint: disable=broad-except + print(f"H:Error reading EEPROM geometry cache on port {port}: {e}") + return None, None + if header is not None: + return header.eeprom_total_size, header.eeprom_page_size + + addr_len = self._hexpansion_eeprom_addr_len[port - 1] + addr = self._hexpansion_eeprom_addr[port - 1] + if addr_len is None or addr is None: + return None, None + + page_size = self._probe_eeprom_page_size(i2c, addr, addr_len) + if page_size is None: + return None, None + total_size = self._probe_eeprom_total_size(i2c, addr, addr_len) + if total_size is None: + return None, None + if not self._erase_eeprom(port, addr, addr_len, total_size, page_size): + return None, None + self._cache_eeprom_geometry(port, total_size, page_size) + return total_size, page_size + def probe_eeprom(self, port: int) -> tuple[int, HexpansionHeader | None]: """Probe a port and report whether its EEPROM is blank, missing, or already written.""" try: @@ -199,12 +326,14 @@ def erase_eeprom_for_type(self, port: int, type_index: int) -> bool: erase_addr = self._hexpansion_eeprom_addr[port - 1] if erase_addr_len is None or erase_addr is None: return False - hexpansion_type = self._app.HEXPANSION_TYPES[type_index] + eeprom_total_size, eeprom_page_size = self._geometry_for_port(port, type_index) + if eeprom_total_size is None or eeprom_page_size is None: + return False return self._erase_eeprom(port, erase_addr, erase_addr_len, - hexpansion_type.eeprom_total_size, - hexpansion_type.eeprom_page_size) + eeprom_total_size, + eeprom_page_size) def prepare_eeprom_for_type(self, port: int, type_index: int, unique_id: int | None) -> bool: @@ -253,6 +382,8 @@ def refresh_slot_records(self): self._hexpansion_state_by_slot = [self.HEXPANSION_STATE_UNKNOWN] * _NUM_HEXPANSION_SLOTS self._hexpansion_eeprom_addr_len = [None] * _NUM_HEXPANSION_SLOTS self._hexpansion_eeprom_addr = [None] * _NUM_HEXPANSION_SLOTS + self._hexpansion_eeprom_total_size = [None] * _NUM_HEXPANSION_SLOTS + self._hexpansion_eeprom_page_size = [None] * _NUM_HEXPANSION_SLOTS self._port_selected_header = None self._hexpansion_serial_number = None @@ -274,6 +405,9 @@ async def _handle_removal(self, event): port = event.port self._hexpansion_type_by_slot[port - 1] = None self._hexpansion_state_by_slot[port - 1] = self.HEXPANSION_STATE_EMPTY + self._hexpansion_eeprom_addr_len[port - 1] = None + self._hexpansion_eeprom_addr[port - 1] = None + self._clear_eeprom_geometry(port) if port in self._ports_to_initialise: self._ports_to_initialise.remove(port) self._ports_to_check_app.discard(port) @@ -284,8 +418,6 @@ async def _handle_removal(self, event): (self._erase_port is not None and port == self._erase_port) or \ (self._port_selected != 0 and port == self._port_selected): # The port from which a hexpansion has been removed is significant - self._hexpansion_eeprom_addr_len[port - 1] = None - self._hexpansion_eeprom_addr[port - 1] = None app.hexpansion_update_required = True if self._logging: print(f"H:Hexpansion removed from port {port}") @@ -294,6 +426,9 @@ async def _handle_removal(self, event): async def _handle_insertion(self, event): if self._app.serialise_active: return + self._hexpansion_eeprom_addr_len[event.port - 1] = None + self._hexpansion_eeprom_addr[event.port - 1] = None + self._clear_eeprom_geometry(event.port) if self._check_port_for_known_hexpansions(event.port) or event.port == self._port_selected: # A known hexpansion type has been detected on the inserted port, so trigger an update of # the hexpansion management state machine to handle it. Or the inserted port is the one @@ -338,7 +473,9 @@ def _read_port_header(self, port: int): """Read the EEPROM header for the given port and set the default detail page.""" try: self._port_selected_header = self._read_header(port) - except (OSError, RuntimeError, Exception) as e: # pylint: disable=broad-except + except RuntimeError: + self._port_selected_header = None + except (OSError, Exception) as e: # pylint: disable=broad-except print(f"H:Error reading header for port {port}: {e}") self._port_selected_header = None self._update_detail_page_count() @@ -354,6 +491,9 @@ def _update_detail_page_count(self): # Unrecognised type - show vid/pid page and EEPROM page but not details page self._port_detail_page_count = 2 self._port_detail_page = self._PAGE_VID_PID + elif state_idx == self.HEXPANSION_STATE_BLANK: + self._port_detail_page_count = 1 + self._port_detail_page = self._PAGE_EEPROM elif state_idx >= self.HEXPANSION_STATE_RECOGNISED: # Recognised type - show vid/pid page and details page self._port_detail_page_count = 3 @@ -572,8 +712,11 @@ def _update_state_erase(self, delta): # pylint: disable=unused-argument erase_type = self._hexpansion_type_by_slot[erase_port - 1] if erase_type is not None: self._hexpansion_init_type = erase_type - eeprom_page_size=app.HEXPANSION_TYPES[self._hexpansion_init_type].eeprom_page_size if self._hexpansion_init_type > 0 else _DEFAULT_EEPROM_PAGE_SIZE - eeprom_total_size=app.HEXPANSION_TYPES[self._hexpansion_init_type].eeprom_total_size if self._hexpansion_init_type > 0 else _DEFAULT_EEPROM_TOTAL_SIZE + eeprom_total_size, eeprom_page_size = self._geometry_for_port(erase_port, self._hexpansion_init_type) + if eeprom_total_size is None: + eeprom_total_size = _DEFAULT_EEPROM_TOTAL_SIZE + if eeprom_page_size is None: + eeprom_page_size = _DEFAULT_EEPROM_PAGE_SIZE erase_addr_len = self._hexpansion_eeprom_addr_len[erase_port - 1] erase_addr = self._hexpansion_eeprom_addr[erase_port - 1] if erase_addr_len is None or erase_addr is None: @@ -678,8 +821,16 @@ def _update_state_port_select(self, delta: int): # pylint: disable=unused-argu app = self._app if app.button_states.get(BUTTON_TYPES["RIGHT"]): app.button_states.clear() - self._port_selected = (self._port_selected % _NUM_HEXPANSION_SLOTS) + 1 - self._read_port_header(self._port_selected) + if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK and not self._has_eeprom_geometry(self._port_selected): + total_size, page_size = self._detect_eeprom_geometry(self._port_selected) + if total_size is not None and page_size is not None: + app.notification = Notification("Scanned", port=self._port_selected) + else: + app.notification = Notification("Failed", port=self._port_selected) + self._read_port_header(self._port_selected) + else: + self._port_selected = (self._port_selected % _NUM_HEXPANSION_SLOTS) + 1 + self._read_port_header(self._port_selected) app.refresh = True elif app.button_states.get(BUTTON_TYPES["LEFT"]): app.button_states.clear() @@ -795,6 +946,7 @@ def _draw_port_select(self, ctx): """Draw the port-select screen with paged details.""" app = self._app hdr = self._port_selected_header + total_size, page_size = self._get_eeprom_geometry(self._port_selected) hexpansion_state = self.HEXPANSION_STATE_NAMES[self._hexpansion_state_by_slot[self._port_selected - 1]] if self._hexpansion_state_by_slot[self._port_selected - 1] > self.HEXPANSION_STATE_BLANK: hexpansion_name = self._type_name_for_port(self._port_selected, None) @@ -807,7 +959,8 @@ def _draw_port_select(self, ctx): else: # Common header lines for all pages page = self._port_detail_page - lines = [f"Slot {self._port_selected}-{self._PAGE_NAMES[page]}", hdr.friendly_name if hdr is not None else hexpansion_name] + page_title = hdr.friendly_name if hdr is not None else ("Blank EEPROM" if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK else hexpansion_name) + lines = [f"Slot {self._port_selected}-{self._PAGE_NAMES[page]}", page_title] colours = [(1, 1, 0), (1, 0, 1)] if page == self._PAGE_VID_PID: # VID / PID page @@ -822,6 +975,10 @@ def _draw_port_select(self, ctx): if hdr is not None: lines += [f"Size: {hdr.eeprom_total_size} Bytes", f"Page: {hdr.eeprom_page_size} Bytes"] colours += [(0, 1, 1), (0, 1, 1)] + else: + lines += [f"Size: {total_size} Bytes" if total_size is not None else "Size: Unknown", + f"Page: {page_size} Bytes" if page_size is not None else "Page: Unknown"] + colours += [(0, 1, 1), (0, 1, 1)] else: # page == self._PAGE_DETAILS: # Details page (only when page_count == 3) type_idx = self._hexpansion_type_by_slot[self._port_selected - 1] @@ -863,7 +1020,8 @@ def _draw_port_select(self, ctx): confirm_label = "Init" if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK else \ "Upgrade" if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_RECOGNISED_OLD_APP else \ "Erase" if self._hexpansion_state_by_slot[self._port_selected - 1] >= self.HEXPANSION_STATE_FAULTY else "" - button_labels(ctx, confirm_label=confirm_label, left_label="" + button_labels(ctx, confirm_label=confirm_label, left_label=" HexpansionHeader | No # pass the exception up to the caller raise OSError(e) from e hexpansion_header = HexpansionHeader.from_bytes(header_bytes) + self._cache_eeprom_geometry(port, hexpansion_header.eeprom_total_size, hexpansion_header.eeprom_page_size) print(f"H:Header on port {port}: {hexpansion_header}") return hexpansion_header @@ -1141,13 +1300,18 @@ def _prepare_eeprom(self, port, type_index: int | None = None, unique_id: int | app = self._app selected_type = self._hexpansion_init_type if type_index is None else type_index serial_number = self._hexpansion_serial_number if unique_id is None else unique_id + eeprom_total_size, eeprom_page_size = self._get_eeprom_geometry(port) + if eeprom_total_size is None or eeprom_page_size is None: + eeprom_total_size, eeprom_page_size = self._detect_eeprom_geometry(port) + if eeprom_total_size is None or eeprom_page_size is None: + return False if self._logging: print(f"H:Initialising EEPROM on port {port} as {selected_type}:{app.HEXPANSION_TYPES[selected_type].name}...") hexpansion_header_to_write = HexpansionHeader( manifest_version="2024", - fs_offset=max(32, app.HEXPANSION_TYPES[selected_type].eeprom_page_size), - eeprom_page_size=app.HEXPANSION_TYPES[selected_type].eeprom_page_size, - eeprom_total_size=app.HEXPANSION_TYPES[selected_type].eeprom_total_size, + fs_offset=max(32, eeprom_page_size), + eeprom_page_size=eeprom_page_size, + eeprom_total_size=eeprom_total_size, vid=app.HEXPANSION_TYPES[selected_type].vid, pid=app.HEXPANSION_TYPES[selected_type].pid, unique_id=serial_number if serial_number is not None else 0, diff --git a/tests/test_serialise.py b/tests/test_serialise.py index 381c53d..760ad2e 100644 --- a/tests/test_serialise.py +++ b/tests/test_serialise.py @@ -19,6 +19,54 @@ def clear(self): self._pressed.clear() +class FakeProbeI2C: + def __init__(self, total_size: int, page_size: int, addr_len: int, base_addr: int = 0x50): + self.total_size = total_size + self.page_size = page_size + self.addr_len = addr_len + self.base_addr = base_addr + self.memory = bytearray([0xFF] * total_size) + + def _device_count(self) -> int: + if self.addr_len == 1: + return max(1, self.total_size // 256) + return 1 + + def _valid_addr(self, addr: int) -> bool: + if self.addr_len == 1: + return self.base_addr <= addr < self.base_addr + self._device_count() + return addr == self.base_addr + + def _absolute_addr(self, addr: int, mem_addr: int) -> int: + if not self._valid_addr(addr): + raise OSError("no EEPROM") + if self.addr_len == 1: + return ((addr - self.base_addr) << 8) | mem_addr + return mem_addr % self.total_size + + def scan(self): + return list(range(self.base_addr, self.base_addr + self._device_count())) + + def writeto(self, addr, data): + if not self._valid_addr(addr): + raise OSError("no EEPROM") + return len(data) + + def writeto_mem(self, addr, mem_addr, data, addrsize=16): # pylint: disable=unused-argument + absolute_addr = self._absolute_addr(addr, mem_addr) + page_base = absolute_addr - (absolute_addr % self.page_size) + page_offset = absolute_addr % self.page_size + for index, value in enumerate(data): + target = page_base + ((page_offset + index) % self.page_size) + if target < self.total_size: + self.memory[target] = value + return len(data) + + def readfrom_mem(self, addr, mem_addr, length, addrsize=16): # pylint: disable=unused-argument + absolute_addr = self._absolute_addr(addr, mem_addr) + return bytes(self.memory[(absolute_addr + index) % self.total_size] for index in range(length)) + + @pytest.fixture def serialise_app(hexmanager_app): from events.input import BUTTON_TYPES @@ -200,6 +248,8 @@ def test_refresh_slot_records_rescans_all_slots(hexmanager_app, monkeypatch): helper._hexpansion_state_by_slot = [1] * _NUM_HEXPANSION_SLOTS helper._hexpansion_eeprom_addr_len = [1] * _NUM_HEXPANSION_SLOTS helper._hexpansion_eeprom_addr = [0x50] * _NUM_HEXPANSION_SLOTS + helper._hexpansion_eeprom_total_size = [2048] * _NUM_HEXPANSION_SLOTS + helper._hexpansion_eeprom_page_size = [16] * _NUM_HEXPANSION_SLOTS monkeypatch.setattr(helper, '_check_port_for_known_hexpansions', lambda port: checked_ports.append(port) or False) monkeypatch.setattr(helper, '_read_port_header', lambda port: header_ports.append(port)) @@ -218,6 +268,99 @@ def test_refresh_slot_records_rescans_all_slots(hexmanager_app, monkeypatch): assert helper._hexpansion_state_by_slot == [HexpansionMgr.HEXPANSION_STATE_UNKNOWN] * _NUM_HEXPANSION_SLOTS assert helper._hexpansion_eeprom_addr_len == [None] * _NUM_HEXPANSION_SLOTS assert helper._hexpansion_eeprom_addr == [None] * _NUM_HEXPANSION_SLOTS + assert helper._hexpansion_eeprom_total_size == [None] * _NUM_HEXPANSION_SLOTS + assert helper._hexpansion_eeprom_page_size == [None] * _NUM_HEXPANSION_SLOTS + + +@pytest.mark.parametrize(("total_size", "page_size", "addr_len"), [(2048, 16, 1), (32768, 64, 2)]) +def test_detect_eeprom_geometry_restores_blank_chip(hexmanager_app, monkeypatch, total_size, page_size, addr_len): + from sim.apps.HexManager import hexpansion_mgr as hexpansion_module + + helper = hexmanager_app._hexpansion_mgr + fake_i2c = FakeProbeI2C(total_size=total_size, page_size=page_size, addr_len=addr_len) + helper._hexpansion_eeprom_addr_len[0] = addr_len + helper._hexpansion_eeprom_addr[0] = 0x50 + + monkeypatch.setattr(hexpansion_module, 'I2C', lambda port: fake_i2c) + + assert helper._detect_eeprom_geometry(1) == (total_size, page_size) + assert helper._hexpansion_eeprom_total_size[0] == total_size + assert helper._hexpansion_eeprom_page_size[0] == page_size + assert fake_i2c.memory == bytearray([0xFF] * total_size) + + +def test_prepare_eeprom_uses_detected_geometry_for_header(hexmanager_app, monkeypatch): + from types import SimpleNamespace + + from sim.apps.HexManager import hexpansion_mgr as hexpansion_module + + helper = hexmanager_app._hexpansion_mgr + helper._hexpansion_eeprom_addr_len[0] = 2 + helper._hexpansion_eeprom_addr[0] = 0x50 + captured = {} + + monkeypatch.setattr(hexpansion_module, 'I2C', lambda port: object()) + monkeypatch.setattr(helper, '_detect_eeprom_geometry', lambda port, force=False: (32768, 64)) + monkeypatch.setattr(hexpansion_module, 'write_header', lambda port, header, addr=None, addr_len=None, page_size=None: captured.update({'header': header, 'addr': addr, 'addr_len': addr_len, 'page_size': page_size})) + monkeypatch.setattr(helper, '_read_header', lambda port, i2c=None: captured['header']) + monkeypatch.setattr(hexpansion_module, 'get_hexpansion_block_devices', lambda i2c, header, addr, addr_len=None: (None, object())) + monkeypatch.setattr(hexpansion_module.vfs, 'VfsLfs2', SimpleNamespace(mkfs=lambda partition: None), raising=False) + monkeypatch.setattr(hexpansion_module.vfs, 'mount', lambda partition, mountpoint, readonly=False: None, raising=False) + + assert helper._prepare_eeprom(1, type_index=0, unique_id=123) + assert captured['header'].eeprom_total_size == 32768 + assert captured['header'].eeprom_page_size == 64 + assert captured['header'].fs_offset == 64 + assert captured['page_size'] == 64 + + +def test_blank_port_scan_button_and_geometry_details(hexmanager_app, monkeypatch): + from events.input import BUTTON_TYPES + from sim.apps.HexManager import hexpansion_mgr as hexpansion_module + from sim.apps.HexManager.hexpansion_mgr import _SUB_PORT_SELECT + + app = hexmanager_app + helper = app._hexpansion_mgr + app.button_states = FakeButtons(BUTTON_TYPES) + helper._sub_state = _SUB_PORT_SELECT + helper._port_selected = 1 + helper._hexpansion_state_by_slot[0] = helper.HEXPANSION_STATE_BLANK + helper._update_detail_page_count() + + rendered = {} + labels = {} + + monkeypatch.setattr(app, 'draw_message', lambda ctx, lines, colours, font: rendered.update({'lines': list(lines)})) + monkeypatch.setattr(hexpansion_module, 'button_labels', lambda ctx, **kwargs: labels.update(kwargs)) + + helper._draw_port_select(None) + assert 'Size: Unknown' in rendered['lines'] + assert 'Page: Unknown' in rendered['lines'] + assert labels['right_label'] == 'Scan' + + scan_calls = [] + + def fake_detect(port, force=False): + scan_calls.append((port, force)) + helper._hexpansion_eeprom_total_size[port - 1] = 8192 + helper._hexpansion_eeprom_page_size[port - 1] = 32 + return 8192, 32 + + monkeypatch.setattr(helper, '_detect_eeprom_geometry', fake_detect) + monkeypatch.setattr(helper, '_read_port_header', lambda port: None) + + app.button_states.press('RIGHT') + helper._update_state_port_select(0) + + assert scan_calls == [(1, False)] + assert helper._port_selected == 1 + + rendered.clear() + labels.clear() + helper._draw_port_select(None) + assert 'Size: 8192 Bytes' in rendered['lines'] + assert 'Page: 32 Bytes' in rendered['lines'] + assert labels['right_label'] == 'Slot>' def test_hexpansion_events_are_suppressed_while_serialise_active(hexmanager_app, monkeypatch): From bf865a88cc051f32fbe31e39294c895f2ba85f56 Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 10 May 2026 15:33:31 +0100 Subject: [PATCH 24/32] Improve warning handling and EEPROM scan UX --- EEPROM/hexcurrent.mpy | Bin 15582 -> 15537 bytes app.mpy | Bin 10043 -> 11358 bytes app.py | 114 ++++++++++++++++++++++++++++++++--- hexpansion_mgr.mpy | Bin 18022 -> 21618 bytes hexpansion_mgr.py | 130 +++++++++++++++++++++++++++++++++------- tests/test_serialise.py | 75 ++++++++++++++++++++++- tests/test_smoke.py | 92 ++++++++++++++++++++++++++++ 7 files changed, 380 insertions(+), 31 deletions(-) diff --git a/EEPROM/hexcurrent.mpy b/EEPROM/hexcurrent.mpy index 36f7d9ded80865f3b89e9ddd26b73312ad121dc9..21a06f24267d7e44a8206d67d5b7ced9d2042a54 100644 GIT binary patch delta 955 zcmY*XT}%{L6y7^a*!2gT-5tl(BFoOs{|@Xjuq(($ah=fEZY&E$*|liNf&u?9QK(=H zdq&rv_@cc@lUCD`J~U1HGV90#4G)x;wogqYF-8(zeCb0&BAB#&>zyH*`0|}Ilkc4K zedpY@>FTuATwC9CW_a@I#F^0x!Gi{qE@H-Y{3 zV&KuPU}f8AcO`PKzOubiSMgQoiXV?hKR`0BM5meseLD+cpNjh1cCaAnJ6TZlIu>}n zo`tx+iv>-mS%~QY7F1j2))6Y(d-!|Q?ClP<0itHny!zmkc0lTqI4cCm7$Qg=JsC77 zN#d<;5Zue%8GF-CSz+*6p&R)%W5i{C<}yz>w6~;H<3o#qSlUy_mq^09$<0B92Ei;o z_`5^4LSy;06=N`H;^$&`?^MtngrL~~8|_p(D&21y&VWdmudz=O>LeQE-n8i{kX8cj zl=4Wy_oWt}WEMf5qw$h76ja?2x4N?ppb#%#%r)YKd}#J=xU~^@vp6TWb%`>sX|bFu zeZ@FNB6BkB$sWo7>e%RE92+Y=b?^CDt~xR=lfgx05-g;&r?(nN=s_^8U z=WOl?Y47-FJcVp`zH~eJaRbDOtZPJA3TKp49Nj5{Ms#Hohq8hoh>>U{Dr)@JUdKqW zh!>O=RL1K{|7^TWhIq&fXMSJlmOn}^Ns4nCrzHeeP!B=M$dYx3pyud+!#6%5ZCrWI z*H$%9PK$i$W;hTAm9+TO87>IDIR^U!ZAxP8&gA;Xk#H(6{!!W-mgEc4JEsp}+8SVg&tz z*AlZRhEFG>NX6x(?wzq{JfGYkQNcg83M3}*v(}$uKYac@U4+wQcQe*up65BHqFrE|Ztam7N#q#Y=6w8~>;>a5iYB-~eJ^oa=x3^=6u4;35qZ xOme~AY>OZ?gF~s`(2TvBTB6XD{kY?I^3NVUm_s$Lt@*BFTM3mZA`uT|;D0B_J5T@s delta 981 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 DZ0oAq delta 5757 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 0 and current: + wrapped.append(current) + current = "" + if not current: + current = chunk + elif len(current) + 1 + len(chunk) <= max_columns: + current = f"{current} {chunk}" + else: + wrapped.append(current) + current = chunk + if current or not wrapped: + wrapped.append(current) + return wrapped + + +def _paginate_message(message, colours, max_lines=_MESSAGE_MAX_LINES): + pages = [] + page_lines = [] + page_colours = [] + source_lines = list(message) if message else [""] + source_colours = list(colours) if colours else [] + + for idx, line in enumerate(source_lines): + colour = source_colours[idx] if idx < len(source_colours) else (1, 1, 1) + for wrapped_line in _wrap_message_line(line): + if len(page_lines) >= max_lines: + pages.append((page_lines, page_colours)) + page_lines = [] + page_colours = [] + page_lines.append(wrapped_line) + page_colours.append(colour) + + if page_lines or not pages: + pages.append((page_lines, page_colours)) + + return pages + + +def _startup_warning_message(warning): + title = _HEXPANSIONS_JSON + body = warning + if warning.startswith("hexpansion_mgr import failed:"): + detail = warning.split(":", 1)[1].strip() + title = "hexpansion_mgr" + body = "import failed: " + detail + elif warning.startswith(_HEXPANSIONS_JSON): + body = warning[len(_HEXPANSIONS_JSON):].strip(": ") + else: + title = "HexManager" + return [title, "warning:", body], [(1, 0.6, 0)] * 3 + + def _load_hexpansion_types(app_file_path, json_path=None): """Load hexpansion type definitions from hexpansions.json next to this app file. @@ -100,8 +171,9 @@ def _load_hexpansion_types(app_file_path, json_path=None): types_list = [] warnings = [] if HexpansionType is None: - warnings.append("hexpansion type support unavailable") - print("H:Warning: hexpansion type support unavailable (HexpansionType import failed)") + reason = _IMPORT_ERRORS.get("hexpansion_mgr", "HexpansionType import failed") + warnings.append(f"hexpansion_mgr import failed: {reason}") + print(f"H:Warning: hexpansion_mgr import failed ({reason})") return types_list, warnings if json_path is None: last_slash = max(app_file_path.rfind("/"), app_file_path.rfind("\\")) @@ -175,6 +247,8 @@ def __init__(self): self.message: list = [] self.message_colours: list = [] self.message_type: str | None = None + self._message_pages: list[tuple[list[str], list[tuple[float, float, float]]]] = [] + self._message_page_index: int = 0 self.current_menu: str | None = None self.menu: Menu | None = None self.scroll_mode_enabled: bool = False # Whether pressing the "C" button can toggle scroll mode on/off, which allows the user to scroll through lines on the display. @@ -382,10 +456,9 @@ def _update_main_application(self, delta: int): # Show any startup warnings once (e.g. hexpansions.json not found or parse error) if self._startup_warnings and self.current_state != STATE_MESSAGE: w = self._startup_warnings[0] - if w.startswith(_HEXPANSIONS_JSON): - w = w[len(_HEXPANSIONS_JSON):].strip(": ") self._startup_warning_active = True - self.show_message([_HEXPANSIONS_JSON, "warning:", w], [(1, 0.6, 0)] * 3, msg_type="warning") + msg_content, msg_colours = _startup_warning_message(w) + self.show_message(msg_content, msg_colours, msg_type="warning") return if self.current_state == STATE_MENU: if self.current_menu is None: @@ -430,6 +503,18 @@ def _update_main_application(self, delta: int): def _update_state_message(self, delta: int): # pylint: disable=unused-argument + if len(self._message_pages) > 1 and self.button_states.get(BUTTON_TYPES["UP"]): + self.button_states.clear() + if self._message_page_index > 0: + self._set_message_page(self._message_page_index - 1) + self.refresh = True + return + if len(self._message_pages) > 1 and self.button_states.get(BUTTON_TYPES["DOWN"]): + self.button_states.clear() + if self._message_page_index < len(self._message_pages) - 1: + self._set_message_page(self._message_page_index + 1) + self.refresh = True + return if self.button_states.get(BUTTON_TYPES["CONFIRM"]): if self.message_type == "reboop": self.button_states.clear() @@ -456,6 +541,8 @@ def _update_state_message(self, delta: int): # pylint: disable=unused-argum self.message = [] self.message_colours = [] self.message_type = None + self._message_pages = [] + self._message_page_index = 0 def scroll_mode_enable(self, enable: bool): @@ -514,7 +601,9 @@ def draw(self, ctx): self.message_colours = [(1,0,0)]*len(self.message) self.draw_message(ctx, self.message, self.message_colours, label_font_size) if self.message_type is None or self.message_type == "warning" or self.message_type == "hexpansion" or self.message_type == "serialise": - button_labels(ctx, confirm_label="OK", cancel_label="Exit") + up_label = "Prev" if self._message_page_index > 0 else "" + down_label = "Next" if self._message_page_index < len(self._message_pages) - 1 else "" + button_labels(ctx, confirm_label="OK", cancel_label="Exit", up_label=up_label, down_label=down_label) else: # Delegate to functional area managers via dispatch table if self.current_state in self._state_draw_dispatch: @@ -561,12 +650,21 @@ def return_to_menu(self, menu_name: str | None = None): self.refresh = True + def _set_message_page(self, index: int): + self._message_page_index = index + if not self._message_pages: + self.message = [] + self.message_colours = [] + return + self.message, self.message_colours = self._message_pages[index] + + def show_message(self, msg_content, msg_colours, msg_type = None): """Utility function to set the current state to the message display, and populate the message content and colours. The message_type can be used to indicate whether this is an 'error' (red) or 'warning' (green) message, which can affect both the display and the behaviour when the user acknowledges the message.""" if self.logging: print(f"Showing message: '{msg_content}' with type {msg_type}") - self.message = msg_content - self.message_colours = msg_colours + self._message_pages = _paginate_message(msg_content, msg_colours) + self._set_message_page(0) self.message_type = msg_type self.current_state = STATE_MESSAGE self.refresh = True diff --git a/hexpansion_mgr.mpy b/hexpansion_mgr.mpy index 961757160f0c17133b56235f4c4be00a7f35fcf6..f19814616d0dbc958781e699a206451f09a4847a 100644 GIT binary patch literal 21618 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 diff --git a/hexpansion_mgr.py b/hexpansion_mgr.py index 67e3dde..5c799d5 100644 --- a/hexpansion_mgr.py +++ b/hexpansion_mgr.py @@ -17,12 +17,13 @@ import vfs from app_components.notification import Notification from app_components.tokens import label_font_size, button_labels +from eeprom_i2c import EEPROM +from eeprom_partition import EEPROMPartition from events.input import BUTTON_TYPES from machine import I2C from system.eventbus import eventbus from system.hexpansion.events import HexpansionInsertionEvent from system.hexpansion.header import HexpansionHeader, write_header -from system.hexpansion.util import get_hexpansion_block_devices, detect_eeprom_addr from system.scheduler import scheduler _NUM_HEXPANSION_SLOTS = 6 @@ -44,8 +45,9 @@ _SUB_UPGRADE_CONFIRM = 5 # Hexpansion ready for App upgrade _SUB_PROGRAMMING = 6 # Hexpansion EEPROM programming (Initialsation or Upgrade) in progress _SUB_PORT_SELECT = 7 # User selecting which hexpansion to erase (if multiple) in order to free up a slot for initialisation or upgrade -_SUB_DONE = 8 # Final state after successful initialisation or upgrade, before returning to menu -_SUB_EXIT = 9 # State for exiting from interactive mode back to menu) +_SUB_SCANNING = 8 # Manual blank-EEPROM scan in progress after showing a wait screen +_SUB_DONE = 9 # Final state after successful initialisation or upgrade, before returning to menu +_SUB_EXIT = 10 # State for exiting from interactive mode back to menu) # EEPROM app programming outcomes @@ -69,6 +71,54 @@ def init_settings(s, MySetting): # pylint: disable=unused-argument, inval return +def detect_eeprom_addr(i2c): + devices = i2c.scan() + if 0x57 in devices and 0x50 not in devices: + return (0x57, 2) + if ( + 0x57 in devices + and 0x56 in devices + and 0x55 in devices + and 0x54 in devices + and 0x53 in devices + and 0x52 in devices + and 0x51 in devices + and 0x50 in devices + ): + return (0x50, 1) + if 0x50 in devices: + return (0x50, 2) + return (None, None) + + +def get_hexpansion_block_devices(i2c, header, addr=0x50, addr_len=2): + if header.eeprom_total_size > 2 ** (8 * addr_len): + chip_size = 2 ** (8 * addr_len) + else: + chip_size = header.eeprom_total_size + if header.eeprom_total_size >= 8192: + block_size = 9 + else: + block_size = 6 + eep = EEPROM( + i2c=i2c, + chip_size=chip_size, + page_size=header.eeprom_page_size, + block_size=block_size, + addrsize=addr_len * 8, + ) + partition = EEPROMPartition( + eep=eep, + offset=header.fs_offset, + length=header.eeprom_total_size - header.fs_offset, + ) + print("eeprom block count:", eep.ioctl(4, None)) + print("partition block count:", partition.ioctl(4, None)) + print("partition block size:", partition.ioctl(5, None)) + + return eep, partition + + # ---- Hexpansion management ------------------------------------------------- class HexpansionMgr: @@ -131,7 +181,9 @@ def __init__(self, app, logging: bool = False): self._waiting_app_port: int | None = None self._erase_port: int | None = None self._upgrade_port: int | None = None + self._scan_port: int | None = None self._ports_to_initialise: set[int] = set() # ports with blank EEPROM which could be initialised + self._ports_initialise_declined: set[int] = set() # blank EEPROMs the user already declined to initialise self._ports_to_check_app: set[int] = set() # ports with recognised hexpansion type which should be checked for the correct app before being used self._reboop_required: bool = False self._hexpansion_serial_number: int | None = None @@ -377,6 +429,7 @@ def refresh_slot_records(self): self._waiting_app_port = None self._erase_port = None self._upgrade_port = None + self._scan_port = None self._hexpansion_app_startup_timer = 0 self._hexpansion_type_by_slot = [None] * _NUM_HEXPANSION_SLOTS self._hexpansion_state_by_slot = [self.HEXPANSION_STATE_UNKNOWN] * _NUM_HEXPANSION_SLOTS @@ -408,14 +461,19 @@ async def _handle_removal(self, event): self._hexpansion_eeprom_addr_len[port - 1] = None self._hexpansion_eeprom_addr[port - 1] = None self._clear_eeprom_geometry(port) + self._ports_initialise_declined.discard(port) + scanning_port_removed = self._scan_port == port if port in self._ports_to_initialise: self._ports_to_initialise.remove(port) self._ports_to_check_app.discard(port) + if scanning_port_removed: + self._scan_port = None if (self._detected_port is not None and port == self._detected_port) or \ (self._upgrade_port is not None and port == self._upgrade_port) or \ (self._waiting_app_port is not None and port == self._waiting_app_port) or \ (self._erase_port is not None and port == self._erase_port) or \ + scanning_port_removed or \ (self._port_selected != 0 and port == self._port_selected): # The port from which a hexpansion has been removed is significant app.hexpansion_update_required = True @@ -426,6 +484,7 @@ async def _handle_removal(self, event): async def _handle_insertion(self, event): if self._app.serialise_active: return + self._ports_initialise_declined.discard(event.port) self._hexpansion_eeprom_addr_len[event.port - 1] = None self._hexpansion_eeprom_addr[event.port - 1] = None self._clear_eeprom_geometry(event.port) @@ -553,6 +612,8 @@ def update(self, delta) -> bool: self._update_state_upgrade(delta) elif self._sub_state == _SUB_PROGRAMMING: self._update_state_programming(delta) + elif self._sub_state == _SUB_SCANNING: + self._update_state_scanning(delta) elif self._sub_state == _SUB_PORT_SELECT: self._update_state_port_select(delta) elif self._sub_state == _SUB_CHECK: @@ -661,6 +722,9 @@ def _update_state_detected(self, delta): # pylint: disable=unused-argumen app.button_states.clear() if self._logging: print("H:Initialise Cancelled") + if self._detected_port is not None: + self._ports_initialise_declined.add(self._detected_port) + self._ports_to_initialise.discard(self._detected_port) self._detected_port = None self._sub_state = _SUB_INIT if self._mode == _MODE_INIT else _SUB_CHECK elif app.button_states.get(BUTTON_TYPES["UP"]): @@ -821,16 +885,8 @@ def _update_state_port_select(self, delta: int): # pylint: disable=unused-argu app = self._app if app.button_states.get(BUTTON_TYPES["RIGHT"]): app.button_states.clear() - if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK and not self._has_eeprom_geometry(self._port_selected): - total_size, page_size = self._detect_eeprom_geometry(self._port_selected) - if total_size is not None and page_size is not None: - app.notification = Notification("Scanned", port=self._port_selected) - else: - app.notification = Notification("Failed", port=self._port_selected) - self._read_port_header(self._port_selected) - else: - self._port_selected = (self._port_selected % _NUM_HEXPANSION_SLOTS) + 1 - self._read_port_header(self._port_selected) + self._port_selected = (self._port_selected % _NUM_HEXPANSION_SLOTS) + 1 + self._read_port_header(self._port_selected) app.refresh = True elif app.button_states.get(BUTTON_TYPES["LEFT"]): app.button_states.clear() @@ -843,6 +899,7 @@ def _update_state_port_select(self, delta: int): # pylint: disable=unused-argu if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK: # The selected port has a blank EEPROM, so we can initialise it without erasing first. self._detected_port = self._port_selected + self._ports_initialise_declined.discard(self._detected_port) app.notification = Notification("Init?", port=self._detected_port) self._sub_state = _SUB_DETECTED elif self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_RECOGNISED_OLD_APP: @@ -865,7 +922,10 @@ def _update_state_port_select(self, delta: int): # pylint: disable=unused-argu app.refresh = True elif app.button_states.get(BUTTON_TYPES["DOWN"]): app.button_states.clear() - if self._port_detail_page_count > 1: + if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK and not self._has_eeprom_geometry(self._port_selected): + self._scan_port = self._port_selected + self._sub_state = _SUB_SCANNING + elif self._port_detail_page_count > 1: self._port_detail_page = (self._port_detail_page + 1) % self._port_detail_page_count app.refresh = True elif app.button_states.get(BUTTON_TYPES["CANCEL"]): @@ -873,6 +933,23 @@ def _update_state_port_select(self, delta: int): # pylint: disable=unused-argu self._sub_state = _SUB_EXIT + def _update_state_scanning(self, delta: int): # pylint: disable=unused-argument + app = self._app + scan_port = self._scan_port + if scan_port is None: + self._sub_state = _SUB_PORT_SELECT if self._mode == _MODE_INTERACTIVE else _SUB_CHECK + return + total_size, page_size = self._detect_eeprom_geometry(scan_port) + if total_size is not None and page_size is not None: + app.notification = Notification("Scanned", port=scan_port) + else: + app.notification = Notification("Failed", port=scan_port) + self._read_port_header(scan_port) + self._scan_port = None + self._sub_state = _SUB_PORT_SELECT if self._mode == _MODE_INTERACTIVE else _SUB_CHECK + app.refresh = True + + # ------------------------------------------------------------------ # Draw hexpansion-related states # ------------------------------------------------------------------ @@ -903,6 +980,10 @@ def draw(self, ctx) -> bool: elif self._sub_state == _SUB_PORT_SELECT: self._draw_port_select(ctx) return True + elif self._sub_state == _SUB_SCANNING: + scan_port = self._scan_port if self._scan_port is not None else self._port_selected + app.draw_message(ctx, [f"Slot {scan_port}", "Blank EEPROM", "Scanning...", "Please wait"], [(1, 1, 0), (1, 0, 1), (0, 1, 1), (1, 1, 1)], label_font_size) + return True elif self._sub_state == _SUB_ERASE_CONFIRM: if self._erase_port is None: return False @@ -1020,9 +1101,11 @@ def _draw_port_select(self, ctx): confirm_label = "Init" if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK else \ "Upgrade" if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_RECOGNISED_OLD_APP else \ "Erase" if self._hexpansion_state_by_slot[self._port_selected - 1] >= self.HEXPANSION_STATE_FAULTY else "" - right_label = "Scan" if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK and not self._has_eeprom_geometry(self._port_selected) else "Slot>" - button_labels(ctx, confirm_label=confirm_label, left_label=" bool: hexpansion_header = self._read_header(port) except OSError: # OSError just means there is no hexpansion EEPROM on this port + self._ports_initialise_declined.discard(port) self._hexpansion_type_by_slot[port - 1] = None self._hexpansion_state_by_slot[port - 1] = self.HEXPANSION_STATE_EMPTY return False @@ -1096,16 +1180,19 @@ def _check_port_for_known_hexpansions(self, port) -> bool: if self._logging: print(f"H:Found EEPROM on port {port}") self._hexpansion_state_by_slot[port - 1] = self.HEXPANSION_STATE_BLANK - self._ports_to_initialise.add(port) + if port not in self._ports_initialise_declined: + self._ports_to_initialise.add(port) return True except Exception as e: # pylint: disable=broad-except print(f"H:Error reading header on port {port}: {e}") return False if hexpansion_header is None: print(f"H:Error reading header on port {port}") + self._ports_initialise_declined.discard(port) self._hexpansion_type_by_slot[port - 1] = None self._hexpansion_state_by_slot[port - 1] = self.HEXPANSION_STATE_EMPTY return False + self._ports_initialise_declined.discard(port) for index, hexpansion_type in enumerate(app.HEXPANSION_TYPES): if hexpansion_header.vid == hexpansion_type.vid and hexpansion_header.pid == hexpansion_type.pid: self._hexpansion_type_by_slot[port - 1] = index @@ -1426,8 +1513,11 @@ def _check_ports_to_initialise(self, delta) -> bool: # pylint: disable=unu """Check for hexpansion presence in any ports, and if a new hexpansion with a blank EEPROM is detected, prompt the user to initialise it. Returns True if we are now in the initialise confirmation state, False otherwise.""" app = self._app - if 0 < len(self._ports_to_initialise) and self._detected_port is None: - self._detected_port = self._ports_to_initialise.pop() + while self._ports_to_initialise and self._detected_port is None: + port = self._ports_to_initialise.pop() + if port in self._ports_initialise_declined: + continue + self._detected_port = port app.notification = Notification("Initialise?", port=self._detected_port) self._sub_state = _SUB_DETECTED return True diff --git a/tests/test_serialise.py b/tests/test_serialise.py index 760ad2e..ac288bf 100644 --- a/tests/test_serialise.py +++ b/tests/test_serialise.py @@ -317,7 +317,7 @@ def test_prepare_eeprom_uses_detected_geometry_for_header(hexmanager_app, monkey def test_blank_port_scan_button_and_geometry_details(hexmanager_app, monkeypatch): from events.input import BUTTON_TYPES from sim.apps.HexManager import hexpansion_mgr as hexpansion_module - from sim.apps.HexManager.hexpansion_mgr import _SUB_PORT_SELECT + from sim.apps.HexManager.hexpansion_mgr import _SUB_PORT_SELECT, _SUB_SCANNING app = hexmanager_app helper = app._hexpansion_mgr @@ -336,7 +336,8 @@ def test_blank_port_scan_button_and_geometry_details(hexmanager_app, monkeypatch helper._draw_port_select(None) assert 'Size: Unknown' in rendered['lines'] assert 'Page: Unknown' in rendered['lines'] - assert labels['right_label'] == 'Scan' + assert labels['down_label'] == 'Scan' + assert labels['right_label'] == 'Slot>' scan_calls = [] @@ -349,10 +350,22 @@ def fake_detect(port, force=False): monkeypatch.setattr(helper, '_detect_eeprom_geometry', fake_detect) monkeypatch.setattr(helper, '_read_port_header', lambda port: None) - app.button_states.press('RIGHT') + app.button_states.press('DOWN') helper._update_state_port_select(0) + assert helper._sub_state == _SUB_SCANNING + assert helper._scan_port == 1 + assert helper._port_selected == 1 + + rendered.clear() + helper.draw(None) + assert 'Scanning...' in rendered['lines'] + + helper._update_state_scanning(0) + assert scan_calls == [(1, False)] + assert helper._sub_state == _SUB_PORT_SELECT + assert helper._scan_port is None assert helper._port_selected == 1 rendered.clear() @@ -363,6 +376,62 @@ def fake_detect(port, force=False): assert labels['right_label'] == 'Slot>' +def test_right_button_keeps_slot_navigation_when_blank_port_can_scan(hexmanager_app): + from events.input import BUTTON_TYPES + from sim.apps.HexManager.hexpansion_mgr import _SUB_PORT_SELECT + + app = hexmanager_app + helper = app._hexpansion_mgr + app.button_states = FakeButtons(BUTTON_TYPES) + helper._sub_state = _SUB_PORT_SELECT + helper._port_selected = 1 + helper._hexpansion_state_by_slot[0] = helper.HEXPANSION_STATE_BLANK + helper._update_detail_page_count() + + app.button_states.press('RIGHT') + helper._update_state_port_select(0) + + assert helper._port_selected == 2 + assert helper._sub_state == _SUB_PORT_SELECT + + +def test_declined_initialise_is_not_reprompted_during_init_rescan(hexmanager_app, monkeypatch): + from events.input import BUTTON_TYPES + from sim.apps.HexManager.hexpansion_mgr import _MODE_INIT, _SUB_DETECTED, _SUB_PORT_SELECT + + app = hexmanager_app + helper = app._hexpansion_mgr + app.button_states = FakeButtons(BUTTON_TYPES) + helper._mode = _MODE_INIT + helper._sub_state = _SUB_DETECTED + helper._detected_port = 3 + helper._hexpansion_state_by_slot[2] = helper.HEXPANSION_STATE_BLANK + + app.button_states.press('CANCEL') + helper._update_state_detected(0) + + assert helper._detected_port is None + assert 3 in helper._ports_initialise_declined + + def raise_blank(port, i2c=None): + raise RuntimeError('blank eeprom') + + monkeypatch.setattr(helper, '_read_header', raise_blank) + helper._ports_to_initialise.clear() + helper._check_port_for_known_hexpansions(3) + + assert 3 not in helper._ports_to_initialise + + helper._mode = 3 + helper._sub_state = _SUB_PORT_SELECT + helper._port_selected = 3 + app.button_states.press('CONFIRM') + helper._update_state_port_select(0) + + assert helper._detected_port == 3 + assert helper._sub_state == _SUB_DETECTED + + def test_hexpansion_events_are_suppressed_while_serialise_active(hexmanager_app, monkeypatch): app = hexmanager_app helper = app._hexpansion_mgr diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 4580320..28d2d60 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -53,6 +53,98 @@ def test_hexmanager_runtime_blocks_type_dependent_flows_when_type_load_fails(mon assert "parse error" in app.message[-1] assert app._startup_warnings == ["hexpansions.json parse error"] + +def test_load_hexpansion_types_reports_real_import_failure_reason(monkeypatch): + import sim.apps.HexManager.app as hexmanager_module + + monkeypatch.setattr(hexmanager_module, "HexpansionType", None) + monkeypatch.setitem( + hexmanager_module._IMPORT_ERRORS, + "hexpansion_mgr", + "can't import name read_hexpansion_header", + ) + + types, warnings = hexmanager_module._load_hexpansion_types("dummy/app.py") + + assert types == [] + assert warnings == [ + "hexpansion_mgr import failed: can't import name read_hexpansion_header" + ] + + +def test_startup_warning_lines_paginate_within_visible_five_lines(): + import sim.apps.HexManager.app as hexmanager_module + + candidate_warnings = [ + "hexpansions.json not found", + "hexpansions.json parse error", + "hexpansions.json load error", + "hexpansions.json: 'hexpansions' must be a list", + "hexpansions.json: no valid entries found", + "hexpansion_mgr import failed: can't import name read_hexpansion_header", + ] + + for warning in candidate_warnings: + msg_content, msg_colours = hexmanager_module._startup_warning_message(warning) + pages = hexmanager_module._paginate_message(msg_content, msg_colours) + assert pages, warning + for page_lines, page_colours in pages: + assert len(page_lines) <= hexmanager_module._MESSAGE_MAX_LINES, warning + assert len(page_lines) == len(page_colours), warning + + +def test_long_startup_warning_uses_pages_and_navigation(monkeypatch): + import sim.apps.HexManager.app as hexmanager_module + from events.input import BUTTON_TYPES + + class FakeButtons: + def __init__(self): + self._pressed = set() + + def press(self, *names): + self._pressed = {BUTTON_TYPES[name] for name in names} + + def get(self, button): + return button in self._pressed + + def clear(self): + self._pressed.clear() + + long_warning = ( + "hexpansion_mgr import failed: can't import name " + "read_hexpansion_header while importing system hexpansion support" + ) + + monkeypatch.setattr( + hexmanager_module, + "_load_hexpansion_types", + lambda app_file_path, json_path=None: ([], [long_warning]), + ) + + app = hexmanager_module.HexManagerApp() + app.button_states = FakeButtons() + app.current_state = hexmanager_module.STATE_SERIALISE + + app.update(0) + + assert app.message_type == "warning" + assert app.message[0] == "hexpansion_mgr" + assert len(app.message) <= hexmanager_module._MESSAGE_MAX_LINES + assert len(app._message_pages) > 1 + + first_page = list(app.message) + app.button_states.press("DOWN") + app.update(0) + + assert app.message != first_page + assert app._message_page_index == 1 + + app.button_states.press("UP") + app.update(0) + + assert app.message == first_page + assert app._message_page_index == 0 + def test_hexdrive_app_init(port): from sim.apps.HexManager.EEPROM.hexdrive import HexDriveApp config = HexpansionConfig(port) From 6b9d7b992293422ef4989c3f8fd9ccb34301c5b4 Mon Sep 17 00:00:00 2001 From: lincoltd7 <117526452+lincoltd7@users.noreply.github.com> Date: Sun, 10 May 2026 16:00:44 +0100 Subject: [PATCH 25/32] Separate display and header friendly names --- README.md | 3 ++- app.mpy | Bin 11358 -> 11411 bytes app.py | 1 + hexpansion_mgr.mpy | Bin 21618 -> 21668 bytes hexpansion_mgr.py | 15 ++++++++--- hexpansions.json | 5 ++-- tests/test_serialise.py | 54 ++++++++++++++++++++++++++++++++++++++++ tests/test_smoke.py | 19 ++++++++++++++ 8 files changed, 91 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4346b14..ea6217e 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ HexManager reads hexpansion type definitions from a JSON file named **`hexpansio | Field | Required | Default | Description | |---|---|---|---| | `pid` | ✅ | – | Product ID (0–65535). Must be unique within the same VID. | - | `name` | ✅ | – | Display name shown on screen ≤ 9 chars. | + | `name` | ✅ | – | Display name shown in HexManager. | + | `friendly_name` | No | `name` | Short name written into the EEPROM header and used by BadgeOS insertion notifications. Must fit within 9 chars. | | `vid` | No | "0xCAFE" | Vendor ID. | | `eeprom_total_size` | No | 8192 | EEPROM size in **bytes** (e.g. 2048, 8192, 32768, 65536). | | `eeprom_page_size` | No | 32 | Write page size in **bytes** – check your EEPROM datasheet (e.g. 16, 32, 64, 128). | diff --git a/app.mpy b/app.mpy index 68f9c282a62e265a53633c981b0ba68fef623f40..a372c758e2753616cec6264d6b241cb1249d190f 100644 GIT binary patch delta 5727 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 T57@W9wMgcPW)t59L+SqmcQFQ1 delta 5732 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 N*~eQgd>ss?{vUBH0apM3 diff --git a/app.py b/app.py index 86a0639..6e8bdc7 100644 --- a/app.py +++ b/app.py @@ -211,6 +211,7 @@ def _load_hexpansion_types(app_file_path, json_path=None): types_list.append(HexpansionType( pid=h["pid"], name=str(h["name"]), + friendly_name=str(h["friendly_name"]) if h.get("friendly_name") is not None else str(h["name"]), vid=h.get("vid", 0xCAFE), eeprom_total_size=h.get("eeprom_total_size", 8192), eeprom_page_size=h.get("eeprom_page_size", 32), diff --git a/hexpansion_mgr.mpy b/hexpansion_mgr.mpy index f19814616d0dbc958781e699a206451f09a4847a..81dd2e53c49a2c9609978bbe13e938d33a74c43e 100644 GIT binary patch delta 325 zcmeygf^o@8#tk187}st7q;Q{!F@EzUA>wzBk#YOx&;BfojF%?=2xw<~JGmh+g7M$xn}NoRj9Qa3QAcaM>A(`MlS7DmRilO+P%8DC6Z5E#Mu zbMwzYV@5{R$-2P~jE0l*f`b^%Cm#$}Wwf3AJXoH?$AC-zwReNiWWEr|Y%TU+V+Jwn zz+}eOid&7Xj8@i7PL0Wpj*UqgDvdskYK^6hX%CV#HB~h`ltTvl4Zjfk960+I| nW+ZO{@*IV%wt#pVLRMQrw8}Q1pwD(7p|%4^l self.HEXPANSION_STATE_BLANK and hexpansion_name: + page_title = hexpansion_name + elif hdr is not None and hdr.friendly_name: + page_title = hdr.friendly_name + else: + page_title = "Blank EEPROM" if self._hexpansion_state_by_slot[self._port_selected - 1] == self.HEXPANSION_STATE_BLANK else hexpansion_name lines = [f"Slot {self._port_selected}-{self._PAGE_NAMES[page]}", page_title] colours = [(1, 1, 0), (1, 0, 1)] if page == self._PAGE_VID_PID: @@ -1402,7 +1407,7 @@ def _prepare_eeprom(self, port, type_index: int | None = None, unique_id: int | vid=app.HEXPANSION_TYPES[selected_type].vid, pid=app.HEXPANSION_TYPES[selected_type].pid, unique_id=serial_number if serial_number is not None else 0, - friendly_name=app.HEXPANSION_TYPES[selected_type].name, + friendly_name=app.HEXPANSION_TYPES[selected_type].friendly_name, ) try: i2c = I2C(port) @@ -1616,7 +1621,9 @@ class HexpansionType: pid: the PID value to identify the hexpansion type from its EEPROM header. May be supplied as an int (``0xCBCA``) or a hex/decimal string (``"0xCBCA"``). - name: human-friendly name of the hexpansion type (e.g. "HexDrive") + name: human-friendly name of the hexpansion type shown in HexManager (e.g. "HexDrive") + friendly_name: short name written to the EEPROM header and used by BadgeOS notifications. + Defaults to ``name`` when omitted. vid: the VID value to identify the hexpansion type from its EEPROM header (default 0xCAFE). Accepts int or hex/decimal string. eeprom_page_size: EEPROM page size in bytes. Accepts int or hex/decimal string. @@ -1630,11 +1637,13 @@ class HexpansionType: def __init__(self, pid: int | str, name: str, vid: int | str = 0xCAFE, eeprom_page_size: int | str = _DEFAULT_EEPROM_PAGE_SIZE, eeprom_total_size: int | str = _DEFAULT_EEPROM_TOTAL_SIZE, + friendly_name: str | None = None, sub_type: str | None = None, app_mpy_name: str | None = None, app_mpy_version: str | None = None, app_name: str | None = None): self.vid: int = _parse_int(vid) self.pid: int = _parse_int(pid) self.name: str = name + self.friendly_name: str = name if friendly_name is None else friendly_name self.eeprom_page_size: int = _parse_int(eeprom_page_size) self.eeprom_total_size: int = _parse_int(eeprom_total_size) self.sub_type: str | None = sub_type diff --git a/hexpansions.json b/hexpansions.json index 88063ac..4534de3 100644 --- a/hexpansions.json +++ b/hexpansions.json @@ -10,7 +10,8 @@ ], "fields": { "pid": "REQUIRED. Product ID (16-bit integer), e.g. \"0xD15C\".", - "name": "REQUIRED. Human-friendly display name shown on screen, e.g. 'HexDrive'. MAXIMUM 9 chars.", + "name": "REQUIRED. Human-friendly display name shown in HexManager, e.g. 'HexDrive'.", + "friendly_name": "OPTIONAL. Short name written into the EEPROM header and shown by BadgeOS insertion notifications. Maximum 9 chars. Defaults to 'name'.", "sub_type": "OPTIONAL. Short label for this specific variant, e.g. '2 Motor'. Shown to the user alongside the name.", "vid": "OPTIONAL. Vendor ID (16-bit integer), e.g. \"0xCAFE\". Default 0xCAFE which is the UHB-IF uncontrolled VID.", "eeprom_total_size": "OPTIONAL. Total EEPROM size in BYTES. Default 8192 (8 KB).", @@ -66,7 +67,7 @@ "eeprom_total_size": 65536, "eeprom_page_size": 128 }, { - "pid": "0x5000", "vid": "0xCBCB", "name": "HexCurrent", + "pid": "0x5000", "vid": "0xCBCB", "name": "HexCurrent", "friendly_name": "HexCurent", "eeprom_total_size": 65536, "eeprom_page_size": 128, "app_mpy_name": "hexcurrent", "app_mpy_version": 1, "app_name": "HexCurrentApp" }, diff --git a/tests/test_serialise.py b/tests/test_serialise.py index ac288bf..37adc2f 100644 --- a/tests/test_serialise.py +++ b/tests/test_serialise.py @@ -311,9 +311,63 @@ def test_prepare_eeprom_uses_detected_geometry_for_header(hexmanager_app, monkey assert captured['header'].eeprom_total_size == 32768 assert captured['header'].eeprom_page_size == 64 assert captured['header'].fs_offset == 64 + assert captured['header'].friendly_name == helper._app.HEXPANSION_TYPES[0].friendly_name assert captured['page_size'] == 64 +def test_prepare_eeprom_uses_header_friendly_name_override(hexmanager_app, monkeypatch): + from types import SimpleNamespace + + from sim.apps.HexManager import hexpansion_mgr as hexpansion_module + + helper = hexmanager_app._hexpansion_mgr + helper._hexpansion_eeprom_addr_len[0] = 2 + helper._hexpansion_eeprom_addr[0] = 0x50 + helper._app.HEXPANSION_TYPES[0].name = 'HexCurrent' + helper._app.HEXPANSION_TYPES[0].friendly_name = 'HexCurent' + captured = {} + + monkeypatch.setattr(hexpansion_module, 'I2C', lambda port: object()) + monkeypatch.setattr(helper, '_detect_eeprom_geometry', lambda port, force=False: (32768, 64)) + monkeypatch.setattr(hexpansion_module, 'write_header', lambda port, header, addr=None, addr_len=None, page_size=None: captured.update({'header': header, 'addr': addr, 'addr_len': addr_len, 'page_size': page_size})) + monkeypatch.setattr(helper, '_read_header', lambda port, i2c=None: captured['header']) + monkeypatch.setattr(hexpansion_module, 'get_hexpansion_block_devices', lambda i2c, header, addr, addr_len=None: (None, object())) + monkeypatch.setattr(hexpansion_module.vfs, 'VfsLfs2', SimpleNamespace(mkfs=lambda partition: None), raising=False) + monkeypatch.setattr(hexpansion_module.vfs, 'mount', lambda partition, mountpoint, readonly=False: None, raising=False) + + assert helper._prepare_eeprom(1, type_index=0, unique_id=123) + assert captured['header'].friendly_name == 'HexCurent' + + +def test_port_detail_prefers_known_type_name_over_header_friendly_name(hexmanager_app, monkeypatch): + from sim.apps.HexManager import hexpansion_mgr as hexpansion_module + + app = hexmanager_app + helper = app._hexpansion_mgr + helper._port_selected = 1 + helper._hexpansion_type_by_slot[0] = 0 + helper._hexpansion_state_by_slot[0] = helper.HEXPANSION_STATE_RECOGNISED + helper._port_selected_header = SimpleNamespace( + friendly_name='HexCurent', + vid=0xCBCB, + pid=0x5000, + unique_id=123, + eeprom_total_size=65536, + eeprom_page_size=128, + ) + helper._port_detail_page = helper._PAGE_VID_PID + helper._port_detail_page_count = 2 + app.HEXPANSION_TYPES[0].name = 'HexCurrent' + + rendered = {} + monkeypatch.setattr(app, 'draw_message', lambda ctx, lines, colours, font: rendered.update({'lines': list(lines)})) + monkeypatch.setattr(hexpansion_module, 'button_labels', lambda ctx, **kwargs: None) + + helper._draw_port_select(None) + + assert rendered['lines'][1] == 'HexCurrent' + + def test_blank_port_scan_button_and_geometry_details(hexmanager_app, monkeypatch): from events.input import BUTTON_TYPES from sim.apps.HexManager import hexpansion_mgr as hexpansion_module diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 28d2d60..cd655ab 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -363,6 +363,25 @@ def test_default_vid_used_when_omitted(self): types, warnings = self._load_from([{"pid": 1, "name": "NoVid"}]) assert not warnings assert types[0].vid == 0xCAFE + assert types[0].friendly_name == "NoVid" + + def test_friendly_name_defaults_to_name_when_omitted(self): + """friendly_name defaults to the display name when JSON omits it.""" + types, warnings = self._load_from([ + {"pid": 1, "name": "HexCurrent"} + ]) + assert not warnings + assert types[0].name == "HexCurrent" + assert types[0].friendly_name == "HexCurrent" + + def test_friendly_name_override_is_loaded_separately(self): + """friendly_name may differ from the HexManager display name.""" + types, warnings = self._load_from([ + {"pid": 1, "name": "HexCurrent", "friendly_name": "HexCurent"} + ]) + assert not warnings + assert types[0].name == "HexCurrent" + assert types[0].friendly_name == "HexCurent" # ------------------------------------------------------------------ # Quoted hex strings for PID From a62b45243ec77322c99554d53d73fd9fd952abfe Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 10 May 2026 16:22:57 +0100 Subject: [PATCH 26/32] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/test_smoke.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/test_smoke.py b/tests/test_smoke.py index cd655ab..bbd6752 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -170,14 +170,25 @@ def extract_version(path: Path) -> int: with open(json_path) as f: data = json.load(f) + repo_root = Path(__file__).resolve().parents[1] + hexdrive2_path = repo_root / "vendor" / "HexDrive2" / "hexdrive2.py" + hexcurrent_path = repo_root / "vendor" / "HexCurrent" / "hexcurrent.py" + + missing_vendored_sources = [ + str(path.relative_to(repo_root)) + for path in (hexdrive2_path, hexcurrent_path) + if not path.exists() + ] + if missing_vendored_sources: + pytest.skip( + "Vendored app sources are unavailable; initialize submodules to run this " + f"test: {', '.join(missing_vendored_sources)}" + ) + expected_versions = { "hexdrive": HexDriveApp.VERSION, - "hexdrive2": extract_version( - Path(__file__).resolve().parents[1] / "vendor" / "HexDrive2" / "hexdrive2.py" - ), - "hexcurrent": extract_version( - Path(__file__).resolve().parents[1] / "vendor" / "HexCurrent" / "hexcurrent.py" - ), + "hexdrive2": extract_version(hexdrive2_path), + "hexcurrent": extract_version(hexcurrent_path), } versioned_entries = [ From 706d5cd0f9e393835cd183ac970b3ef0ab0dd8cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 15:23:24 +0000 Subject: [PATCH 27/32] fix: refresh release file set after mpy generation Agent-Logs-Url: https://github.com/Robotmad/HexManager/sessions/3dbe6bc7-1ce8-409b-b5d5-2f9980a30c63 Co-authored-by: Robotmad <3315650+Robotmad@users.noreply.github.com> --- dev/build_release.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev/build_release.py b/dev/build_release.py index 397fe60..8ab2f41 100644 --- a/dev/build_release.py +++ b/dev/build_release.py @@ -77,8 +77,6 @@ def find_files(top_level_dir): parser.add_argument("-f", "--force", action="store_true", help="Skip confirmation prompt before file removal.") options = parser.parse_args() force_mode = options.force - found_files = set(find_files(".")) - for file in files_to_mpy: print(f"Mpy-ing file: {file}") mpy_cross.run(file, "-v") @@ -88,6 +86,7 @@ def find_files(top_level_dir): spec.artifact.parent.mkdir(parents=True, exist_ok=True) mpy_cross.run(str(spec.source), "-v", "-o", str(spec.artifact)) + found_files = set(find_files(".")) if not files_to_keep.issubset(found_files): raise FileNotFoundError(f"Some of {files_to_keep} are not found so assuming wrong directory. " "Please run this script from HexManager dir.") From 10e99f9b4d5d8e5f621ef55c41a48e34f2179adc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 10 May 2026 15:25:46 +0000 Subject: [PATCH 28/32] fix: init submodules in release workflow and remove debug prints Agent-Logs-Url: https://github.com/Robotmad/HexManager/sessions/4248c750-a1c6-45b3-85ff-1a3980584125 Co-authored-by: Robotmad <3315650+Robotmad@users.noreply.github.com> --- .github/workflows/release.yml | 2 ++ hexpansion_mgr.py | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21bcd4a..c1584aa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: recursive - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/hexpansion_mgr.py b/hexpansion_mgr.py index 9ae53eb..3253580 100644 --- a/hexpansion_mgr.py +++ b/hexpansion_mgr.py @@ -112,10 +112,6 @@ def get_hexpansion_block_devices(i2c, header, addr=0x50, addr_len=2): offset=header.fs_offset, length=header.eeprom_total_size - header.fs_offset, ) - print("eeprom block count:", eep.ioctl(4, None)) - print("partition block count:", partition.ioctl(4, None)) - print("partition block size:", partition.ioctl(5, None)) - return eep, partition From bd15dab85aaae9a56cb406ab0eb19661ca23276a Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 10 May 2026 16:26:31 +0100 Subject: [PATCH 29/32] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- hexpansion_mgr.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hexpansion_mgr.py b/hexpansion_mgr.py index 3253580..e107e14 100644 --- a/hexpansion_mgr.py +++ b/hexpansion_mgr.py @@ -237,8 +237,7 @@ def _write_eeprom_bytes(cls, i2c, addr: int, addr_len: int, mem_addr: int, data: return except OSError: pass - finally: - time.sleep_ms(1) + time.sleep_ms(1) @classmethod def _read_eeprom_bytes(cls, i2c, addr: int, addr_len: int, mem_addr: int, size: int) -> bytes: From a795bbc75fab879a6cfcdca4e99726e2f6433fe4 Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 10 May 2026 16:45:26 +0100 Subject: [PATCH 30/32] Simplify submodule management in tests workflow Removed checkout command for HexManager submodule. --- .github/workflows/tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d153f5b..4710488 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,9 +18,7 @@ jobs: repository: TeamRobotmad/badge-2024-software - name: Manage submodule run: | - git submodule update --init --force --remote - cd ./sim/apps/HexManager - git checkout ${{ github.head_ref || github.ref_name }} + git submodule update --init --force - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: From 0381d16ba50db08094616d253e7f15158e7d3a0d Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 10 May 2026 17:02:55 +0100 Subject: [PATCH 31/32] Update tests.yml to include badge repo checkout step Added a step to checkout the badge repository based on the PR branch. --- .github/workflows/tests.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 089529f..4d0d214 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,14 +9,20 @@ on: jobs: build: runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.10"] steps: + - name: Checkout badge repo matching PR branch when available + run: | + BRANCH="${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }}" + if git ls-remote --exit-code --heads https://github.com/TeamRobotmad/badge-2024-software.git "$BRANCH"; then + echo "REF=$BRANCH" >> "$GITHUB_ENV" + else + echo "REF=" >> "$GITHUB_ENV" + fi + - uses: actions/checkout@v4 with: repository: TeamRobotmad/badge-2024-software - + ref: ${{ env.REF }} - name: Manage submodule run: | From e4f78e36be85f69ac4f40617f5a47f3d1b6e0a94 Mon Sep 17 00:00:00 2001 From: Christopher Barnes Date: Sun, 10 May 2026 17:08:47 +0100 Subject: [PATCH 32/32] Change Python setup to use version 3.11 Updated Python setup step to use a fixed version 3.11. --- .github/workflows/tests.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d0d214..ccc0ed9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,10 +27,16 @@ jobs: - name: Manage submodule run: | git submodule update --init --force - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} + #- name: Set up Python ${{ matrix.python-version }} + # uses: actions/setup-python@v3 + # with: + # python-version: ${{ matrix.python-version }} + + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Install dependencies run: | python -m pip install --upgrade pip