From 0ca2832bbcbd6e0bbd14268f544164142194353b Mon Sep 17 00:00:00 2001 From: Alex J Lennon Date: Sat, 8 Aug 2026 18:03:57 +0100 Subject: [PATCH] feat(machine): add imx8mm-jaguar-screen (INST clone) Pin bsp/distro branches that introduce the SCREEN machine, add improv machine overrides, and list SCREEN in kas/fio helper scripts. Co-authored-by: Cursor --- meta-dynamicdevices-bsp | 2 +- meta-dynamicdevices-distro | 2 +- .../python3-improv/MACHINE_SPECIFIC_FILES.md | 2 + .../imx8mm-jaguar-screen/improv.service | 17 ++ .../imx8mm-jaguar-screen/onboarding-server.py | 284 ++++++++++++++++++ scripts/add-additional-labels.sh | 1 + scripts/fio-program-board.bat | 2 +- scripts/fio-program-board.sh | 14 +- scripts/kas-build-base-enhanced.sh | 3 +- scripts/kas-build-profiling.sh | 3 +- 10 files changed, 320 insertions(+), 10 deletions(-) create mode 100644 recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/improv.service create mode 100644 recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/onboarding-server.py diff --git a/meta-dynamicdevices-bsp b/meta-dynamicdevices-bsp index 869a3db5..4e024250 160000 --- a/meta-dynamicdevices-bsp +++ b/meta-dynamicdevices-bsp @@ -1 +1 @@ -Subproject commit 869a3db5b729efad77c13a9117f5fa5a4b91d4ab +Subproject commit 4e02425013cf86d0e654b247f0c52d95e010f123 diff --git a/meta-dynamicdevices-distro b/meta-dynamicdevices-distro index d0b555d4..cf382750 160000 --- a/meta-dynamicdevices-distro +++ b/meta-dynamicdevices-distro @@ -1 +1 @@ -Subproject commit d0b555d4d77c3526459d2cdd76897eb16cc51823 +Subproject commit cf38275005adae036435d51b549532535a3bff24 diff --git a/recipes-devtools/python/python3-improv/MACHINE_SPECIFIC_FILES.md b/recipes-devtools/python/python3-improv/MACHINE_SPECIFIC_FILES.md index 4fde5017..ad1a8290 100644 --- a/recipes-devtools/python/python3-improv/MACHINE_SPECIFIC_FILES.md +++ b/recipes-devtools/python/python3-improv/MACHINE_SPECIFIC_FILES.md @@ -80,6 +80,7 @@ recipes-devtools/python/python3-improv/ │ ├── improv.service │ └── onboarding-server.py └── imx8mm-jaguar-inst/ (machine override - same filenames) +└── imx8mm-jaguar-screen/ (machine override - same filenames) ├── improv.service └── onboarding-server.py ``` @@ -88,6 +89,7 @@ recipes-devtools/python/python3-improv/ - Yocto picks up `improv.service` and `onboarding-server.py` from `${MACHINE}/` when present; otherwise uses recipe root. - `imx93-jaguar-eink`: Uses files from `imx93-jaguar-eink/` (eink-XXXX BLE name, improv-eink connection). - `imx8mm-jaguar-inst`: Uses files from `imx8mm-jaguar-inst/` (Improv-Inst BLE, improv-inst connection). +- `imx8mm-jaguar-screen`: Uses files from `imx8mm-jaguar-screen/` (Improv-Screen BLE, improv-screen connection). - All other machines: Use files from recipe directory root. - Recipe has no machine-specific SRC_URI, do_install, or SYSTEMD_SERVICE logic. diff --git a/recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/improv.service b/recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/improv.service new file mode 100644 index 00000000..9b8e6fda --- /dev/null +++ b/recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/improv.service @@ -0,0 +1,17 @@ +[Unit] +Description=Improv BLE WiFi onboarding service for imx8mm-jaguar-screen (SCREEN) +Documentation=https://www.improv-wifi.com/ble/ +After=bluetooth.target +Wants=bluetooth.target + +[Service] +Type=simple +ExecStart=/usr/share/improv/onboarding-server.py +Restart=always +RestartSec=12 +Environment="IMPROV_WIFI_INTERFACE=wlan0" +Environment="IMPROV_SERVICE_NAME=Improv-Screen" +Environment="IMPROV_CONNECTION_NAME=improv-screen" + +[Install] +WantedBy=default.target diff --git a/recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/onboarding-server.py b/recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/onboarding-server.py new file mode 100644 index 00000000..91efffd3 --- /dev/null +++ b/recipes-devtools/python/python3-improv/imx8mm-jaguar-screen/onboarding-server.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Custom Improv onboarding server for imx8mm-jaguar-screen (SCREEN board) +# Based on onboarding-server.py but with board-specific customizations +# + +from improv import * +from bless import ( # type: ignore + BlessServer, + BlessGATTCharacteristic, + GATTCharacteristicProperties, + GATTAttributePermissions +) +from bless.backends.bluezdbus.server import BlessServerBlueZDBus +from typing import Any, Dict, Union, Optional +import sys +import threading +import asyncio +import logging +import uuid +import nmcli +import subprocess +import os +import re + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(name=__name__) + +# NOTE: Some systems require different synchronization methods. +trigger: Union[asyncio.Event, threading.Event] +if sys.platform in ["darwin", "win32"]: + trigger = threading.Event() +else: + trigger = asyncio.Event() + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(name=__name__) + + +def build_gatt(): + gatt: Dict = { + ImprovUUID.SERVICE_UUID.value: { + ImprovUUID.STATUS_UUID.value: { + "Properties": (GATTCharacteristicProperties.read | + GATTCharacteristicProperties.notify), + "Permissions": (GATTAttributePermissions.readable | + GATTAttributePermissions.writeable) + }, + ImprovUUID.ERROR_UUID.value: { + "Properties": (GATTCharacteristicProperties.read | + GATTCharacteristicProperties.notify), + "Permissions": (GATTAttributePermissions.readable | + GATTAttributePermissions.writeable) + }, + ImprovUUID.RPC_COMMAND_UUID.value: { + "Properties": (GATTCharacteristicProperties.read | + GATTCharacteristicProperties.write | + GATTCharacteristicProperties.write_without_response), + "Permissions": (GATTAttributePermissions.readable | + GATTAttributePermissions.writeable) + }, + ImprovUUID.RPC_RESULT_UUID.value: { + "Properties": (GATTCharacteristicProperties.read | + GATTCharacteristicProperties.notify), + "Permissions": (GATTAttributePermissions.readable) + }, + ImprovUUID.CAPABILITIES_UUID.value: { + "Properties": (GATTCharacteristicProperties.read), + "Permissions": (GATTAttributePermissions.readable) + }, + } + } + return gatt + +""" + Names longer than 10 characters will result in bless + only advertising the name without the UUIDs on macOS, + leading to a break with the Improv spec: + + Bluetooth LE Advertisement +The device MUST advertise the Service UUID. +""" + +# Board-specific configuration for imx8mm-jaguar-screen (overridable via environment) +SERVER_HOST = os.getenv("IMPROV_SERVER_HOST", "api.co.uk") +SERVICE_NAME = os.getenv("IMPROV_SERVICE_NAME", "Improv-Screen") +CON_NAME = os.getenv("IMPROV_CONNECTION_NAME", "improv-screen") +INTERFACE = os.getenv("IMPROV_WIFI_INTERFACE", "wlan0") +TIMEOUT = int(os.getenv("IMPROV_CONNECTION_TIMEOUT", "10000")) + +# Use new_event_loop() or get_event_loop() depending on Python version +# get_event_loop() is deprecated in Python 3.10+ but still works +try: + loop = asyncio.get_running_loop() +except RuntimeError: + # No running loop, create new one + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) +server = BlessServer(name=SERVICE_NAME, loop=loop) + +def wifi_connect(ssid: str, passwd: str) -> Optional[list[str]]: + logger.warning( + f"Creating Improv WiFi connection for '{ssid.decode('utf-8')}' with password: '{passwd.decode('utf-8')}'") + + try: + nmcli.connection.delete(f"{CON_NAME}") + except: + print(f'No connection {CON_NAME} to remove') + + try: + # Create connection with secrets stored in file (not agent-only) + # This prevents "no secrets" errors on headless systems when 4-way handshake fails + # + # ⚠️ CRITICAL: wifi-sec.psk-flags:'0' is REQUIRED for the NetworkManager patch + # (0001-wifi-dont-clear-secrets-if-stored-in-keyfile.patch) to work correctly. + # Without this, the patch will not activate and connections may fail permanently + # after 4-way handshake failures. + # See: meta-dynamicdevices-distro/recipes-connectivity/networkmanager/networkmanager/README_PATCH_REQUIREMENTS.md + nmcli.connection.add('wifi', { + 'ssid':ssid.decode('utf-8'), + 'wifi-sec.key-mgmt':'wpa-psk', + 'wifi-sec.psk':passwd.decode('utf-8'), + 'wifi-sec.psk-flags':'0', # REQUIRED: Store PSK in file, not agent-only (patch requirement) + 'connection.autoconnect':'yes', + 'connection.autoconnect-retries':'-1', # Retry connection indefinitely (-1 = unlimited) + 'connection.auth-retries':'-1', # Retry authentication indefinitely (-1 = unlimited) + 'connection.permissions':'' # Allow system-wide use + }, f"{INTERFACE}", f"{CON_NAME}", True) + logger.info(f"Successfully created WiFi connection {CON_NAME}") + + except Exception as e: + logger.error(f"Failed to create WiFi connection {CON_NAME}: {e}", exc_info=True) + print(f'Could not add new connection {CON_NAME}: {e}') + return None + + # CRITICAL: Explicitly add psk-flags=0 to connection file + connection_file = f"/etc/NetworkManager/system-connections/{CON_NAME}.nmconnection" + try: + if os.path.exists(connection_file): + with open(connection_file, 'r') as f: + content = f.read() + if 'psk-flags=0' not in content and 'psk-flags=0\n' not in content: + pattern = r'(\[wifi-security\]\n(?:[^\[]*\n)*?psk=[^\n]+\n)' + replacement = r'\1psk-flags=0\n' + new_content = re.sub(pattern, replacement, content) + if new_content == content: + pattern = r'(\[wifi-security\]\n)' + replacement = r'\1psk-flags=0\n' + new_content = re.sub(pattern, replacement, content) + if new_content != content: + with open(connection_file, 'w') as f: + f.write(new_content) + logger.info(f"Added psk-flags=0 to connection file {connection_file}") + else: + logger.warning(f"Connection file not found at {connection_file} - cannot add psk-flags=0") + except PermissionError as e: + try: + subprocess.run(['nmcli', 'connection', 'modify', f"{CON_NAME}", + '802-11-wireless-security.psk-flags', '0'], + check=True, capture_output=True, timeout=5) + except Exception as e2: + logger.warning(f"nmcli modify also failed: {e2}") + except Exception as e: + logger.warning(f"Unexpected error adding psk-flags=0 to file: {e}", exc_info=True) + + try: + subprocess.run(['nmcli', 'connection', 'reload'], + check=True, capture_output=True, timeout=5) + except Exception: + pass + + connection_file = f"/etc/NetworkManager/system-connections/{CON_NAME}.nmconnection" + try: + if os.path.exists(connection_file): + with open(connection_file, 'r') as f: + content = f.read() + if 'psk-flags=0' in content or 'psk-flags=0\n' in content: + logger.debug(f"Verified psk-flags=0 in connection file") + except Exception as e: + logger.debug(f"Could not verify connection file: {e}") + + try: + nmcli.connection.up(f"{CON_NAME}", TIMEOUT) + except: + print(f'Error bringing connection {CON_NAME} up') + return None + + dev_details = nmcli.device.show(f"{INTERFACE}") + if 'IP4.ADDRESS[1]' in dev_details.keys(): + dev_addr = dev_details['IP4.ADDRESS[1]'] + ip_addr = dev_addr.split('/')[0] + else: + print('Error connecting') + return None + + token = uuid.uuid4() + server = f"https://{SERVER_HOST}?ip_address={ip_addr}&token={token}" + return [server] + +# Improv chunks its RPC response into <= max_response_bytes packets. The library +# default (100) can be below the Wi-Fi-success redirect URL length (host + UUID +# token), which trips a pyImprov bug: it emits a spurious zero-length +# WIFI_SETTINGS packet *before* the packet carrying the URL. Clients that +# complete on the first result then never see the token. Raise the threshold so +# the URL is returned in a single packet (within the BLE MTU the app negotiates). +improv_server = ImprovProtocol(wifi_connect_callback=wifi_connect, + max_response_bytes=200) + +def read_request( + characteristic: BlessGATTCharacteristic, + **kwargs +) -> bytearray: + try: + improv_char = ImprovUUID(characteristic.uuid) + logger.info(f"Reading {improv_char} : {characteristic}") + except Exception: + logger.info(f"Reading {characteristic.uuid}") + pass + if characteristic.service_uuid == ImprovUUID.SERVICE_UUID.value: + return improv_server.handle_read(characteristic.uuid) + return characteristic.value + + +def write_request( + characteristic: BlessGATTCharacteristic, + value: bytearray, + **kwargs +): + + if characteristic.service_uuid == ImprovUUID.SERVICE_UUID.value: + (target_uuid, target_values) = improv_server.handle_write( + characteristic.uuid, value) + if target_uuid != None and target_values != None: + for value in target_values: + logger.debug( + f"Setting {ImprovUUID(target_uuid)} to {value}") + server.get_characteristic( + target_uuid, + ).value = value + success = server.update_value( + ImprovUUID.SERVICE_UUID.value, + target_uuid + ) + if not success: + logger.warning( + f"Updating characteristic return status={success}") + +async def run(loop): + + server.read_request_func = read_request + server.write_request_func = write_request + + if isinstance(server, BlessServerBlueZDBus): + await server.setup_task + interface = server.adapter.get_interface('org.bluez.Adapter1') + powered = await interface.get_powered() + if not powered: + logger.info("bluetooth device is not powered, powering now!") + await interface.set_powered(True) + + await server.add_gatt(build_gatt()) + await server.start() + + logger.info("Server started") + + try: + trigger.clear() + if trigger.__module__ == "threading": + trigger.wait() + else: + await trigger.wait() + except KeyboardInterrupt: + logger.debug("Shutting Down") + pass + await server.stop() + +# Actually start the server +try: + loop.run_until_complete(run(loop)) +except KeyboardInterrupt: + logger.debug("Shutting Down") + trigger.set() + pass diff --git a/scripts/add-additional-labels.sh b/scripts/add-additional-labels.sh index 346a0540..8fcafe02 100755 --- a/scripts/add-additional-labels.sh +++ b/scripts/add-additional-labels.sh @@ -31,6 +31,7 @@ gh api repos/DynamicDevices/meta-dynamicdevices/labels -f name="board: edge-eink gh api repos/DynamicDevices/meta-dynamicdevices/labels -f name="board: edge-ev" -f color="FF8C00" -f description="Edge EV Board (imx8mm-jaguar-phasora) specific issues" || echo "Label may already exist" +gh api repos/DynamicDevices/meta-dynamicdevices/labels -f name="board: screen" -f color="4169E1" -f description="SCREEN Board (imx8mm-jaguar-screen) specific issues" || echo "Label may already exist" gh api repos/DynamicDevices/meta-dynamicdevices/labels -f name="board: edge-gw" -f color="00CED1" -f description="Edge GW Board (imx8mm-jaguar-inst) specific issues" || echo "Label may already exist" # IMPACT/SCOPE LABELS - Help understand the breadth of impact diff --git a/scripts/fio-program-board.bat b/scripts/fio-program-board.bat index f36868b0..a02e36d8 100644 --- a/scripts/fio-program-board.bat +++ b/scripts/fio-program-board.bat @@ -57,7 +57,7 @@ set "DEFAULT_FACTORY=" set "DEFAULT_MACHINE=" REM Supported machines -set "SUPPORTED_MACHINES=imx8mm-jaguar-sentai imx93-jaguar-eink imx8mm-jaguar-phasora imx8mm-jaguar-inst imx93-11x11-lpddr4x-evk" +set "SUPPORTED_MACHINES=imx8mm-jaguar-sentai imx93-jaguar-eink imx8mm-jaguar-phasora imx8mm-jaguar-inst imx8mm-jaguar-screen imx93-11x11-lpddr4x-evk" REM Command line variables set "TARGET_NUMBER=" diff --git a/scripts/fio-program-board.sh b/scripts/fio-program-board.sh index a94d9f6e..d857ccc4 100755 --- a/scripts/fio-program-board.sh +++ b/scripts/fio-program-board.sh @@ -488,6 +488,7 @@ Supported Machines: - imx93-jaguar-eink (Edge EInk Board) - imx8mm-jaguar-phasora (Edge EV Board) - imx8mm-jaguar-inst (Edge GW Board) + - imx8mm-jaguar-screen (SCREEN Board) - imx93-11x11-lpddr4x-evk (NXP EVK) Examples: @@ -593,10 +594,11 @@ configure_interactive() { echo " 2) imx93-jaguar-eink (Edge EInk Board)" echo " 3) imx8mm-jaguar-phasora (Edge EV Board)" echo " 4) imx8mm-jaguar-inst (Edge GW Board)" - echo " 5) imx93-11x11-lpddr4x-evk (NXP EVK)" - echo " 6) Custom machine name" + echo " 5) imx8mm-jaguar-screen (SCREEN Board)" + echo " 6) imx93-11x11-lpddr4x-evk (NXP EVK)" + echo " 7) Custom machine name" echo - echo -n "Select default machine (1-6)" + echo -n "Select default machine (1-7)" if [[ -n "$DEFAULT_MACHINE" ]]; then echo -n " (current: $DEFAULT_MACHINE)" fi @@ -608,8 +610,9 @@ configure_interactive() { 2) machine="imx93-jaguar-eink" ;; 3) machine="imx8mm-jaguar-phasora" ;; 4) machine="imx8mm-jaguar-inst" ;; - 5) machine="imx93-11x11-lpddr4x-evk" ;; - 6) + 5) machine="imx8mm-jaguar-screen" ;; + 6) machine="imx93-11x11-lpddr4x-evk" ;; + 7) echo -n "Enter custom machine name: " read -r custom_machine if [[ -n "$custom_machine" ]]; then @@ -853,6 +856,7 @@ validate_machine() { "imx93-jaguar-eink" "imx8mm-jaguar-phasora" "imx8mm-jaguar-inst" + "imx8mm-jaguar-screen" "imx93-11x11-lpddr4x-evk" ) diff --git a/scripts/kas-build-base-enhanced.sh b/scripts/kas-build-base-enhanced.sh index e5d39633..211d1211 100755 --- a/scripts/kas-build-base-enhanced.sh +++ b/scripts/kas-build-base-enhanced.sh @@ -55,7 +55,8 @@ trap cleanup EXIT # Supported machines list SUPPORTED_MACHINES=( "imx8mm-jaguar-sentai" - "imx8mm-jaguar-inst" + "imx8mm-jaguar-inst" + "imx8mm-jaguar-screen" "imx8mm-jaguar-phasora" "imx8mm-jaguar-handheld" "imx93-jaguar-eink" diff --git a/scripts/kas-build-profiling.sh b/scripts/kas-build-profiling.sh index f0fef47a..48cece16 100755 --- a/scripts/kas-build-profiling.sh +++ b/scripts/kas-build-profiling.sh @@ -23,7 +23,7 @@ echo "" # Validate machine case "$MACHINE" in - imx93-jaguar-eink|imx8mm-jaguar-sentai|imx8mm-jaguar-inst|imx8mm-jaguar-handheld|imx8mm-jaguar-phasora) + imx93-jaguar-eink|imx8mm-jaguar-sentai|imx8mm-jaguar-inst|imx8mm-jaguar-screen|imx8mm-jaguar-handheld|imx8mm-jaguar-phasora) echo "✓ Building for supported machine: $MACHINE" ;; *) @@ -32,6 +32,7 @@ case "$MACHINE" in echo " - imx93-jaguar-eink (default)" echo " - imx8mm-jaguar-sentai" echo " - imx8mm-jaguar-inst" + echo " - imx8mm-jaguar-screen" echo " - imx8mm-jaguar-handheld" echo " - imx8mm-jaguar-phasora" exit 1