Skip to content

Commit

Permalink
tftp: negotiate blocksize and timeout
Browse files Browse the repository at this point in the history
Signed-off-by: Benny Zlotnik <[email protected]>
  • Loading branch information
bennyz committed Jan 23, 2025
1 parent 856042f commit e5ccb8a
Show file tree
Hide file tree
Showing 7 changed files with 696 additions and 49 deletions.
Empty file.
185 changes: 185 additions & 0 deletions contrib/drivers/rcars4/jumpstarter_driver_rcars4/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
from dataclasses import dataclass
from pathlib import Path
import logging
import click
import sys
import time

from jumpstarter.client import DriverClient
from jumpstarter.drivers.composite.client import CompositeClient
from jumpstarter.client.adapters import PexpectAdapter

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@dataclass(kw_only=True)
class RCarSetupClient(CompositeClient, DriverClient):
def flash(self, initramfs: str, kernel: str, dtb: str, os_image: str):
self.children["tftp"].start()
self.children["http"].start()

existing_tftp_files = self.children["tftp"].list_files()
for path in [initramfs, kernel, dtb]:
filename = Path(path).name
if filename not in existing_tftp_files:
logger.info(f"Putting {path} in TFTP server")
self.children["tftp"].put_local_file(path)
else:
logger.info(f"File {filename} already exists on TFTP server")

existing_http_files = self.children["http"].list_files()
os_image_name = Path(os_image).name
if os_image_name not in existing_http_files:
logger.info(f"Putting {os_image} in HTTP server")
self.children["http"].put_local_file(os_image)
else:
logger.info(f"File {os_image_name} already exists on HTTP server")

try:
params = self.call("power_cycle")

with PexpectAdapter(client=self.children["serial"]) as console:
console.logfile = sys.stdout.buffer

logger.info("Waiting for U-Boot prompt...")
console.expect("=>", timeout=60)
logger.info("U-Boot prompt detected")

time.sleep(10)

logger.info("Configuring network...")
console.sendline("dhcp")
console.expect("DHCP client bound to address ([0-9.]+)")
ip_address = console.match.group(1).decode('utf-8')

console.expect("sending through gateway ([0-9.]+)")
gateway = console.match.group(1).decode('utf-8')
console.expect("=>")

tftp_host = self.children["tftp"].get_host()
env_vars = {
"ipaddr": ip_address,
"serverip": tftp_host,
"fdtaddr": "0x48000000",
"ramdiskaddr": "0x48080000",
"boot_tftp": (
"tftp ${fdtaddr} ${fdtfile}; "
"tftp ${loadaddr} ${bootfile}; "
"tftp ${ramdiskaddr} ${ramdiskfile}; "
"booti ${loadaddr} ${ramdiskaddr} ${fdtaddr}"
),
"bootfile": Path(kernel).name,
"fdtfile": Path(dtb).name,
"ramdiskfile": Path(initramfs).name
}

for key, value in env_vars.items():
logger.info(f"Setting env {key}={value}")
console.sendline(f"setenv {key} '{value}'")
console.expect("=>")

logger.info("Booting into initramfs...")
console.sendline("run boot_tftp")
console.expect("/ #", timeout=1000)

logger.info("Configuring initramfs network...")
for cmd in [
f"ip link set dev tsn0 up",
f"ip addr add {ip_address}/24 dev tsn0",
f"ip route add default via {gateway}"
]:
console.sendline(cmd)
console.expect("/ #")

logger.info("Flashing OS image...")
http_url = self.children["http"].get_url()
flash_cmd = (
f'wget -O - "{http_url}/{Path(os_image).name}" | '
f'zcat | dd of=/dev/mmcblk0 bs=64K iflag=fullblock'
)
console.sendline(flash_cmd)
console.expect(r"\d+ bytes \(.+?\) copied, [0-9.]+ seconds, .+?", timeout=600)
console.expect("/ #")

logger.info("Setting up watchdog for reboot...")
console.sendline("modprobe renesas_wdt")
console.expect("/ #")
console.sendline("watchdog -T 2 -t 120 /dev/watchdog")
console.expect("/ #")

logger.info("Waiting for reboot...")
console.expect("=>", timeout=60)

boot_env = {
"bootcmd": (
"if part number mmc 0 boot boot_part; then "
"run boot_grub; else run boot_aboot; fi"
),
"boot_aboot": (
"mmc dev 0; "
"part start mmc 0 boot_a boot_start; "
"part size mmc 0 boot_a boot_size; "
"mmc read $loadaddr $boot_start $boot_size; "
"abootimg get dtb --index=0 dtb0_start dtb0_size; "
"setenv bootargs androidboot.slot_suffix=_a; "
"bootm $loadaddr $loadaddr $dtb0_start"
),
"boot_grub": (
"ext4load mmc 0:${boot_part} 0x48000000 "
"dtb/renesas/r8a779f0-spider.dtb; "
"fatload mmc 0:1 0x70000000 /EFI/BOOT/BOOTAA64.EFI && "
"bootefi 0x70000000 0x48000000"
)
}

