ci: EasyMesh R1: flash Freedom via FEAT-29 update_prpl

Align the Easymesh R1 smoke test with the DUT flashing procedure from FEAT-29.
This commit aligns the DUT deploy scripts from the prplMesh repository.

Reference: PPM-4139
Signed-off-by: Maarten De Decker <maarten.dedecker@mind.be>
This commit is contained in:
Maarten De Decker 2026-07-09 22:41:22 +02:00 committed by FAOUZI YAHYA
parent 781f619476
commit 7f572dc941
9 changed files with 192 additions and 91 deletions

View File

@ -13,8 +13,8 @@ EasyMesh-Smoke-test-R1-Freedom:
EASYMESH_TESTS=$EASYMESH_TESTS_SIMPLE
echo -e "${TXT_RED}Executing Easymesh tests: ${TXT_PRPL}$EASYMESH_TESTS${TXT_CLEAR}"
- mkdir -p build/freedom/
- cp bin/targets/ipq95xx/generic/prplos-ipq95xx-generic-prpl_freedom-squashfs-sysupgrade.bin build/freedom/prplos-ipq95xx-generic-prpl_freedom-squashfs-sysupgrade.bin
- .gitlab/scripts/prplMesh/deploy_firmware.py --device "$RUN_ON_DEVICE" --full --target-name "$RUN_ON_DEVICE" --image "prplos-ipq95xx-generic-prpl_freedom-squashfs-sysupgrade.bin" --whm --configuration ".gitlab/scripts/prplMesh/dut-configuration/R1/freedom.sh"
- cp bin/targets/ipq95xx/generic/{prplos-ipq95xx-generic-prpl_freedom-update_script.itb,prplos-ipq95xx-generic-prpl_freedom-kernel.itb,prplos-ipq95xx-generic-prpl_freedom-rootfs.itb} build/freedom/
- .gitlab/scripts/prplMesh/deploy_firmware.py --device "$RUN_ON_DEVICE" --full --target-name "$RUN_ON_DEVICE" --image "prplos-ipq95xx-generic-prpl_freedom-kernel.itb" --rootfs "prplos-ipq95xx-generic-prpl_freedom-rootfs.itb" --whm --configuration ".gitlab/scripts/prplMesh/dut-configuration/R1/freedom.sh"
- .gitlab/scripts/prplMesh/git-clean-reset.sh ~/easymesh_cert "$(<".gitlab/scripts/prplMesh/easymesh_cert_version")"
- ~/easymesh_cert/run_test_file.py --verbose --log-folder logs --easymesh-revision "$EASYMESH_REVISION" --device-under-test $RUN_ON_DEVICE "$EASYMESH_TESTS"
after_script:
@ -33,6 +33,6 @@ EasyMesh-Smoke-test-R1-Freedom:
- logs
when: always
tags:
- dut-prplmesh-freedom
- dut-prplmesh-freedom-SMD
- prplmesh-R1

View File

