Files
Zynq_Flasher/src/reboot_manager.py
T
yuysh 66c04da5e0 fix: stream Vivado output live, remove broken normalize_port
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.
2026-06-11 17:24:31 +08:00

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)