for key, value in boot_env.items():
logger.info(f"Setting boot env {key}")
console.sendline(f"setenv {key} '{value}'")
console.expect("=>")

logger.info("Performing final boot...")
console.sendline("boot")
console.expect("login:", timeout=300)
console.sendline("root")
console.expect("Password:")
console.sendline("password")
console.expect("#")

return "Flash and boot completed successfully"

except Exception as e:
logger.error(f"Flash failed: {str(e)}")
raise

def cli(self):
@click.group()
def rcar():
"""RCar setup and flashing commands"""
pass

@rcar.command()
@click.option('--kernel', required=True, type=click.Path(exists=True),
help='Linux kernel ARM64 boot executable (uncompressed Image)')
@click.option('--initramfs', required=True, type=click.Path(exists=True),
help='Initial RAM filesystem (uImage format)')
@click.option('--dtb', required=True, type=click.Path(exists=True),
help='Device Tree Binary file')
@click.option('--os-image', required=True, type=click.Path(exists=True),
help='Operating system image to flash')
def flash(kernel, initramfs, dtb, os_image):
"""
Flash images to the device using TFTP and HTTP servers
"""
result = self.flash(
str(Path(initramfs).resolve()),
str(Path(kernel).resolve()),
str(Path(dtb).resolve()),
str(Path(os_image).resolve())
)
click.echo(result)

for name, child in self.children.items():
if hasattr(child, "cli"):
rcar.add_command(child.cli(), name)

return rcar
32 changes: 32 additions & 0 deletions contrib/drivers/rcars4/jumpstarter_driver_rcars4/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from dataclasses import dataclass
import logging
import time

from jumpstarter.driver import Driver, export
from jumpstarter.drivers.composite.driver import CompositeInterface
from jumpstarter.drivers.pyserial.driver import PySerial

logger = logging.getLogger(__name__)

@dataclass(kw_only=True)
class RCarSetup(CompositeInterface, Driver):
@classmethod
def client(cls) -> str:
return "jumpstarter_driver_rcars4.client.RCarSetupClient"

def __post_init__(self):
super().__post_init__()
if "serial" not in self.children:
self.children["serial"] = PySerial(url="/dev/ttyUSB0", baudrate=1843200)

@export
def power_cycle(self) -> dict:
logger.info("power cycling device...")
self.children["gpio"].off()
time.sleep(3)
self.children["gpio"].on()

return {
"tftp_host": self.children["tftp"].get_host(),
"http_url": self.children["http"].get_url()
}
176 changes: 176 additions & 0 deletions contrib/drivers/rcars4/jumpstarter_driver_rcars4/rcar_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import sys
import time

from jumpstarter.client.adapters import PexpectAdapter

from uboot import DhcpInfo


class ConsoleHelper:
def __init__(self, console, expected_str=None, delay=0):
self.console = console
self.expected_str: str | None = expected_str
self.delay = delay

def send_command(self, cmd: str, expect_str=None, timeout: int = 30, delay: int = 0):
self.console.sendline(cmd)
time.sleep(delay or self.delay)

if expect_str is None and self.expected_str:
expect_str = self.expected_str

if expect_str:
self.console.expect(expect_str, timeout=timeout)
if self.console.before is not None:
return self.console.before.decode('utf-8')
return ""

def wait_for_pattern(self, pattern: str, timeout: int = 60):
self.console.expect(pattern, timeout=timeout)
return (self.console.before + self.console.after).decode('utf-8')

def get_dhcp_info(self, timeout: int = 60) -> DhcpInfo:
print("\nRunning DHCP to obtain network configuration...")
self.send_command("dhcp")

self.console.expect("DHCP client bound to address ([0-9.]+)", timeout=timeout)
ip_address = self.console.match.group(1).decode('utf-8')

self.console.expect("sending through gateway ([0-9.]+)", timeout=timeout)
gateway = self.console.match.group(1).decode('utf-8')
self.wait_for_pattern("=>")
self.send_command("printenv netmask")
try:
self.console.expect("netmask=([0-9.]+)", timeout=timeout)
netmask = self.console.match.group(1).decode('utf-8')
except Exception:
print("Could not get netmask, using default 255.255.255.0")
netmask = "255.255.255.0"

self.wait_for_pattern("=>")

