JTAG: Start hw_server explicitly (Windows needs it), then run Vivado TCL with Popen + line-by-line stdout streaming via callback so user can see Vivado progress in real-time (not just on failure). COM: Remove normalize_port — pyserial >= 3.0 handles COM>=10 prefix internally. The manual \\.\ prefix was causing PermissionError(13) on Windows where pyserial's own handling conflicted.
401 lines
10 KiB
Python
401 lines
10 KiB
Python
"""Reboot manager for Zynq Flasher GUI.
|
|
|
|
Handles rebooting the Zynq device via JTAG, UDP, or serial.
|
|
Also provides UDP-based version query.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
from dataclasses import dataclass
|
|
from typing import Callable
|
|
|
|
from config_manager import Config
|
|
|
|
|
|
@dataclass
|
|
class RebootResult:
|
|
"""Result of a reboot operation."""
|
|
|
|
success: bool
|
|
message: str
|
|
method: str = ""
|
|
output: str = ""
|
|
|
|
|
|
@dataclass
|
|
class UdpVersionResult:
|
|
"""Result of a UDP version query."""
|
|
|
|
success: bool
|
|
message: str
|
|
version: str = ""
|
|
|
|
|
|
ProgressCallback = Callable[[str, str], None] | None
|
|
|
|
# ── UDP helpers ──────────────────────────────────────────────────────
|
|
|
|
|
|
def _udp_port_from_ip(ip: str) -> int:
|
|
"""Derive UDP command port from the last octet of the IP address.
|
|
|
|
Port = last_octet - 3 (e.g., 192.168.100.11 → 8).
|
|
|
|
Args:
|
|
ip: Zynq IP address string.
|
|
|
|
Returns:
|
|
UDP port number, or -1 on parse failure.
|
|
"""
|
|
try:
|
|
octets = ip.strip().split(".")
|
|
last = int(octets[3])
|
|
return last - 3
|
|
except (ValueError, IndexError):
|
|
return -1
|
|
|
|
|
|
def _send_udp_command(
|
|
ip: str,
|
|
command: bytes,
|
|
timeout: float = 5.0,
|
|
buffer_size: int = 4096,
|
|
) -> tuple[bool, str]:
|
|
"""Send a UDP command to the Zynq and receive the response.
|
|
|
|
Args:
|
|
ip: Zynq IP address.
|
|
command: Command bytes to send (e.g., b'reboot()').
|
|
timeout: Receive timeout in seconds.
|
|
buffer_size: Max receive buffer size.
|
|
|
|
Returns:
|
|
(success, response_text).
|
|
"""
|
|
port = _udp_port_from_ip(ip)
|
|
if port <= 0:
|
|
return False, f"Invalid IP for UDP port derivation: {ip}"
|
|
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
sock.settimeout(timeout)
|
|
try:
|
|
sock.sendto(command, (ip, port))
|
|
data, addr = sock.recvfrom(buffer_size)
|
|
return True, data.decode("utf-8", errors="replace").strip()
|
|
except socket.timeout:
|
|
return False, f"UDP timeout ({timeout}s)"
|
|
except OSError as e:
|
|
return False, f"UDP error: {e}"
|
|
finally:
|
|
sock.close()
|
|
|
|
|
|
# ── Reboot methods ───────────────────────────────────────────────────
|
|
|
|
|
|
def reboot_via_serial(
|
|
config: Config,
|
|
callback: ProgressCallback = None,
|
|
) -> RebootResult:
|
|
"""Reboot the Zynq device via serial port.
|
|
|
|
Sends reboot() through the serial console.
|
|
|
|
Args:
|
|
config: Application configuration.
|
|
callback: Optional progress callback.
|
|
|
|
Returns:
|
|
RebootResult with status.
|
|
"""
|
|
if not config.serial_port:
|
|
return RebootResult(
|
|
success=False,
|
|
message="No serial port configured",
|
|
method="serial",
|
|
)
|
|
|
|
if callback:
|
|
callback("start", f"Sending reboot via serial {config.serial_port}...")
|
|
|
|
try:
|
|
import serial
|
|
|
|
with serial.Serial(
|
|
config.serial_port,
|
|
config.serial_baudrate,
|
|
timeout=5,
|
|
) as ser:
|
|
ser.reset_input_buffer()
|
|
# Send Ctrl+C to break out of any running process
|
|
ser.write(b"\x03")
|
|
time.sleep(0.5)
|
|
# Send reboot command as function call
|
|
ser.write(b"reboot()\r\n")
|
|
ser.reset_input_buffer()
|
|
|
|
if callback:
|
|
callback("progress", "reboot() sent via serial")
|
|
|
|
return RebootResult(
|
|
success=True,
|
|
message="reboot() sent via serial",
|
|
method="serial",
|
|
)
|
|
except ImportError:
|
|
return RebootResult(
|
|
success=False,
|
|
message="pyserial not installed",
|
|
method="serial",
|
|
)
|
|
except serial.SerialException as e:
|
|
return RebootResult(
|
|
success=False,
|
|
message=f"Serial error: {e}",
|
|
method="serial",
|
|
)
|
|
except OSError as e:
|
|
return RebootResult(
|
|
success=False,
|
|
message=f"OS error: {e}",
|
|
method="serial",
|
|
)
|
|
|
|
|
|
def reboot_via_jtag(
|
|
config: Config,
|
|
callback: ProgressCallback = None,
|
|
) -> RebootResult:
|
|
"""Reboot the Zynq device via JTAG system reset.
|
|
|
|
Connects to the Zynq over JTAG through hw_server and issues
|
|
a full system reset (rst -system), then resumes ARM execution.
|
|
|
|
Args:
|
|
config: Application configuration.
|
|
callback: Optional progress callback.
|
|
|
|
Returns:
|
|
RebootResult with status.
|
|
"""
|
|
from vitis_checker import get_xsct_path
|
|
|
|
xsct = get_xsct_path(
|
|
xilinx_root=getattr(config, "xilinx_path", ""),
|
|
)
|
|
if not xsct:
|
|
return RebootResult(
|
|
success=False,
|
|
message="xsct not found — Xilinx tools not available",
|
|
method="jtag",
|
|
)
|
|
|
|
if callback:
|
|
callback("start", f"JTAG reset via xsct ({xsct})...")
|
|
|
|
# Build TCL: system reset then resume ARM execution (no stop)
|
|
tcl_script = """\
|
|
puts "INFO: Connecting to hw_server..."
|
|
connect
|
|
after 500
|
|
|
|
puts "INFO: Selecting ARM target..."
|
|
targets -set -nocase -filter {name =~ "arm*#0"}
|
|
after 200
|
|
|
|
puts "INFO: Issuing system reset..."
|
|
rst -system
|
|
after 1500
|
|
|
|
puts "INFO: Resuming ARM execution..."
|
|
catch { con }
|
|
|
|
puts "INFO: System reset complete — disconnecting..."
|
|
disconnect
|
|
exit
|
|
"""
|
|
|
|
try:
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".tcl", delete=False,
|
|
) as f:
|
|
f.write(tcl_script)
|
|
tcl_path = f.name
|
|
|
|
if callback:
|
|
callback("progress", "Running JTAG system reset...")
|
|
|
|
from vitis_checker import build_tool_command
|
|
result = subprocess.run(
|
|
build_tool_command(xsct, tcl_path),
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
os.unlink(tcl_path)
|
|
|
|
success = result.returncode == 0
|
|
output = (result.stdout + result.stderr).strip()
|
|
|
|
if callback:
|
|
callback(
|
|
"complete" if success else "error",
|
|
"JTAG reset " + ("succeeded" if success else "failed"),
|
|
)
|
|
|
|
return RebootResult(
|
|
success=success,
|
|
message=f"JTAG system reset {'OK' if success else 'failed'}",
|
|
method="jtag",
|
|
output=output[:2000],
|
|
)
|
|
except subprocess.TimeoutExpired:
|
|
try:
|
|
os.unlink(tcl_path)
|
|
except OSError:
|
|
pass
|
|
return RebootResult(
|
|
success=False,
|
|
message="JTAG reset timed out (30s)",
|
|
method="jtag",
|
|
)
|
|
except OSError as e:
|
|
try:
|
|
os.unlink(tcl_path)
|
|
except OSError:
|
|
pass
|
|
return RebootResult(
|
|
success=False,
|
|
message=f"JTAG reset error: {e}",
|
|
method="jtag",
|
|
)
|
|
|
|
|
|
def reboot_via_udp(
|
|
config: Config,
|
|
callback: ProgressCallback = None,
|
|
) -> RebootResult:
|
|
"""Reboot the Zynq device via UDP command.
|
|
|
|
Sends reboot() to the Zynq's derived UDP port (last-octet - 3).
|
|
|
|
Args:
|
|
config: Application configuration.
|
|
callback: Optional progress callback.
|
|
|
|
Returns:
|
|
RebootResult with status.
|
|
"""
|
|
ip = config.zynq_ip
|
|
port = _udp_port_from_ip(ip)
|
|
|
|
if callback:
|
|
callback("start", f"UDP reboot → {ip}:{port}")
|
|
|
|
ok, resp = _send_udp_command(ip, b"reboot()")
|
|
|
|
if callback:
|
|
if ok:
|
|
callback("complete", f"UDP reboot OK: {resp}")
|
|
else:
|
|
callback("error", f"UDP reboot failed: {resp}")
|
|
|
|
return RebootResult(
|
|
success=ok,
|
|
message=f"reboot() via UDP: {resp}" if ok else resp,
|
|
method="udp",
|
|
output=resp,
|
|
)
|
|
|
|
|
|
# ── Version query ────────────────────────────────────────────────────
|
|
|
|
|
|
def get_version_via_udp(
|
|
config: Config,
|
|
callback: ProgressCallback = None,
|
|
) -> UdpVersionResult:
|
|
"""Query Zynq firmware version via UDP.
|
|
|
|
Sends ver() to the Zynq's derived UDP port (last-octet - 3).
|
|
|
|
Args:
|
|
config: Application configuration.
|
|
callback: Optional progress callback.
|
|
|
|
Returns:
|
|
UdpVersionResult with version string.
|
|
"""
|
|
ip = config.zynq_ip
|
|
port = _udp_port_from_ip(ip)
|
|
|
|
if callback:
|
|
callback("start", f"UDP version query → {ip}:{port}")
|
|
|
|
ok, resp = _send_udp_command(ip, b"ver()")
|
|
|
|
if callback:
|
|
if ok:
|
|
callback("complete", f"Version: {resp}")
|
|
else:
|
|
callback("error", f"Version query failed: {resp}")
|
|
|
|
return UdpVersionResult(
|
|
success=ok,
|
|
message=f"Version: {resp}" if ok else resp,
|
|
version=resp if ok else "",
|
|
)
|
|
|
|
|
|
# ── Top-level dispatcher ─────────────────────────────────────────────
|
|
|
|
|
|
def reboot_zynq(
|
|
config: Config,
|
|
method: str = "auto",
|
|
timeout: int | None = None,
|
|
callback: ProgressCallback = None,
|
|
) -> RebootResult:
|
|
"""Reboot the Zynq device using the best available method.
|
|
|
|
Tries JTAG first, falls back to UDP then serial.
|
|
|
|
Args:
|
|
config: Application configuration.
|
|
method: Reboot method: 'auto', 'jtag', 'udp', or 'serial'.
|
|
timeout: Seconds to wait for reboot (ssh-only, kept for compat).
|
|
callback: Optional progress callback.
|
|
|
|
Returns:
|
|
RebootResult with status.
|
|
"""
|
|
timeout = timeout or config.reboot_timeout
|
|
|
|
if method == "jtag":
|
|
return reboot_via_jtag(config, callback)
|
|
elif method == "udp":
|
|
return reboot_via_udp(config, callback)
|
|
elif method == "serial":
|
|
return reboot_via_serial(config, callback)
|
|
|
|
# Auto: try JTAG first, then UDP, then serial
|
|
result = reboot_via_jtag(config, callback)
|
|
if result.success:
|
|
return result
|
|
|
|
if callback:
|
|
callback("progress", "JTAG failed, trying UDP...")
|
|
result = reboot_via_udp(config, callback)
|
|
if result.success:
|
|
return result
|
|
|
|
if callback:
|
|
callback("progress", "UDP failed, trying serial...")
|
|
|
|
return reboot_via_serial(config, callback)
|