Files
embedded-app-pc-debug/scripts/v1_discover.py
T

85 lines
2.8 KiB
Python

#!/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())