Skip to content

Commit eb27213

Browse files
committed
Add GitHub Actions workflow for testing JLinkRemoteServer with STM32F103RE devices
- Create a new workflow file `test-flash.yml` to automate testing of JLinkRemoteServer. - Define jobs to clean up previous instances, install dependencies, and detect connected devices. - Build Docker image for JLinkRemoteServer and set up containers for three STM32F103RE devices. - Implement tests to flash devices individually and in parallel, verifying success for each operation. - Include cleanup steps to remove Docker containers and networks after tests.
1 parent e66d01b commit eb27213

File tree

4 files changed

+258
-23
lines changed

4 files changed

+258
-23
lines changed

.github/workflows/fw.bin

242 KB
Binary file not shown.

.github/workflows/test-flash.yml

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
name: Test Flash
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
env:
10+
VNET_BASE: 192.168.3
11+
VNET_MASK: 24
12+
13+
jobs:
14+
test-flash:
15+
runs-on: self-hosted
16+
17+
steps:
18+
- name: Cleanup before start
19+
if: always()
20+
run: |
21+
pkill -9 -f JLinkRemoteServer || true
22+
docker rm -f jlink100 jlink101 jlink102 2>/dev/null || true
23+
docker network rm jlink-net 2>/dev/null || true
24+
sleep 2
25+
26+
- uses: actions/checkout@v4
27+
28+
- name: Install package
29+
run: |
30+
python3 -m venv .venv
31+
. .venv/bin/activate
32+
pip install -e ".[dev]"
33+
34+
- name: Detect devices and save serials
35+
run: |
36+
. .venv/bin/activate
37+
bmlab-scan | tee scan.txt
38+
count=$(grep -c "Target: *STM32F103RE" scan.txt)
39+
awk '/JLink Programmer/ {serial=""} /Serial:/ {serial=$2} /Target: *STM32F103RE/ && serial {print serial}' scan.txt | sort | uniq > serials.txt
40+
41+
if [ "$count" -ne 3 ]; then
42+
echo "Test failed: expected 3 STM32F103RE, found $count"
43+
cat scan.txt
44+
exit 1
45+
fi
46+
47+
- name: Build JLinkRemoteServer Docker image
48+
run: |
49+
docker build -t my-jlink-image -f .github/workflows/jlinkRemoteServerEmu.dockerfile .
50+
51+
- name: Setup JLinkRemoteServer Docker containers
52+
run: |
53+
docker network rm jlink-net 2>/dev/null || true
54+
docker network create --driver bridge --subnet=$VNET_BASE.0/$VNET_MASK --gateway=$VNET_BASE.1 jlink-net
55+
56+
serial1=$(sed -n '1p' serials.txt)
57+
serial2=$(sed -n '2p' serials.txt)
58+
serial3=$(sed -n '3p' serials.txt)
59+
60+
docker run -d --rm \
61+
--device=/dev/bus/usb:/dev/bus/usb \
62+
--name jlink100 \
63+
--network jlink-net \
64+
--ip $VNET_BASE.100 \
65+
my-jlink-image \
66+
-select usb=$serial1 -device STM32F103RE -endian little -speed 4000 -if swd
67+
sleep 3
68+
69+
docker run -d --rm \
70+
--device=/dev/bus/usb:/dev/bus/usb \
71+
--name jlink101 \
72+
--network jlink-net \
73+
--ip $VNET_BASE.101 \
74+
my-jlink-image \
75+
-select usb=$serial2 -device STM32F103RE -endian little -speed 4000 -if swd
76+
sleep 3
77+
78+
docker run -d --rm \
79+
--device=/dev/bus/usb:/dev/bus/usb \
80+
--name jlink102 \
81+
--network jlink-net \
82+
--ip $VNET_BASE.102 \
83+
my-jlink-image \
84+
-select usb=$serial3 -device STM32F103RE -endian little -speed 4000 -if swd
85+
sleep 3
86+
87+
- name: Verify network devices are available
88+
run: |
89+
. .venv/bin/activate
90+
bmlab-scan --network $VNET_BASE.0/$VNET_MASK | tee scan-network.txt
91+
92+
f103_count=$(grep -c "Target:.*STM32F103RE" scan-network.txt || echo 0)
93+
94+
if [ "$f103_count" -ne 3 ]; then
95+
echo "Expected 3 STM32F103RE devices, found $f103_count"
96+
cat scan-network.txt
97+
exit 1
98+
fi
99+
100+
- name: Test 1 — Flash single device by IP
101+
run: |
102+
. .venv/bin/activate
103+
bmlab-flash .github/workflows/fw.bin --ip $VNET_BASE.100 --mcu STM32F103RE | tee flash1.txt
104+
105+
if ! grep -q "Success" flash1.txt; then
106+
echo "Test 1 failed: flash not successful"
107+
cat flash1.txt
108+
exit 1
109+
fi
110+
111+
- name: Test 2 — Flash two devices in parallel
112+
run: |
113+
. .venv/bin/activate
114+
bmlab-flash .github/workflows/fw.bin --ip $VNET_BASE.100 $VNET_BASE.101 --mcu STM32F103RE | tee flash2.txt
115+
116+
success_count=$(grep -c "Success" flash2.txt || echo 0)
117+
118+
if [ "$success_count" -ne 2 ]; then
119+
echo "Test 2 failed: expected 2 successful flashes, found $success_count"
120+
cat flash2.txt
121+
exit 1
122+
fi
123+
124+
if ! grep -q "$VNET_BASE.100" flash2.txt; then
125+
echo "Test 2 failed: no result for $VNET_BASE.100"
126+
cat flash2.txt
127+
exit 1
128+
fi
129+
130+
if ! grep -q "$VNET_BASE.101" flash2.txt; then
131+
echo "Test 2 failed: no result for $VNET_BASE.101"
132+
cat flash2.txt
133+
exit 1
134+
fi
135+
136+
- name: Test 3 — Flash three devices in parallel
137+
run: |
138+
. .venv/bin/activate
139+
bmlab-flash .github/workflows/fw.bin --ip $VNET_BASE.100 $VNET_BASE.101 $VNET_BASE.102 --mcu STM32F103RE | tee flash3.txt
140+
141+
success_count=$(grep -c "Success" flash3.txt || echo 0)
142+
143+
if [ "$success_count" -ne 3 ]; then
144+
echo "Test 3 failed: expected 3 successful flashes, found $success_count"
145+
cat flash3.txt
146+
exit 1
147+
fi
148+
149+
for ip in 100 101 102; do
150+
if ! grep -q "$VNET_BASE\.$ip" flash3.txt; then
151+
echo "Test 3 failed: no result for $VNET_BASE.$ip"
152+
cat flash3.txt
153+
exit 1
154+
fi
155+
done
156+
157+
- name: Cleanup after tests
158+
if: always()
159+
run: |
160+
docker rm -f jlink100 jlink101 jlink102 || true
161+
docker network rm jlink-net || true
162+
pkill -9 -f JLinkRemoteServer || true
163+
sleep 2

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ Thumbs.db
4848
*.hex
4949
*.bin
5050
*.elf
51+
!fw.bin

src/bmlab_toolkit/flashing.py

Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33
import argparse
44
import logging
5+
from concurrent.futures import ThreadPoolExecutor, as_completed
56

67
from .constants import SUPPORTED_PROGRAMMERS, DEFAULT_PROGRAMMER, PROGRAMMER_JLINK
78
from .programmer import Programmer
@@ -27,6 +28,9 @@ def main():
2728
# Flash via JLink IP address
2829
bmlab-flash firmware.hex --ip 192.168.1.100
2930
31+
# Flash multiple devices in parallel
32+
bmlab-flash firmware.hex --ip 192.168.1.100 192.168.1.101 192.168.1.102
33+
3034
# Flash with specific MCU
3135
bmlab-flash firmware.hex --mcu STM32F765ZG
3236
@@ -51,8 +55,9 @@ def main():
5155
parser.add_argument(
5256
"--ip",
5357
type=str,
58+
nargs='+',
5459
default=None,
55-
help="JLink IP address for network connection (e.g., 192.168.1.100)"
60+
help="JLink IP address(es) for network connection (e.g., 192.168.1.100 or multiple: 192.168.1.100 192.168.1.101)"
5661
)
5762

5863
parser.add_argument(
@@ -85,37 +90,103 @@ def main():
8590
print("Error: Cannot specify both --serial and --ip")
8691
sys.exit(1)
8792

93+
# Check firmware file exists
94+
fw_file = os.path.abspath(args.firmware_file)
95+
if not os.path.exists(fw_file):
96+
print(f"Error: Firmware file not found: {fw_file}")
97+
print(f"To list connected devices, run: bmlab-scan")
98+
sys.exit(1)
99+
88100
try:
89101
# Convert log level string to logging constant
90102
log_level = getattr(logging, args.log_level.upper())
91103

92-
# Create programmer instance
93-
if args.programmer.lower() == PROGRAMMER_JLINK:
94-
prog = JLinkProgrammer(serial=args.serial, ip_addr=args.ip, log_level=log_level)
95-
else:
96-
raise NotImplementedError(f"Programmer '{args.programmer}' is not yet implemented")
97-
98-
# Flash firmware
99-
fw_file = os.path.abspath(args.firmware_file)
100-
if not os.path.exists(fw_file):
101-
print(f"Error: Firmware file not found: {fw_file}")
102-
print(f"To list connected devices, run: bmlab-scan")
103-
sys.exit(1)
104-
105-
# Check if programmer is available
106-
if not prog.probe():
107-
raise RuntimeError(f"Programmer not found or not accessible")
104+
# Handle IP addresses (convert to list for uniform processing)
105+
ip_list = args.ip if args.ip else None
108106

109-
print(f"Flashing {fw_file}...")
107+
flash_devices(args.serial, ip_list, fw_file, args.mcu, args.programmer, log_level)
110108

111-
# Flash firmware (will auto-connect, verify, flash, reset, and disconnect)
112-
if not prog.flash(fw_file, mcu=args.mcu):
113-
raise RuntimeError("Flash operation failed")
109+
except Exception as e:
110+
print(f"Error: {e}")
111+
sys.exit(1)
112+
113+
114+
def flash_device_task(serial, ip_addr, fw_file, mcu, programmer_type, log_level):
115+
"""Flash a single device (used in parallel execution)."""
116+
try:
117+
if programmer_type.lower() == PROGRAMMER_JLINK:
118+
prog = JLinkProgrammer(serial=serial, ip_addr=ip_addr, log_level=log_level)
119+
else:
120+
device_id = ip_addr or serial
121+
return {'device': device_id, 'success': False, 'error': f"Programmer '{programmer_type}' not implemented"}
114122

115-
print("\n✓ Flashing completed successfully!")
123+
if not prog.flash(fw_file, mcu=mcu):
124+
device_id = ip_addr or serial
125+
return {'device': device_id, 'success': False, 'error': 'Flash operation failed'}
116126

127+
device_id = ip_addr or serial
128+
return {'device': device_id, 'success': True, 'error': None}
117129
except Exception as e:
118-
print(f"Error: {e}")
130+
device_id = ip_addr or serial
131+
return {'device': device_id, 'success': False, 'error': str(e)}
132+
133+
134+
def flash_devices(serial, ip_list, fw_file, mcu, programmer_type, log_level):
135+
"""Flash one or multiple devices in parallel."""
136+
# Build device list
137+
devices = []
138+
if ip_list:
139+
devices = [{'serial': None, 'ip': ip} for ip in ip_list]
140+
else:
141+
devices = [{'serial': serial, 'ip': None}]
142+
143+
# Print header
144+
if len(devices) == 1:
145+
device_str = devices[0]['ip'] or f"serial {devices[0]['serial']}" or "auto-detected"
146+
print(f"Flashing {device_str}")
147+
else:
148+
print(f"Flashing {len(devices)} device(s) in parallel: {', '.join(d['ip'] for d in devices)}")
149+
print(f"Firmware: {fw_file}\n")
150+
151+
results = []
152+
with ThreadPoolExecutor(max_workers=len(devices)) as executor:
153+
# Submit all flash tasks
154+
future_to_device = {
155+
executor.submit(flash_device_task, dev['serial'], dev['ip'], fw_file, mcu, programmer_type, log_level): dev
156+
for dev in devices
157+
}
158+
159+
# Process results as they complete
160+
for future in as_completed(future_to_device):
161+
dev = future_to_device[future]
162+
try:
163+
result = future.result(timeout=300) # 5 min timeout per device
164+
results.append(result)
165+
166+
if result['success']:
167+
print(f"✓ {result['device']}: Success")
168+
else:
169+
print(f"✗ {result['device']}: {result['error']}")
170+
except Exception as e:
171+
device_id = dev['ip'] or dev['serial']
172+
results.append({'device': device_id, 'success': False, 'error': str(e)})
173+
print(f"✗ {device_id}: {e}")
174+
175+
# Summary
176+
success_count = sum(1 for r in results if r['success'])
177+
fail_count = len(results) - success_count
178+
179+
if len(devices) > 1:
180+
print(f"\n{'='*60}")
181+
print(f"Flashing completed: {success_count} successful, {fail_count} failed")
182+
print(f"{'='*60}")
183+
184+
if fail_count > 0:
185+
if len(devices) > 1:
186+
print("\nFailed devices:")
187+
for r in results:
188+
if not r['success']:
189+
print(f" - {r['device']}: {r['error']}")
119190
sys.exit(1)
120191

121192
if __name__ == "__main__":

0 commit comments

Comments
 (0)