**JTAG check**: Switch from Vivado to xsct - xsct is already proven to work (JTAG reboot succeeds) - Much lighter than Vivado (no GUI infrastructure / open_hw_manager) - Uses -eval inline TCL: connect; targets; disconnect; quit - Removes dead tempfile/Path/os imports **Windows COM port**: Add normalize_port() to serial_monitor.py - COM ports >= 10 require NT namespace prefix (\.\COM11) - Without it: PermissionError(13, '拒绝访问。') - Applied at all 7 call sites: serial_monitor (5), reboot_manager, widgets
402 lines
10 KiB
Python
402 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
|
|
from serial_monitor import normalize_port
|
|
|
|
with serial.Serial(
|
|
normalize_port(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)
|