Add V1 protocol discovery and upgrade support
This commit is contained in:
1 parent
63a32aaf02
commit
d50be29246
5 files changed
+240
-6
No files matched your search
@@ -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