@ -23,7 +23,7 @@ def main():
or using sysupgrade, depending on the target device.""")
parser.add_argument('-d', '--device',
help="""Device to upgrade. Currently supported targets are:
turris-omnia haze urx_osp urx_ospv2""", required=True)
freedom urx_osp urx_ospv2 mozart""", required=True)
parser.add_argument(
'-t',
'--target-name',
@ -35,6 +35,22 @@ def main():
help="Name of the image to use for the upgrade (should exist in the artifacts folder).",
required=True)
parser.add_argument(
'-s',
'--rootfs',
help="Name of the rootfs FIT image to use for the upgrade "
"(should exist in the artifacts folder).", required=False)
parser.add_argument(
'-a',
'--ipaddr',
help="Bootloader IP address of the DUT", required=False)
parser.add_argument(
'-v',
'--serverip',
help="Bootloader IP address of the TFTP server", required=False)
parser.add_argument(
'-f',
'--full',
@ -54,7 +70,11 @@ def main():
args = parser.parse_args()
dev = device_from_name(args.device, args.target_name, args.image)
if args.device == "freedom" and args.rootfs is None:
raise ValueError("--rootfs is required when upgrading Freedom")
dev = device_from_name(args.device, args.target_name, args.image, args.rootfs,
args.ipaddr, args.serverip)
def do_upgrade(dev):
try:

View File

@ -25,6 +25,15 @@ class Freedom(GenericPrplOS):
bootloader_prompt = r"IPQ9574# "
"""The u-boot prompt on the target."""
update_script = "prplos-ipq95xx-generic-prpl_freedom-update_script.itb"
"""The name of the u-boot update script file"""
default_ipaddr = "192.168.250.150"
"""The bootloader IP address of the DUT, used when none is given."""
default_serverip = "192.168.250.199"
"""The bootloader IP address of the TFTP server, used when none is given."""
def upgrade_from_u_boot(self, shell: pexpect.fdpexpect.fdspawn):
"""Upgrade from u-boot and remove the overlay.
@ -37,29 +46,55 @@ class Freedom(GenericPrplOS):
shell.sendline("")
shell.expect(self.bootloader_prompt)
# Give the ethernet interfaces some time to initialize:
time.sleep(10)
time.sleep(5)
shell.sendline(f"tftpboot 0x44000000 {self.image}")
shell.sendline("")
shell.expect("Loading: ")
shell.sendline(f"setenv serverip {self.serverip}")
shell.expect(self.bootloader_prompt)
shell.sendline(f"setenv ipaddr {self.ipaddr}")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv loadaddr 0x50000000")
shell.expect(self.bootloader_prompt)
time.sleep(5)
shell.sendline(f"tftpboot ${{loadaddr}} {self.update_script}")
shell.expect("done")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv untar_addr_kernel; setenv untar_addr_root")
shell.sendline("source ${loadaddr}:update-script")
shell.expect("run update_prpl")
shell.expect("========================================================")
shell.expect(self.bootloader_prompt)
shell.sendline("untar 0x$fileaddr 0x$filesize kernel root")
shell.expect("filename: sysupgrade-prpl_freedom/CONTROL")
shell.expect("filename: sysupgrade-prpl_freedom/kernel")
shell.expect("filename: sysupgrade-prpl_freedom/root")
shell.sendline(f"setenv img_kernel {self.image}")
shell.expect(self.bootloader_prompt)
if self.rootfs is not None:
shell.sendline(f"setenv img_rootfs {self.rootfs}")
else:
shell.sendline(f"setenv img_rootfs")
shell.expect(self.bootloader_prompt)
shell.sendline("if test -n $untar_addr_kernel; then flash '0:HLOS' 0x$untar_addr_kernel 0x$untar_size_kernel; else echo 'kernel not found'; fi") # noqa E501
shell.expect("blocks erased: OK", timeout=10)
shell.expect("blocks written: OK", timeout=10)
shell.sendline("setenv img_uboot")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv img_xbl")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv img_smd")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv update_rescue_bank yes")
shell.expect(self.bootloader_prompt)
shell.sendline("if test -n $untar_addr_root; then flash 'rootfs' 0x$untar_addr_root 0x$untar_size_root; else echo 'rootfs not found'; fi") # noqa E501
shell.expect("blocks erased: OK", timeout=10)
shell.expect("blocks written: OK", timeout=10)
shell.sendline("mmc dev 0")
shell.expect(self.bootloader_prompt)
shell.sendline("mmc erase 0x534022 0x40000")
shell.expect("blocks erased: OK")
shell.expect(self.bootloader_prompt)
shell.sendline("run update_prpl")
shell.expect("kernel.itb written successfully to kernel-active", timeout=45)
shell.expect("kernel.itb written successfully to kernel-inactive", timeout=45)
shell.expect("rootfs.itb written successfully to rootfs-active", timeout=45)
shell.expect("rootfs.itb written successfully to rootfs-inactive", timeout=45)
shell.expect("img_uboot not set")
shell.expect("img_xbl not set")
shell.expect("img_smd not set")
shell.expect("Done.")
shell.expect(self.bootloader_prompt)
print("Flashing completed successfully")

View File

@ -7,6 +7,7 @@
# Standard library
import difflib
import ipaddress
import os
import re
import time
@ -41,11 +42,11 @@ class GenericDevice():
baudrate = 115200
"""The baudrate of the serial connection to the device."""
initialization_time = 200
initialization_time = 180
"""The time (in seconds) the device needs to initialize when it boots
for the first time after flashing a new image."""
configuration_initialization_time = 30
configuration_initialization_time = 5
"""The time (in seconds) the device needs to initialize after configuring it"""
bootloader_prompt = "=> "
@ -63,8 +64,21 @@ class GenericDevice():
tftp_dir = "/srv/tftp"
"""The root directory of the tftp server. OS images will be copied there."""
update_script = None
"""The name of the u-boot update script file"""
rootfs = None
"""The name of the rootfs image to use"""
uboot_ipaddr = "192.165.100.155"
"""The default bootloader IP address of the DUT"""
uboot_serverip = "192.165.100.199"
"""The default bootloader IP address of the TFTP server"""
def __init__(self, device: str, name: str, image: Union[str, None] = None,
username: str = "root"):
rootfs: Union[str, None] = None, ipaddr: Union[str, None] = None,
serverip: Union[str, None] = None, username: str = "root"):
"""
Parameters
@ -75,12 +89,25 @@ class GenericDevice():
The name of the device (it should ne reachable through ssh without a password).
image: Union[str, None]
The name of the image that can be used to upgrade the device.
rootfs: Union[str, None], optional
The name of the rootfs FIT image that can be used to upgrade the device.
ipaddr: Union[str, None], optional
The bootloader IP address of the DUT (defaults to `default_ipaddr`).
serverip: Union[str, None], optional
The bootloader IP address of the TFTP server (defaults to `default_serverip`).
username: str, optional
The username to use when connecting to the device over SSH.
"""
self.device = device
self.name = name
self.image = image
self.rootfs = rootfs
if ipaddr is not None:
ipaddress.ip_address(ipaddr)
if serverip is not None:
ipaddress.ip_address(serverip)
self.ipaddr = ipaddr if ipaddr is not None else self.uboot_ipaddr
self.serverip = serverip if serverip is not None else self.uboot_serverip
self.username = username
self.rootdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../../..")

View File

@ -12,14 +12,14 @@ from typing import Union
# Third party
import device.generic
import device.prplos
import device.turris_prplos
import device.turris_rdk_b
import device.haze
import device.urx_osp
import device.freedom
def device_from_name(name: str, target_name: str, image: Union[str, None] = None
def device_from_name(name: str, target_name: str, image: Union[str, None] = None,
rootfs: Union[str, None] = None, ipaddr: Union[str, None] = None,
serverip: Union[str, None] = None
) -> device.generic.GenericDevice:
"""Construct a device based on its name and type.
@ -31,17 +31,22 @@ def device_from_name(name: str, target_name: str, image: Union[str, None] = None
The name of the target.
image: image: Union[str, None]
The name of the image (optional, defaults to None).
rootfs: rootfs: Union[str, None]
The name of the rootfs FIT image (optional, defaults to None).
ipaddr: Union[str, None]
The bootloader IP address of the DUT (optional, defaults to None).
serverip: Union[str, None]
The bootloader IP address of the TFTP server (optional, defaults to None).
"""
if name == "turris-omnia-rdk":
dev = device.turris_rdk_b.TurrisRdkb(name, target_name, image)
elif name == "turris-omnia":
dev = device.turris_prplos.TurrisPrplOS(name, target_name, image)
elif name == "haze":
dev = device.haze.Haze(name, target_name, image)
elif name == "urx_osp" or name == "urx_ospv2":
dev = device.urx_osp.URXOSP(name, target_name, image)
dev = device.urx_osp.URXOSP(name, target_name, image, ipaddr=ipaddr, serverip=serverip)
elif name == "freedom":
dev = device.freedom.Freedom(name, target_name, image)
dev = device.freedom.Freedom(name, target_name, image, rootfs, ipaddr=ipaddr,
serverip=serverip)
elif name == "mozart":
dev = device.mozart.Mozart(name, target_name, image)
else:
# if no device matched, try the generic prplOS (sysupgrade)
print("No specific device matched, using GenericPrplOS.")

View File

@ -27,16 +27,7 @@ LOG_COMMANDS = \
LogCommand('ubus call IP _get \'{ "depth": "10" }\'', "dm-IP.txt"),
LogCommand('ubus call Bridging _get \'{ "depth": "10" }\'', "dm-Bridge.txt"),
LogCommand('ubus call Ethernet _get \'{ "depth": "10" }\'', "dm-Ethernet.txt"),
LogCommand('ubus call DHCPv4 _get \'{ "depth": "10" }\'', "dm-DHCPv4.txt"),
LogCommand('ubus call DHCPv6 _get \'{ "depth": "10" }\'', "dm-DHCPv6.txt"),
LogCommand('ubus call Firewall _get \'{ "depth": "10" }\'', "dm-Firewall.txt"),
LogCommand('ubus call WiFi _get \'{ "depth": "10" }\'', "dm-WiFi.txt"),
LogCommand('logread', "syslog.txt"),
LogCommand('iptables -L', "iptables-filter.txt"),
LogCommand('find /etc/config/tr181-bridging/ -type f -print -exec cat {} \';\'',
"config-tr181-bridging.txt"),
LogCommand('find /etc/amx/tr181-bridging/ -type f -print -exec cat {} \';\'',
"amx-tr181-bridging.txt"),
]
@ -72,18 +63,23 @@ def capture_logs(device: GenericDevice, path: str):
with open(f"{path}/all-console-{timestamp}.txt", "wb") as logfile:
with SerialDevice(device.baudrate, device.name, device.serial_prompt,
expect_prompt_on_connect=False, logfile=logfile) as shell:
shell.logfile = open(f"{path}/ci-expect-debug.txt", "wb")
shell.sendline("")
time.sleep(1)
shell.flush()
# Interrupt any running command:
shell.send("\003")
shell.expect(device.serial_prompt)
# There might have been two prompts, if no command had to be interrupted:
shell.expect([device.serial_prompt, pexpect.TIMEOUT])
for log in LOG_COMMANDS:
shell.sendline(log.cmd)
shell.expect(log.cmd)
shell.expect(device.serial_prompt)
cmd_output = shell.before.decode("utf-8")
with open(f"{path}/{log.filename}", "w", encoding="utf-8") as output_file:
output_file.writelines(cmd_output)
try:
for log in LOG_COMMANDS:
print(f"send command {log.cmd}")
shell.sendline(log.cmd)
shell.expect(device.serial_prompt)
cmd_output = shell.before.decode("utf-8", errors="replace")
with open(f"{path}/{log.filename}", "w", encoding="utf-8") as output_file:
output_file.writelines(cmd_output)
except pexpect.exceptions.TIMEOUT:
print(f"Warning: I/O error hitted, refer to ci-expect-debug.txt")
print("Done")

View File

@ -41,24 +41,6 @@ class GenericPrplOS(GenericDevice):
with open(os.path.join(self.artifacts_dir, "prplwrt-version")) as version_file:
return version_file.read().splitlines()
def read_remote_prplos_version(self) -> List[str]:
"""Read prplos build.version on a remote device
Returns
-------
List[str]
The content of the build.version file on the device as a list of strings.
"""
version = None
with pexpect.pxssh.pxssh(logfile=sys.stdout.buffer) as shell:
shell.login(self.name, self.username)
shell.sendline("cat /etc/build.version")
shell.expect("cat /etc/build.version")
shell.prompt()
# remove the first (empty) line:
version = shell.before.decode().splitlines()[1:]
return version
def read_remote_version(self) -> List[str]:
"""Read prplwrt-version on a remote device
@ -89,7 +71,14 @@ class GenericPrplOS(GenericDevice):
+ "Please make sure you have an appropriate udev rule for it.")
print("Copying image '{}' to '{}'".format(self.image, self.tftp_dir))
shutil.copy(os.path.join(self.artifacts_dir, self.image), self.tftp_dir)
print("Image copied to {}.".format(self.tftp_dir))
if self.update_script is not None:
print("Copying U-Boot update script '{}' to '{}'".format(
self.update_script, self.tftp_dir))
shutil.copy(os.path.join(self.artifacts_dir, self.update_script), self.tftp_dir)
if self.rootfs is not None:
print("Copying rootfs '{}' to '{}'".format(self.rootfs, self.tftp_dir))
shutil.copy(os.path.join(self.artifacts_dir, self.rootfs), self.tftp_dir)
print("Image(s) copied to {}.".format(self.tftp_dir))
with SerialDevice(self.baudrate, self.name, self.serial_prompt,
expect_prompt_on_connect=False) as shell:
self.reboot(self.check_serial_type(),

View File

@ -62,7 +62,9 @@ class SerialDevice(pexpect.fdpexpect.fdspawn):
+ "Please make sure you have an appropriate udev rule for it.")
if self.serial is not None:
raise ValueError("Serial already connected!")
self.serial = serial.Serial(self.serial_path, self.baudrate, xonxoff=True)
"""Setting xonxoff (software flow control)
will not work for the serial connection to Freedom in R6"""
self.serial = serial.Serial(self.serial_path, self.baudrate)
self.serial.flushInput()
super().__init__(self.serial, logfile=self.logfile)
@ -104,3 +106,11 @@ class SerialDevice(pexpect.fdpexpect.fdspawn):
for char in s:
self.send(char)
time.sleep(self.send_delay / 1000.)
def flush(self):
"""Flush the serial read buffer"""
if self.serial.is_open:
if hasattr(self.serial, "reset_input_buffer"):
self.serial.reset_input_buffer()
else:
self.serial.flushInput()

View File

@ -46,28 +46,47 @@ class URXOSP(GenericPrplOS):
# Give the ethernet interfaces some time to initialize:
time.sleep(5)
# Changes the default image name to upgrade
# shell.sendline(f"setenv fullimage {self.image}")
# shell.sendline("")
# shell.sendline("saveenv")
# shell.expect("OK")
shell.sendline("run update_fullimage")
shell.expect("done", timeout=30)
# Image transfer successful
shell.expect("Found device tree image", timeout=15)
shell.expect("Saving Environment to MMC", timeout=15)
shell.expect("OK", timeout=15)
# Image written to MMC
shell.sendline("")
shell.sendline(f"setenv serverip {self.serverip}")
shell.expect(self.bootloader_prompt)
shell.sendline(f"setenv ipaddr {self.ipaddr}")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv loadaddr 0x8200000")
shell.expect(self.bootloader_prompt)
time.sleep(10)
shell.sendline(
"mmc erase ${overlay_container_a_block_start} ${overlay_container_a_block_size}")
shell.expect("blocks erased: OK", timeout=15)
time.sleep(5)
shell.sendline(
"mmc erase ${overlay_container_a_block_start} ${overlay_container_a_block_size}")
shell.expect("blocks erased: OK", timeout=15)
shell.sendline(f"tftpboot ${{loadaddr}} {self.update_script}")
shell.expect("done")
shell.expect(self.bootloader_prompt)
shell.sendline("source ${loadaddr}:update-script")
shell.expect("run update_prpl")
shell.expect("========================================================")
shell.expect(self.bootloader_prompt)
shell.sendline(f"setenv img_kernel {self.image}")
shell.expect(self.bootloader_prompt)
shell.sendline(f"setenv img_rootfs")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv img_uboot")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv img_rbe")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv img_smd")
shell.expect(self.bootloader_prompt)
shell.sendline("setenv update_rescue_bank yes")
shell.expect(self.bootloader_prompt)
shell.sendline("mmc dev 0")
shell.expect(self.bootloader_prompt)
shell.sendline("mmc erase 0x86c00 0x10000")
shell.expect("blocks erased: OK")
shell.expect(self.bootloader_prompt)
shell.sendline("run update_prpl")
shell.expect("image.ext4 written successfully to kernel-active.", timeout=45)
shell.expect("image.ext4 written successfully to kernel-inactive", timeout=45)
shell.expect("img_uboot not set")
shell.expect("img_rbe not set")
shell.expect("img_smd not set")
shell.expect("Done.")
shell.expect(self.bootloader_prompt)
print("Flashing completed successfully")