feat: Add mDNS, watchdog, human-readable uptime, esp-fleet tool
Firmware: - mDNS announcement as <hostname>.local (configurable via Kconfig) - Task watchdog with 30s timeout and auto-reboot on hang - STATUS now returns human-readable uptime (e.g., 3d2h15m) and hostname Pi-side tools: - esp-cmd: mDNS hostname resolution (esp-cmd amber-maple.local STATUS) - esp-fleet: parallel command to all sensors (esp-fleet status) Tested on amber-maple — mDNS resolves, watchdog active, fleet tool works.
This commit is contained in:
@@ -8,7 +8,9 @@ DEFAULT_PORT = 5501
|
||||
TIMEOUT = 2.0
|
||||
|
||||
USAGE = """\
|
||||
Usage: esp-cmd <ip> <command> [args...]
|
||||
Usage: esp-cmd <host> <command> [args...]
|
||||
|
||||
Host can be an IP address or mDNS hostname (e.g., amber-maple.local).
|
||||
|
||||
Commands:
|
||||
STATUS Query device state (uptime, heap, RSSI, tx_power, rate)
|
||||
@@ -18,11 +20,19 @@ Commands:
|
||||
POWER <2-20> Set TX power in dBm (saved to NVS)
|
||||
|
||||
Examples:
|
||||
esp-cmd 192.168.1.50 STATUS
|
||||
esp-cmd 192.168.1.50 RATE 50
|
||||
esp-cmd 192.168.1.50 POWER 10
|
||||
esp-cmd 192.168.1.50 REBOOT
|
||||
esp-cmd 192.168.1.50 IDENTIFY"""
|
||||
esp-cmd amber-maple.local STATUS
|
||||
esp-cmd 192.168.129.30 RATE 50
|
||||
esp-cmd amber-maple.local IDENTIFY"""
|
||||
|
||||
|
||||
def resolve(host):
|
||||
"""Resolve hostname to IP address (supports mDNS .local)."""
|
||||
try:
|
||||
result = socket.getaddrinfo(host, DEFAULT_PORT, socket.AF_INET, socket.SOCK_DGRAM)
|
||||
return result[0][4][0]
|
||||
except socket.gaierror as e:
|
||||
print(f"ERR: cannot resolve {host}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -30,8 +40,9 @@ def main():
|
||||
print(USAGE)
|
||||
sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2)
|
||||
|
||||
ip = sys.argv[1]
|
||||
host = sys.argv[1]
|
||||
cmd = " ".join(sys.argv[2:]).strip()
|
||||
ip = resolve(host)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(TIMEOUT)
|
||||
@@ -41,7 +52,7 @@ def main():
|
||||
data, _ = sock.recvfrom(512)
|
||||
print(data.decode().strip())
|
||||
except socket.timeout:
|
||||
print(f"ERR: no reply from {ip}:{DEFAULT_PORT} (timeout {TIMEOUT}s)", file=sys.stderr)
|
||||
print(f"ERR: no reply from {host} ({ip}:{DEFAULT_PORT}), timeout {TIMEOUT}s", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except OSError as e:
|
||||
print(f"ERR: {e}", file=sys.stderr)
|
||||
|
||||
78
tools/esp-fleet
Executable file
78
tools/esp-fleet
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Query all ESP32 CSI sensors in parallel."""
|
||||
|
||||
import concurrent.futures
|
||||
import socket
|
||||
import sys
|
||||
|
||||
DEFAULT_PORT = 5501
|
||||
TIMEOUT = 2.0
|
||||
|
||||
SENSORS = [
|
||||
("muddy-storm", "muddy-storm.local"),
|
||||
("amber-maple", "amber-maple.local"),
|
||||
("hollow-acorn", "hollow-acorn.local"),
|
||||
]
|
||||
|
||||
USAGE = """\
|
||||
Usage: esp-fleet <command> [args...]
|
||||
|
||||
Sends a command to all known sensors in parallel and prints results.
|
||||
|
||||
Commands:
|
||||
status Query all devices
|
||||
identify Blink LEDs on all devices
|
||||
rate <10-100> Set ping rate on all devices
|
||||
power <2-20> Set TX power on all devices
|
||||
reboot Reboot all devices
|
||||
|
||||
Examples:
|
||||
esp-fleet status
|
||||
esp-fleet identify
|
||||
esp-fleet rate 50"""
|
||||
|
||||
|
||||
def query(name, host, cmd):
|
||||
"""Send command to one sensor, return (name, reply_or_error)."""
|
||||
try:
|
||||
info = socket.getaddrinfo(host, DEFAULT_PORT, socket.AF_INET, socket.SOCK_DGRAM)
|
||||
ip = info[0][4][0]
|
||||
except socket.gaierror:
|
||||
return (name, f"ERR: cannot resolve {host}")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(TIMEOUT)
|
||||
try:
|
||||
sock.sendto(cmd.encode(), (ip, DEFAULT_PORT))
|
||||
data, _ = sock.recvfrom(512)
|
||||
return (name, data.decode().strip())
|
||||
except socket.timeout:
|
||||
return (name, f"ERR: timeout ({ip})")
|
||||
except OSError as e:
|
||||
return (name, f"ERR: {e}")
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] in ("-h", "--help"):
|
||||
print(USAGE)
|
||||
sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2)
|
||||
|
||||
cmd = " ".join(sys.argv[1:]).strip().upper()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(SENSORS)) as pool:
|
||||
futures = {pool.submit(query, name, host, cmd): name for name, host in SENSORS}
|
||||
results = {}
|
||||
for f in concurrent.futures.as_completed(futures):
|
||||
name, reply = f.result()
|
||||
results[name] = reply
|
||||
|
||||
# Print in sensor order
|
||||
max_name = max(len(n) for n, _ in SENSORS)
|
||||
for name, _ in SENSORS:
|
||||
print(f"{name:<{max_name}} {results[name]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user