Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meta-dynamicdevices-bsp
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions scripts/add-additional-labels.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/fio-program-board.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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="
Expand Down
14 changes: 9 additions & 5 deletions scripts/fio-program-board.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -853,6 +856,7 @@ validate_machine() {
"imx93-jaguar-eink"
"imx8mm-jaguar-phasora"
"imx8mm-jaguar-inst"
"imx8mm-jaguar-screen"
"imx93-11x11-lpddr4x-evk"
)

Expand Down
3 changes: 2 additions & 1 deletion scripts/kas-build-base-enhanced.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading