Skip to content

Commit da3e933

Browse files
author
Alex J Lennon
committed
Simplify python3-improv: use same filenames in machine override folders
- eink: replace improv-eink.service/onboarding-server-eink.py with improv.service and onboarding-server.py in imx93-jaguar-eink/ - INST: add imx8mm-jaguar-inst/ with improv.service and onboarding-server.py - Recipe: remove all machine-specific SRC_URI, do_install conditionals, FILES and SYSTEMD_SERVICE logic; Yocto picks up files from MACHINE/ when present via FILESEXTRAPATHS - Update MACHINE_SPECIFIC_FILES.md to describe simplified pattern
1 parent 16a1ccd commit da3e933

File tree

7 files changed

+330
-159
lines changed

7 files changed

+330
-159
lines changed

recipes-devtools/python/python3-improv/MACHINE_SPECIFIC_FILES.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,27 +65,31 @@ SYSTEMD_SERVICE:${PN} = "${@bb.utils.contains('MACHINE', 'imx93-jaguar-eink', 'm
6565
## How It Works
6666

6767
1. **FILESEXTRAPATHS**: Extends the file search path to the recipe directory (`THISDIR`)
68-
2. **Automatic Lookup**: Yocto automatically looks in `${MACHINE}/` subdirectory first
69-
3. **Machine-Specific**: For `imx93-jaguar-eink`, files are found in `imx93-jaguar-eink/`
70-
4. **Other Machines**: For other machines, files are found in the recipe directory root
68+
2. **Automatic Lookup**: Yocto automatically looks in `${MACHINE}/` subdirectory first, then recipe root
69+
3. **Same filenames**: Use the same filenames in machine folders (e.g. `improv.service`, `onboarding-server.py`) so the recipe needs no machine-specific SRC_URI, do_install, or SYSTEMD_SERVICE logic
7170

7271
## Example: python3-improv Recipe
7372

7473
**Structure:**
7574
```
7675
recipes-devtools/python/python3-improv/
77-
├── improv.service (common - all machines)
78-
├── onboarding-server.py (common - all machines)
79-
├── python3-improv_git.bb (recipe)
80-
└── imx93-jaguar-eink/ (machine-specific folder)
81-
├── improv-eink.service
82-
└── onboarding-server-eink.py
76+
├── improv.service (default - all machines without override)
77+
├── onboarding-server.py (default)
78+
├── python3-improv_git.bb (recipe in parent directory)
79+
├── imx93-jaguar-eink/ (machine override - same filenames)
80+
│ ├── improv.service
81+
│ └── onboarding-server.py
82+
└── imx8mm-jaguar-inst/ (machine override - same filenames)
83+
├── improv.service
84+
└── onboarding-server.py
8385
```
8486

8587
**Result:**
86-
- `imx93-jaguar-eink`: Uses files from `imx93-jaguar-eink/` subdirectory
87-
- All other machines (sentai, etc.): Use files from recipe directory root
88-
- No changes to original files - other machine behavior preserved
88+
- Yocto picks up `improv.service` and `onboarding-server.py` from `${MACHINE}/` when present; otherwise uses recipe root.
89+
- `imx93-jaguar-eink`: Uses files from `imx93-jaguar-eink/` (eink-XXXX BLE name, improv-eink connection).
90+
- `imx8mm-jaguar-inst`: Uses files from `imx8mm-jaguar-inst/` (Improv-Inst BLE, improv-inst connection).
91+
- All other machines: Use files from recipe directory root.
92+
- Recipe has no machine-specific SRC_URI, do_install, or SYSTEMD_SERVICE logic.
8993

9094
## Key Points
9195

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[Unit]
2+
Description=Improv BLE WiFi onboarding service for imx8mm-jaguar-inst (INST)
3+
Documentation=https://www.improv-wifi.com/ble/
4+
After=bluetooth.target
5+
Wants=bluetooth.target
6+
7+
[Service]
8+
Type=simple
9+
ExecStart=/usr/share/improv/onboarding-server.py
10+
Restart=always
11+
RestartSec=12
12+
Environment="IMPROV_WIFI_INTERFACE=wlan0"
13+
Environment="IMPROV_SERVICE_NAME=Improv-Inst"
14+
Environment="IMPROV_CONNECTION_NAME=improv-inst"
15+
16+
[Install]
17+
WantedBy=default.target

recipes-devtools/python/python3-improv/imx93-jaguar-eink/onboarding-server-eink.py renamed to recipes-devtools/python/python3-improv/imx8mm-jaguar-inst/onboarding-server.py

Lines changed: 20 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
33
#
4-
# Custom Improv onboarding server for imx93-jaguar-eink board
4+
# Custom Improv onboarding server for imx8mm-jaguar-inst (INST board)
55
# Based on onboarding-server.py but with board-specific customizations
66
#
77

@@ -20,9 +20,9 @@
2020
import logging
2121
import uuid
2222
import nmcli
23+
import subprocess
2324
import os
2425
import re
25-
import subprocess
2626

2727
logging.basicConfig(level=logging.DEBUG)
2828
logger = logging.getLogger(name=__name__)
@@ -82,44 +82,11 @@ def build_gatt():
8282
The device MUST advertise the Service UUID.
8383
"""
8484

85-
def get_board_id():
86-
"""Get unique board ID from SOC serial number.
87-
88-
Reads the SOC serial number from /sys/devices/soc0/serial_number
89-
and extracts the last 4 characters to create a unique board identifier.
90-
91-
Returns:
92-
str: Board ID in format "XXXX" (4 hex characters), or "0000" if unavailable
93-
"""
94-
try:
95-
# Read SOC serial number (32-character hex string)
96-
soc_serial_path = "/sys/devices/soc0/serial_number"
97-
if os.path.exists(soc_serial_path):
98-
with open(soc_serial_path, 'r') as f:
99-
serial = f.read().strip()
100-
# Extract last 4 characters (most unique portion)
101-
# Remove any non-hex characters and take last 4
102-
serial_clean = re.sub(r'[^0-9a-fA-F]', '', serial)
103-
if len(serial_clean) >= 4:
104-
board_id = serial_clean[-4:].upper() # Last 4 chars, uppercase
105-
logger.info(f"Board ID from SOC serial: {board_id}")
106-
return board_id
107-
logger.warning("SOC serial number not found, using default board ID")
108-
except Exception as e:
109-
logger.error(f"Error reading board ID: {e}")
110-
111-
# Fallback to default if unavailable
112-
return "0000"
113-
114-
# Board-specific configuration for imx93-jaguar-eink
115-
# Can be overridden via environment variables
85+
# Board-specific configuration for imx8mm-jaguar-inst (overridable via environment)
11686
SERVER_HOST = os.getenv("IMPROV_SERVER_HOST", "api.co.uk")
117-
# Generate unique service name from board ID: "eink-XXXX" where XXXX is last 4 chars of SOC serial
118-
BOARD_ID = get_board_id()
119-
DEFAULT_SERVICE_NAME = f"eink-{BOARD_ID}"
120-
SERVICE_NAME = os.getenv("IMPROV_SERVICE_NAME", DEFAULT_SERVICE_NAME)
121-
CON_NAME = os.getenv("IMPROV_CONNECTION_NAME", "improv-eink")
122-
INTERFACE = os.getenv("IMPROV_WIFI_INTERFACE", "wlan0") # imx93-jaguar-eink uses wlan0
87+
SERVICE_NAME = os.getenv("IMPROV_SERVICE_NAME", "Improv-Inst")
88+
CON_NAME = os.getenv("IMPROV_CONNECTION_NAME", "improv-inst")
89+
INTERFACE = os.getenv("IMPROV_WIFI_INTERFACE", "wlan0")
12390
TIMEOUT = int(os.getenv("IMPROV_CONNECTION_TIMEOUT", "10000"))
12491

12592
# Use new_event_loop() or get_event_loop() depending on Python version
@@ -142,28 +109,23 @@ def wifi_connect(ssid: str, passwd: str) -> Optional[list[str]]:
142109
print(f'No connection {CON_NAME} to remove')
143110

144111
try:
145-
# Create WiFi connection with improved settings for boot-time reliability:
146-
# - autoconnect-priority 20 (highest priority - ensures wifi-connect.service selects this connection)
147-
# Higher than GrosnyIoT (10) so Improv-configured networks are preferred
148-
# - dhcp-timeout 60 (60 seconds for DHCP, longer than default 45)
149-
# - autoconnect-retries -1 (retry connection indefinitely - never give up)
150-
# - auth-retries -1 (retry authentication indefinitely - never give up)
112+
# Create connection with secrets stored in file (not agent-only)
113+
# This prevents "no secrets" errors on headless systems when 4-way handshake fails
114+
#
151115
# ⚠️ CRITICAL: wifi-sec.psk-flags:'0' is REQUIRED for the NetworkManager patch
152116
# (0001-wifi-dont-clear-secrets-if-stored-in-keyfile.patch) to work correctly.
153117
# Without this, the patch will not activate and connections may fail permanently
154118
# after 4-way handshake failures.
155119
# See: meta-dynamicdevices-distro/recipes-connectivity/networkmanager/networkmanager/README_PATCH_REQUIREMENTS.md
156-
nmcli.connection.add('wifi', {
157-
'ssid': ssid.decode('utf-8'),
158-
'wifi-sec.key-mgmt': 'wpa-psk',
159-
'wifi-sec.psk': passwd.decode('utf-8'),
160-
'wifi-sec.psk-flags': '0', # REQUIRED: Store PSK in file, not agent-only (patch requirement)
161-
'connection.autoconnect': 'yes',
162-
'connection.autoconnect-priority': '20',
163-
'connection.autoconnect-retries': '-1', # Retry connection indefinitely (-1 = unlimited)
164-
'connection.auth-retries': '-1', # Retry authentication indefinitely (-1 = unlimited)
165-
'connection.permissions': '', # Allow system-wide use
166-
'ipv4.dhcp-timeout': '60'
120+
nmcli.connection.add('wifi', {
121+
'ssid':ssid.decode('utf-8'),
122+
'wifi-sec.key-mgmt':'wpa-psk',
123+
'wifi-sec.psk':passwd.decode('utf-8'),
124+
'wifi-sec.psk-flags':'0', # REQUIRED: Store PSK in file, not agent-only (patch requirement)
125+
'connection.autoconnect':'yes',
126+
'connection.autoconnect-retries':'-1', # Retry connection indefinitely (-1 = unlimited)
127+
'connection.auth-retries':'-1', # Retry authentication indefinitely (-1 = unlimited)
128+
'connection.permissions':'' # Allow system-wide use
167129
}, f"{INTERFACE}", f"{CON_NAME}", True)
168130
logger.info(f"Successfully created WiFi connection {CON_NAME}")
169131

@@ -173,93 +135,50 @@ def wifi_connect(ssid: str, passwd: str) -> Optional[list[str]]:
173135
return None
174136

175137
# CRITICAL: Explicitly add psk-flags=0 to connection file
176-
# NetworkManager 1.46.0 may not write psk-flags=0 to the keyfile even when set,
177-
# but the NetworkManager patch REQUIRES it to be explicitly in the file to detect
178-
# that secrets are stored. Directly edit the file to ensure it's present.
179138
connection_file = f"/etc/NetworkManager/system-connections/{CON_NAME}.nmconnection"
180139
try:
181140
if os.path.exists(connection_file):
182-
# Read the file
183141
with open(connection_file, 'r') as f:
184142
content = f.read()
185-
186-
# Check if psk-flags=0 is already in the file
187143
if 'psk-flags=0' not in content and 'psk-flags=0\n' not in content:
188-
# Add psk-flags=0 to [wifi-security] section
189-
# Find [wifi-security] section and add psk-flags=0 after psk line
190144
pattern = r'(\[wifi-security\]\n(?:[^\[]*\n)*?psk=[^\n]+\n)'
191145
replacement = r'\1psk-flags=0\n'
192146
new_content = re.sub(pattern, replacement, content)
193-
194-
# If pattern didn't match, try adding it after [wifi-security]
195147
if new_content == content:
196148
pattern = r'(\[wifi-security\]\n)'
197149
replacement = r'\1psk-flags=0\n'
198150
new_content = re.sub(pattern, replacement, content)
199-
200-
# Write the file back (requires root, but we're running as root via sudo)
201151
if new_content != content:
202152
with open(connection_file, 'w') as f:
203153
f.write(new_content)
204154
logger.info(f"Added psk-flags=0 to connection file {connection_file}")
205-
else:
206-
logger.warning(f"Could not find [wifi-security] section to add psk-flags=0")
207-
else:
208-
logger.debug(f"psk-flags=0 already in connection file")
209155
else:
210156
logger.warning(f"Connection file not found at {connection_file} - cannot add psk-flags=0")
211157
except PermissionError as e:
212-
logger.warning(f"Permission denied editing connection file {connection_file}: {e}")
213-
# Try nmcli modify as fallback
214158
try:
215159
subprocess.run(['nmcli', 'connection', 'modify', f"{CON_NAME}",
216160
'802-11-wireless-security.psk-flags', '0'],
217161
check=True, capture_output=True, timeout=5)
218-
logger.debug(f"Used nmcli to set psk-flags=0 (file edit failed)")
219162
except Exception as e2:
220163
logger.warning(f"nmcli modify also failed: {e2}")
221164
except Exception as e:
222165
logger.warning(f"Unexpected error adding psk-flags=0 to file: {e}", exc_info=True)
223166

224-
# NetworkManager automatically saves connections when created/modified
225-
# In NetworkManager 1.46.0+, connections are saved automatically to
226-
# /etc/NetworkManager/system-connections/ when created with nmcli.connection.add()
227-
# The explicit modify above ensures psk-flags=0 is written to the file.
228-
# Use 'reload' to ensure NetworkManager picks up the connection immediately
229167
try:
230168
subprocess.run(['nmcli', 'connection', 'reload'],
231169
check=True, capture_output=True, timeout=5)
232-
logger.debug(f"Reloaded NetworkManager connections")
233-
except subprocess.CalledProcessError as e:
234-
logger.debug(f"Could not reload NetworkManager connections (exit code {e.returncode}): {e.stderr.decode() if e.stderr else 'unknown error'}")
235-
# Non-critical - connection is already saved automatically
236-
except subprocess.TimeoutExpired as e:
237-
logger.debug(f"Timeout reloading NetworkManager connections: {e}")
238-
# Non-critical - connection is already saved automatically
239-
except FileNotFoundError:
240-
logger.debug(f"nmcli command not found - cannot reload connections")
241-
# Non-critical - connection is already saved automatically
242-
except Exception as e:
243-
logger.debug(f"Unexpected error reloading connections: {e}")
244-
# Non-critical - connection is already saved automatically
170+
except Exception:
171+
pass
245172

246-
# Verify connection file exists and has correct settings
247173
connection_file = f"/etc/NetworkManager/system-connections/{CON_NAME}.nmconnection"
248174
try:
249175
if os.path.exists(connection_file):
250-
logger.debug(f"Connection file exists: {connection_file}")
251-
# Read file to verify psk-flags=0 is set
252176
with open(connection_file, 'r') as f:
253177
content = f.read()
254178
if 'psk-flags=0' in content or 'psk-flags=0\n' in content:
255179
logger.debug(f"Verified psk-flags=0 in connection file")
256-
else:
257-
logger.warning(f"psk-flags=0 not found in connection file - connection may not work correctly")
258-
else:
259-
logger.warning(f"Connection file not found at {connection_file} - connection may not persist")
260180
except Exception as e:
261181
logger.debug(f"Could not verify connection file: {e}")
262-
# Non-critical - just for verification
263182

264183
try:
265184
nmcli.connection.up(f"{CON_NAME}", TIMEOUT)
@@ -356,4 +275,3 @@ async def run(loop):
356275
logger.debug("Shutting Down")
357276
trigger.set()
358277
pass
359-

recipes-devtools/python/python3-improv/imx93-jaguar-eink/improv-eink.service

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[Unit]
2+
Description=Improv BLE WiFi onboarding service for imx93-jaguar-eink
3+
Documentation=https://www.improv-wifi.com/ble/
4+
# CRITICAL: Must start WITHOUT network - this service is used to CONFIGURE WiFi!
5+
After=bluetooth.target
6+
Wants=bluetooth.target
7+
8+
[Service]
9+
Type=simple
10+
ExecStart=/usr/share/improv/onboarding-server.py
11+
Restart=always
12+
RestartSec=12
13+
Environment="IMPROV_WIFI_INTERFACE=wlan0"
14+
Environment="IMPROV_SERVICE_NAME=Improv-Eink"
15+
Environment="IMPROV_CONNECTION_NAME=improv-eink"
16+
17+
[Install]
18+
WantedBy=default.target

0 commit comments

Comments
 (0)