-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tftp: negotiate blocksize and timeout
Signed-off-by: Benny Zlotnik <[email protected]>
- Loading branch information
Showing
7 changed files
with
696 additions
and
49 deletions.
There are no files selected for viewing
Empty file.
185 changes: 185 additions & 0 deletions
185
contrib/drivers/rcars4/jumpstarter_driver_rcars4/client.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
32
contrib/drivers/rcars4/jumpstarter_driver_rcars4/driver.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
176
contrib/drivers/rcars4/jumpstarter_driver_rcars4/rcar_setup.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.