return DhcpInfo(ip_address=ip_address, gateway=gateway, netmask=netmask)

def set_env(self, key: str, value: str):
self.send_command(f"setenv {key} '{value}'", "=>", timeout=10)

def restart_from_initramfs(console):
helper = ConsoleHelper(console)
commands = [
"modprobe renesas_wdt",
"watchdog -T 2 -t 120 /dev/watchdog",
]

for cmd in commands:
helper.send_command(cmd, "/ #")

def initramfs_shell_flash(console, http_url, dhcp_info: DhcpInfo):
helper = ConsoleHelper(console)

commands = [
"ip link set dev tsn0 up",
f"ip addr add {dhcp_info.ip_address}/{dhcp_info.cidr} dev tsn0",
f"ip route add default via {dhcp_info.gateway}",
]

for cmd in commands:
helper.send_command(cmd, "/ #")

flash_cmd = f'wget -O - "{http_url}/target.gz" | zcat | dd of=/dev/mmcblk0 bs=64K iflag=fullblock'
helper.send_command(flash_cmd)
console.expect(r"\d+ bytes \(.+?\) copied, [0-9.]+ seconds, .+?", timeout=600)
console.expect("/ #", timeout=30)

def upload_files_to_tftp(tftp, test_files):
for filename in test_files:
print(f"Uploading {filename} to TFTP server...")
tftp.put_local_file(filename)

def upload_target_to_http(http_client, filename="target.gz", local_filepath="target.gz"):
try:
existing_files = http_client.list_files()
if filename in existing_files:
print(f"File '{filename}' already exists on the server.")
return True

print(f"Uploading '{filename}' to the HTTP server...")
uploaded_filename = http_client.put_local_file(local_filepath)
print(f"File '{uploaded_filename}' uploaded successfully.")
return True

except Exception as e:
print(f"Error uploading file to HTTP server: {e}")
return False

def setup_environment(client, skip_flash=False):
gpio = client.gpio
serial = client.serial
tftp = client.tftp
http = client.http
if not skip_flash:
tftp_host = tftp.get_host()

http.start()
upload_target_to_http(http, filename="target.gz")

test_files = ["Image", "r8a779f0-spider.dtb", "initramfs-debug.img"]
tftp.start()
upload_files_to_tftp(tftp, test_files)

print("Turning power off")
gpio.off()
time.sleep(3)
print("Turning power on")
gpio.on()

with PexpectAdapter(client=serial) as console:
console.logfile = sys.stdout.buffer
console.expect("=>", timeout=60)
time.sleep(10)

helper = ConsoleHelper(console)
dhcp_info = helper.get_dhcp_info()

env_vars = {
"ipaddr": dhcp_info.ip_address,
"serverip": tftp_host,
"fdtaddr": "0x48000000",
"ramdiskaddr": "0x48080000",
"boot_tftp": "tftp ${fdtaddr} ${fdtfile}; tftp ${loadaddr} ${bootfile}; tftp ${ramdiskaddr} ${ramdiskfile}; booti ${loadaddr} ${ramdiskaddr} ${fdtaddr}",
"bootfile": "Image",
"fdtfile": "r8a779f0-spider.dtb",
"ramdiskfile": "initramfs-debug.img"
}

for key, value in env_vars.items():
helper.set_env(key, value)

helper.send_command("run boot_tftp", '/ #', timeout=3000)

initramfs_shell_flash(console, http.get_url(), dhcp_info)
restart_from_initramfs(console)

console.expect("=>", timeout=60)

boot_env = {
"bootcmd": "if part number mmc 0 boot boot_part; then run boot_grub; else run boot_aboot; fi",
"boot_aboot": "mmc dev 0; part start mmc 0 boot_a boot_start; part size mmc 0 boot_a boot_size; mmc read $loadaddr $boot_start $boot_size; abootimg get dtb --index=0 dtb0_start dtb0_size; setenv bootargs androidboot.slot_suffix=_a; bootm $loadaddr $loadaddr $dtb0_start",
"boot_grub": "ext4load mmc 0:${boot_part} 0x48000000 dtb/renesas/r8a779f0-spider.dtb; fatload mmc 0:1 0x70000000 /EFI/BOOT/BOOTAA64.EFI && bootefi 0x70000000 0x48000000"
}

for key, value in boot_env.items():
helper.set_env(key, value)

helper.send_command("boot")
console.expect("login:", timeout=300)
helper.send_command("root", expect_str="Password:", timeout=10)
helper.send_command("password", expect_str="#", timeout=10)

return client

def teardown_environment(client):
http = client.http
tftp = client.tftp

http.stop()
tftp.stop()
Loading

0 comments on commit e5ccb8a

Please sign in to comment.