Add V1 protocol discovery and upgrade support
This commit is contained in:
1 parent
63a32aaf02
commit
d50be29246
5 files changed
+238
-4
No files matched your search
@@ -11,7 +11,7 @@ description: 用于 Windows 电脑端的嵌入式应用调试,包括远程编
|
|||||||
|
|
||||||
- 涉及远程编译、工程定位、软链接、下载产物时,读取 [references/build-workflow.md](references/build-workflow.md)。
|
- 涉及远程编译、工程定位、软链接、下载产物时,读取 [references/build-workflow.md](references/build-workflow.md)。
|
||||||
- 涉及 NFS 模块部署、`DxMain`、开机启动脚本、工厂模式或加密 param 时,读取 [references/application-debugging.md](references/application-debugging.md)。
|
- 涉及 NFS 模块部署、`DxMain`、开机启动脚本、工厂模式或加密 param 时,读取 [references/application-debugging.md](references/application-debugging.md)。
|
||||||
- 涉及广播搜索、基础信息、EPFP 文件传输或升级时,读取 [references/device-protocols.md](references/device-protocols.md)。
|
- 涉及 V1/V2 广播搜索、工厂/产测命令、统一配置、EPFP 文件传输、音视频协议或升级时,读取 [references/device-protocols.md](references/device-protocols.md)。
|
||||||
- 涉及串口监测、升级成功判据或故障定位时,读取 [references/validation-and-diagnostics.md](references/validation-and-diagnostics.md)。
|
- 涉及串口监测、升级成功判据或故障定位时,读取 [references/validation-and-diagnostics.md](references/validation-and-diagnostics.md)。
|
||||||
- 涉及 Linux 打包任务与 Windows 设备任务之间交接时,读取 [references/cross-host-coordination.md](references/cross-host-coordination.md)。
|
- 涉及 Linux 打包任务与 Windows 设备任务之间交接时,读取 [references/cross-host-coordination.md](references/cross-host-coordination.md)。
|
||||||
|
|
||||||
@@ -60,7 +60,9 @@ description: 用于 Windows 电脑端的嵌入式应用调试,包括远程编
|
|||||||
|
|
||||||
## 工具脚本
|
## 工具脚本
|
||||||
|
|
||||||
- `scripts/device_upgrade.py`:V2 `30101` 身份预检与 `EPFP/4001` 升级。缺少 `--execute` 时绝不发送 ROM。
|
- `scripts/v1_discover.py`:V1 UDP `4942` 搜索(`0x5A0EFF00/01`);可用 `--interface` 指定本机 IPv4。端口冲突时停止,不抢占进程。
|
||||||
|
- `scripts/v1_command.py`:V1 TCP `4941` 通用 CMD/Data 请求器;默认仅预检,所有命令只有显式 `--execute` 才发送。它不解析每条命令的业务语义,副作用命令必须先查对应协议并核对设备身份。
|
||||||
|
- `scripts/device_upgrade.py`:默认使用 V2 `30101` 身份预检;可用 `--protocol v1` 通过 V1 `0x5A0E2104` 核验后执行 EPFP/4001。V1 与 EPFP 的兼容性须由目标固件资料确认。缺少 `--execute` 时绝不连接升级端口或发送 ROM。
|
||||||
- `scripts/serial_monitor.ps1`:无第三方依赖的 Windows 串口实时输出、原始字节保存和带时间戳文本日志。
|
- `scripts/serial_monitor.ps1`:无第三方依赖的 Windows 串口实时输出、原始字节保存和带时间戳文本日志。
|
||||||
- `scripts/serial_web_console.ps1`:使用 Windows PowerShell 5.1 启动本地串口网页控制台;由单个进程独占 COM 口并同时负责接收、记录和发送,页面仅监听 `127.0.0.1`,写操作和关闭通知使用随机会话令牌。网页只提供用户主动输入和发送,不启动 U-Boot 持续按键或自动切换 A/B 分区。最后网页关闭后服务退出,不部署常驻管理进程或自动重连。必须用 `powershell.exe` 启动,不要改用 `pwsh.exe`。
|
- `scripts/serial_web_console.ps1`:使用 Windows PowerShell 5.1 启动本地串口网页控制台;由单个进程独占 COM 口并同时负责接收、记录和发送,页面仅监听 `127.0.0.1`,写操作和关闭通知使用随机会话令牌。网页只提供用户主动输入和发送,不启动 U-Boot 持续按键或自动切换 A/B 分区。最后网页关闭后服务退出,不部署常驻管理进程或自动重连。必须用 `powershell.exe` 启动,不要改用 `pwsh.exe`。
|
||||||
- `scripts/v2_discover.py`:枚举适合广播的 Windows IPv4 网卡,从每个接口的本地 `8951` 向设备 `8941` 发送 V2 广播,并严格筛选完整 SN/型号。
|
- `scripts/v2_discover.py`:枚举适合广播的 Windows IPv4 网卡,从每个接口的本地 `8951` 向设备 `8941` 发送 V2 广播,并严格筛选完整 SN/型号。
|
||||||
|
|||||||
@@ -12,6 +12,43 @@
|
|||||||
- `C:\Users\ZK\Downloads\工厂协议V1.md`
|
- `C:\Users\ZK\Downloads\工厂协议V1.md`
|
||||||
- `E:\CodeSpace\git-pro\eapil_qt_player_v2\EpProtocolDemo`
|
- `E:\CodeSpace\git-pro\eapil_qt_player_v2\EpProtocolDemo`
|
||||||
|
|
||||||
|
## 协议资料总览
|
||||||
|
|
||||||
|
本技能覆盖的协议资料不止设备搜索和升级。按当前本机资料,范围包括:
|
||||||
|
|
||||||
|
| 协议族 | 资料范围 | 本技能的使用方式 |
|
||||||
|
|---|---|---|
|
||||||
|
| V1 工厂协议 | 66 条工厂/产测命令;UDP 搜索与 TCP 控制 | `scripts/v1_discover.py` 搜索;`scripts/v1_command.py` 对文档中任意 CMD 发送 JSON 请求 |
|
||||||
|
| V2 广播发现 | EPBC/EPBD、UDP 8941/8951、cmd 60000 | `scripts/v2_discover.py` |
|
||||||
|
| V2 统一配置 | cmd 30101 起,覆盖基本信息、网络、音视频、音频、设备操作、SD 卡、夜视、时间、OSD、IO 等文档章节 | TCP 8942;当前脚本仅做身份查询,其他命令按文档单独实现/调用;不得把语义协议误当作固定传输协议 |
|
||||||
|
| V2 产测 | cmd 1–26,LED、按键、老化、带宽、PCBA、屏幕、MIC、SD、芯片、镜头等 | 文档定义命令语义;需使用匹配固件提供的传输封装 |
|
||||||
|
| TCP 文件传输 | EPFP,2001/2002/3001/4001,端口 8943 | `scripts/device_upgrade.py` 实现 4001 固件升级;其余命令保留文档语义,不由该脚本执行 |
|
||||||
|
| TCP 音视频播放 | 端口 8944,cmd 0x01–0x05 | 长连接心跳、解码器列表、播放控制及媒体帧;不是设备发现或升级通道 |
|
||||||
|
| V2 工厂整合协议 | 工厂扩展命令及设备信息接口 | 按资料版本与设备支持情况使用;与 V1 工厂命令空间不能混用 |
|
||||||
|
|
||||||
|
“覆盖”指协议字段、命令范围、边界和现有脚本能力都在参考文档中可查;不代表每种协议都有通用执行器,也不代表所有固件都启用了每条命令。新增写操作前须逐条查对应协议的请求、响应和副作用。
|
||||||
|
|
||||||
|
## V1 工厂协议
|
||||||
|
|
||||||
|
V1 使用独立于 V2 的报文,不带 `EPBP`/`EPTP`/`EPFP` 同步字:
|
||||||
|
|
||||||
|
```text
|
||||||
|
uint32 CMD (network byte order)
|
||||||
|
+ uint32 Data length (network byte order)
|
||||||
|
+ Data bytes (normally UTF-8 JSON; optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 功能 | 传输 | 端口 | 命令 |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| 设备发现请求/响应 | UDP 广播 | 设备 4942 | `0x5A0EFF00` / `0x5A0EFF01` |
|
||||||
|
| 工厂/产测控制 | TCP | 4941 | 按 V1 文档中各接口的 CMD |
|
||||||
|
|
||||||
|
搜索请求 Data 为 `{"session":<随机整数>}`。只接受 `0x5A0EFF01` 且回显本次 session 的响应。设备响应 JSON 保留原始字段;协议版本之间字段可能不同,型号、SN、IP、MAC 等信息必须根据实际响应展示,缺失即标为不可用。多网卡或设备搜索工具已占用 UDP 4942 时,先确定本机接口/端口状态;不结束占用进程。V1 搜索用 `scripts/v1_discover.py`,对单个接口显式指定 `--interface <本机IPv4>`。
|
||||||
|
|
||||||
|
`scripts/v1_command.py` 是通用 TCP 请求器:指定 IP、CMD 和 JSON Data;默认仅显示预检,不建立连接。只有显式 `--execute` 才发送。它校验 TCP 响应头、长度和响应 CMD,但不解释各命令的业务成功字段、不判断写操作安全性,也不执行设备身份核验。因此必须先将发现的设备与完整 SN/型号人工核对;对写配置、复位、格式化、烧号或其他有副作用命令,先核实请求字段及后果并取得本次操作确认。不得用该通用脚本发送 ROM。
|
||||||
|
|
||||||
|
V1 工厂协议文档列出的 66 条命令及字段定义是命令语义来源。传输层客户端可承载这些 CMD,但各命令是否被具体型号/固件实现,仍需用对应版本文档或设备响应确认。V1 示例工程只实现了搜索和前五个 TCP 示例命令,不代表其它 V1 命令已在设备上验证。
|
||||||
|
|
||||||
## V2 协议矩阵
|
## V2 协议矩阵
|
||||||
|
|
||||||
| 功能 | 传输与端口 | 帧格式 |
|
| 功能 | 传输与端口 | 帧格式 |
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""设备 V2 基础信息预检与 EPFP/4001 固件升级工具。"""
|
"""V1/V2 身份预检与 EPFP/4001 固件升级工具。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|||||||
INFO_PORT = 8942
|
INFO_PORT = 8942
|
||||||
UPGRADE_PORT = 8943
|
UPGRADE_PORT = 8943
|
||||||
INFO_COMMAND = 30101
|
INFO_COMMAND = 30101
|
||||||
|
V1_INFO_PORT = 4941
|
||||||
|
V1_INFO_COMMAND = 0x5A0E2104
|
||||||
UPGRADE_COMMAND = 4001
|
UPGRADE_COMMAND = 4001
|
||||||
MAX_JSON_SIZE = 1024 * 1024
|
MAX_JSON_SIZE = 1024 * 1024
|
||||||
MAX_STATUS_SIZE = 1024 * 1024
|
MAX_STATUS_SIZE = 1024 * 1024
|
||||||
@@ -116,6 +118,30 @@ def query_device_info(host: str, timeout: float, logger: EventLogger) -> Dict[st
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def query_v1_device_info(host: str, timeout: float, logger: EventLogger) -> Dict[str, Any]:
|
||||||
|
"""通过 V1 工厂 TCP/2104 获取设备身份与固件字段。"""
|
||||||
|
logger.emit("v1_info_request", host=host, port=V1_INFO_PORT,
|
||||||
|
command="0x%08X" % V1_INFO_COMMAND)
|
||||||
|
with socket.create_connection((host, V1_INFO_PORT), timeout=timeout) as sock:
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
sock.sendall(struct.pack("!II", V1_INFO_COMMAND, 0))
|
||||||
|
header = recv_exact(sock, 8)
|
||||||
|
command, size = struct.unpack("!II", header)
|
||||||
|
if command != V1_INFO_COMMAND or size > MAX_JSON_SIZE:
|
||||||
|
raise UpgradeError("V1 固件信息响应 CMD 或长度异常")
|
||||||
|
payload = recv_exact(sock, size)
|
||||||
|
try:
|
||||||
|
response = json.loads(payload.decode("utf-8"))
|
||||||
|
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
||||||
|
raise UpgradeError("V1 固件信息响应不是有效 JSON:%s" % exc) from exc
|
||||||
|
if not isinstance(response, dict) or response.get("code") != 0:
|
||||||
|
raise UpgradeError("V1 固件信息查询失败或响应格式异常")
|
||||||
|
if not response.get("sn") or not response.get("dev_type"):
|
||||||
|
raise UpgradeError("V1 固件信息缺少 SN 或 dev_type,拒绝升级")
|
||||||
|
logger.emit("device_info", protocol="V1", response=response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def response_data(response: Dict[str, Any]) -> Dict[str, Any]:
|
def response_data(response: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""取得响应 data 对象,格式错误时拒绝继续。"""
|
"""取得响应 data 对象,格式错误时拒绝继续。"""
|
||||||
|
|
||||||
@@ -379,9 +405,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
"""创建命令行参数解析器。"""
|
"""创建命令行参数解析器。"""
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="V2 设备 30101 身份预检与 EPFP/4001 ROM 升级;默认仅预检。"
|
description="设备身份预检与 EPFP/4001 ROM 升级;默认仅预检。"
|
||||||
)
|
)
|
||||||
parser.add_argument("--host", required=True, help="目标设备 IPv4/主机名")
|
parser.add_argument("--host", required=True, help="目标设备 IPv4/主机名")
|
||||||
|
parser.add_argument("--protocol", choices=("v1", "v2"), default="v2",
|
||||||
|
help="身份查询协议;固件传输均使用 EPFP/4001")
|
||||||
parser.add_argument("--expected-sn", required=True, help="必须精确匹配的完整设备 SN")
|
parser.add_argument("--expected-sn", required=True, help="必须精确匹配的完整设备 SN")
|
||||||
parser.add_argument("--expected-model", help="可选,必须出现在设备型号字段中的型号")
|
parser.add_argument("--expected-model", help="可选,必须出现在设备型号字段中的型号")
|
||||||
parser.add_argument("--rom", required=True, type=Path, help="ROM 文件绝对或相对路径")
|
parser.add_argument("--rom", required=True, type=Path, help="ROM 文件绝对或相对路径")
|
||||||
@@ -422,10 +450,23 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|||||||
logger.emit("mode", execute=bool(args.execute), log_file=str(logger.path))
|
logger.emit("mode", execute=bool(args.execute), log_file=str(logger.path))
|
||||||
rom = validate_rom(args.rom)
|
rom = validate_rom(args.rom)
|
||||||
logger.emit("rom", **rom)
|
logger.emit("rom", **rom)
|
||||||
|
if args.protocol == "v1":
|
||||||
|
response = query_v1_device_info(args.host, args.connect_timeout, logger)
|
||||||
|
if response.get("sn") != args.expected_sn:
|
||||||
|
raise UpgradeError("设备 SN 不匹配:期望 %s,实际 %r" % (args.expected_sn, response.get("sn")))
|
||||||
|
if args.expected_model and args.expected_model not in str(response.get("dev_type", "")):
|
||||||
|
raise UpgradeError("设备型号不匹配:期望 %s,实际 %r" % (args.expected_model, response.get("dev_type")))
|
||||||
|
data = {"sn": response.get("sn"), "inner": {"type": response.get("dev_type"),
|
||||||
|
"version": response.get("firmware")}, "outer": {"version": response.get("dev_ver")},
|
||||||
|
"hardware": {"version": response.get("hardware")}}
|
||||||
|
models = [str(response["dev_type"])]
|
||||||
|
else:
|
||||||
response = query_device_info(args.host, args.connect_timeout, logger)
|
response = query_device_info(args.host, args.connect_timeout, logger)
|
||||||
data, models = validate_identity(response, args.expected_sn, args.expected_model)
|
data, models = validate_identity(response, args.expected_sn, args.expected_model)
|
||||||
summary = {
|
summary = {
|
||||||
"host": args.host,
|
"host": args.host,
|
||||||
|
"identity_protocol": args.protocol,
|
||||||
|
"upgrade_protocol_compatibility": "V1 身份查询 + EPFP/4001;仍须依据目标固件资料确认兼容性",
|
||||||
"sn": data.get("sn"),
|
"sn": data.get("sn"),
|
||||||
"mac": data.get("mac"),
|
"mac": data.get("mac"),
|
||||||
"models": models,
|
"models": models,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""V1 工厂 TCP 命令客户端;发送任何命令都必须显式使用 --execute。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MAX_DATA = 16 * 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def receive_exact(sock: socket.socket, length: int) -> bytes:
|
||||||
|
chunks = bytearray()
|
||||||
|
while len(chunks) < length:
|
||||||
|
part = sock.recv(length - len(chunks))
|
||||||
|
if not part:
|
||||||
|
raise ConnectionError("peer closed before complete V1 frame arrived")
|
||||||
|
chunks.extend(part)
|
||||||
|
return bytes(chunks)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("host", help="device IP selected from discovery results")
|
||||||
|
parser.add_argument("command", help="V1 CMD, decimal or 0x-prefixed")
|
||||||
|
parser.add_argument("--data", default="", help="UTF-8 JSON/text payload (omit for empty Data)")
|
||||||
|
parser.add_argument("--timeout", type=float, default=8.0)
|
||||||
|
parser.add_argument("--execute", action="store_true", help="actually send the command")
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
command = int(args.command, 0)
|
||||||
|
payload = args.data.encode("utf-8")
|
||||||
|
if not 0 <= command <= 0xFFFFFFFF:
|
||||||
|
raise ValueError("CMD must fit uint32")
|
||||||
|
if len(payload) > MAX_DATA:
|
||||||
|
raise ValueError("request Data exceeds 16 MiB limit")
|
||||||
|
if args.data:
|
||||||
|
json.loads(args.data) # V1 factory command payloads are documented JSON.
|
||||||
|
if not args.execute:
|
||||||
|
print(json.dumps({"preflight_only": True, "host": args.host, "port": 4941,
|
||||||
|
"command": "0x%08X" % command, "data_bytes": len(payload),
|
||||||
|
"message": "No network connection opened; add --execute to send."}, ensure_ascii=False))
|
||||||
|
return 0
|
||||||
|
request = struct.pack("!II", command, len(payload)) + payload
|
||||||
|
with socket.create_connection((args.host, 4941), timeout=args.timeout) as sock:
|
||||||
|
sock.settimeout(args.timeout)
|
||||||
|
sock.sendall(request)
|
||||||
|
header = receive_exact(sock, 8)
|
||||||
|
response_command, response_length = struct.unpack("!II", header)
|
||||||
|
if response_length > MAX_DATA:
|
||||||
|
raise ValueError("response Data exceeds 16 MiB limit")
|
||||||
|
response = receive_exact(sock, response_length)
|
||||||
|
result = {"request_command": "0x%08X" % command,
|
||||||
|
"response_command": "0x%08X" % response_command,
|
||||||
|
"response_length": response_length,
|
||||||
|
"response_data": response.decode("utf-8", errors="replace")}
|
||||||
|
print(json.dumps(result, ensure_ascii=False))
|
||||||
|
if response_command != command:
|
||||||
|
return 2
|
||||||
|
return 0
|
||||||
|
except (OSError, ValueError, ConnectionError, json.JSONDecodeError) as exc:
|
||||||
|
print("V1 command error: %s" % exc, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""搜索 V1 工厂协议设备(UDP 4942)。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ipaddress
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
PORT = 4942
|
||||||
|
REQUEST = 0x5A0EFF00
|
||||||
|
RESPONSE = 0x5A0EFF01
|
||||||
|
MAX_DATA = 1024 * 1024
|
||||||
|
|
||||||
|
|
||||||
|
def frame(command: int, payload: bytes) -> bytes:
|
||||||
|
return struct.pack("!II", command, len(payload)) + payload
|
||||||
|
|
||||||
|
|
||||||
|
def parse_frame(packet: bytes) -> tuple[int, bytes]:
|
||||||
|
if len(packet) < 8:
|
||||||
|
raise ValueError("packet shorter than V1 header")
|
||||||
|
command, length = struct.unpack("!II", packet[:8])
|
||||||
|
if length > MAX_DATA or len(packet) != 8 + length:
|
||||||
|
raise ValueError("invalid V1 payload length")
|
||||||
|
return command, packet[8:]
|
||||||
|
|
||||||
|
|
||||||
|
def discover(timeout: float, interface: str | None) -> int:
|
||||||
|
session = secrets.randbelow(0x7FFFFFFF) + 1
|
||||||
|
request = frame(REQUEST, json.dumps({"session": session}).encode("utf-8"))
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||||
|
sock.settimeout(min(0.5, timeout))
|
||||||
|
try:
|
||||||
|
# Bind the documented client port. If another discovery utility owns
|
||||||
|
# it, stop and report the conflict instead of silently using another port.
|
||||||
|
sock.bind((interface or "0.0.0.0", PORT))
|
||||||
|
sock.sendto(request, ("255.255.255.255", PORT))
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
seen: set[tuple[str, str]] = set()
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
try:
|
||||||
|
packet, address = sock.recvfrom(MAX_DATA + 8)
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
command, data = parse_frame(packet)
|
||||||
|
obj: Any = json.loads(data.decode("utf-8"))
|
||||||
|
if command != RESPONSE or not isinstance(obj, dict):
|
||||||
|
continue
|
||||||
|
if obj.get("session") != session:
|
||||||
|
continue
|
||||||
|
key = (address[0], str(obj.get("sn", "")))
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
print(json.dumps({"source_ip": address[0], "response": obj}, ensure_ascii=False))
|
||||||
|
except (ValueError, UnicodeDecodeError, json.JSONDecodeError):
|
||||||
|
continue
|
||||||
|
return 0
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--timeout", type=float, default=3.0)
|
||||||
|
parser.add_argument("--interface", help="optional local IPv4 address to bind")
|
||||||
|
args = parser.parse_args()
|
||||||
|
if args.timeout <= 0:
|
||||||
|
parser.error("--timeout must be positive")
|
||||||
|
if args.interface:
|
||||||
|
ipaddress.IPv4Address(args.interface)
|
||||||
|
return discover(args.timeout, args.interface)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in new issue
Block a user