diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index 0f97938..ce8ba9f 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -23,30 +23,38 @@ idf.py reconfigure # Re-fetch managed components ## Deployed Sensors -| Name | IP | Location | -|------|-----|----------| -| muddy-storm | 192.168.129.29 | Living Room | -| amber-maple | 192.168.129.30 | Office | -| hollow-acorn | 192.168.129.31 | Kitchen | +| Name | IP | mDNS | Location | +|------|-----|------|----------| +| muddy-storm | 192.168.129.29 | muddy-storm.local | Living Room | +| amber-maple | 192.168.129.30 | amber-maple.local | Office | +| hollow-acorn | 192.168.129.31 | hollow-acorn.local | Kitchen | **Target:** `192.168.129.11:5500` (Pi) | **Cmd port:** `5501` ## Remote Management (esp-cmd) ```bash -esp-cmd STATUS # Uptime, heap, RSSI, tx_power, rate -esp-cmd IDENTIFY # LED solid 5s (find the device) -esp-cmd RATE 50 # Set ping rate to 50 Hz (NVS saved) -esp-cmd POWER 15 # Set TX power to 15 dBm (NVS saved) -esp-cmd REBOOT # Restart device +esp-cmd STATUS # Uptime, heap, RSSI, tx_power, rate +esp-cmd IDENTIFY # LED solid 5s (find the device) +esp-cmd RATE 50 # Set ping rate to 50 Hz (NVS saved) +esp-cmd POWER 15 # Set TX power to 15 dBm (NVS saved) +esp-cmd REBOOT # Restart device ``` -Examples with deployed sensors: +Host can be an IP or mDNS name (`amber-maple.local`). ```bash -esp-cmd 192.168.129.29 STATUS # muddy-storm -esp-cmd 192.168.129.30 IDENTIFY # amber-maple -esp-cmd 192.168.129.31 RATE 25 # hollow-acorn +esp-cmd amber-maple.local STATUS # Single device via mDNS +esp-cmd 192.168.129.29 IDENTIFY # Single device via IP +``` + +## Fleet Management (esp-fleet) + +```bash +esp-fleet status # Query all sensors at once +esp-fleet identify # Blink all LEDs +esp-fleet rate 50 # Set rate on all devices +esp-fleet reboot # Reboot entire fleet ``` ### LED States @@ -83,6 +91,8 @@ nc -lu 5500 | wc -l # Count packets/sec (Ctrl+C) | Example Connection Configuration → Password | WiFi password | | CSI UDP Configuration → IP | `192.168.129.11` | | CSI UDP Configuration → Port | `5500` | +| CSI UDP Configuration → Cmd port | `5501` | +| CSI UDP Configuration → Hostname | mDNS name (e.g., `amber-maple`) | ## CSI Data Format @@ -99,6 +109,7 @@ secondary_channel,timestamp,ant,sig_len,rx_state,len,first_word,"[I,Q,...]" | `get-started/csi_recv_router/main/app_main.c` | Main firmware source | | `get-started/csi_recv_router/main/Kconfig.projbuild` | UDP/cmd port config | | `tools/esp-cmd` | Pi-side management CLI | +| `tools/esp-fleet` | Fleet-wide command tool | | `get-started/csi_recv_router/sdkconfig.defaults` | SDK defaults | | `get-started/csi_recv_router/main/idf_component.yml` | Dependencies | | `get-started/csi_recv_router/CMakeLists.txt` | Build config | diff --git a/get-started/csi_recv_router/main/Kconfig.projbuild b/get-started/csi_recv_router/main/Kconfig.projbuild index 35bacf4..6e7703c 100644 --- a/get-started/csi_recv_router/main/Kconfig.projbuild +++ b/get-started/csi_recv_router/main/Kconfig.projbuild @@ -20,4 +20,10 @@ menu "CSI UDP Configuration" help UDP port for receiving management commands (STATUS, REBOOT, etc.). + config CSI_HOSTNAME + string "Device mDNS hostname" + default "esp32-csi" + help + mDNS hostname for this device. Accessible as .local on the network. + endmenu diff --git a/get-started/csi_recv_router/main/app_main.c b/get-started/csi_recv_router/main/app_main.c index fda4bbb..3a5597a 100644 --- a/get-started/csi_recv_router/main/app_main.c +++ b/get-started/csi_recv_router/main/app_main.c @@ -29,7 +29,9 @@ #include "esp_netif.h" #include "esp_now.h" #include "esp_timer.h" +#include "esp_task_wdt.h" #include "driver/gpio.h" +#include "mdns.h" #include "lwip/inet.h" #include "lwip/netdb.h" @@ -400,7 +402,10 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) /* STATUS */ if (strncmp(cmd, "STATUS", 6) == 0) { - int64_t uptime_s = esp_timer_get_time() / 1000000LL; + int64_t up = esp_timer_get_time() / 1000000LL; + int days = (int)(up / 86400); + int hours = (int)((up % 86400) / 3600); + int mins = (int)((up % 3600) / 60); uint32_t heap = esp_get_free_heap_size(); wifi_ap_record_t ap; @@ -409,9 +414,19 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size) rssi = ap.rssi; } + char uptime_str[32]; + if (days > 0) { + snprintf(uptime_str, sizeof(uptime_str), "%dd%dh%dm", days, hours, mins); + } else if (hours > 0) { + snprintf(uptime_str, sizeof(uptime_str), "%dh%dm", hours, mins); + } else { + snprintf(uptime_str, sizeof(uptime_str), "%dm", mins); + } + snprintf(reply, reply_size, - "OK STATUS uptime=%lld heap=%lu rssi=%d tx_power=%d rate=%d", - uptime_s, (unsigned long)heap, rssi, (int)s_tx_power_dbm, s_send_frequency); + "OK STATUS uptime=%s heap=%lu rssi=%d tx_power=%d rate=%d hostname=%s", + uptime_str, (unsigned long)heap, rssi, (int)s_tx_power_dbm, + s_send_frequency, CONFIG_CSI_HOSTNAME); return strlen(reply); } @@ -526,6 +541,21 @@ void app_main() esp_wifi_set_max_tx_power(s_tx_power_dbm * 4); ESP_LOGI(TAG, "TX power set to %d dBm", (int)s_tx_power_dbm); + /* mDNS: announce as .local */ + ESP_ERROR_CHECK(mdns_init()); + mdns_hostname_set(CONFIG_CSI_HOSTNAME); + mdns_instance_name_set("ESP32 CSI Sensor"); + ESP_LOGI(TAG, "mDNS hostname: %s.local", CONFIG_CSI_HOSTNAME); + + /* Watchdog: 30s timeout, auto-reboot on hang */ + esp_task_wdt_config_t wdt_cfg = { + .timeout_ms = 30000, + .idle_core_mask = (1 << 0) | (1 << 1), + .trigger_panic = true, + }; + ESP_ERROR_CHECK(esp_task_wdt_reconfigure(&wdt_cfg)); + ESP_LOGI(TAG, "Watchdog configured: 30s timeout"); + s_led_mode = LED_SLOW_BLINK; udp_socket_init(); diff --git a/get-started/csi_recv_router/main/idf_component.yml b/get-started/csi_recv_router/main/idf_component.yml index 7f4789b..aaadec8 100644 --- a/get-started/csi_recv_router/main/idf_component.yml +++ b/get-started/csi_recv_router/main/idf_component.yml @@ -2,3 +2,4 @@ dependencies: idf: ">=4.4.1" esp_csi_gain_ctrl: ">=0.1.4" + espressif/mdns: ">=1.0.0" diff --git a/get-started/csi_recv_router/sdkconfig.sample b/get-started/csi_recv_router/sdkconfig.sample index de3d29f..78336af 100644 --- a/get-started/csi_recv_router/sdkconfig.sample +++ b/get-started/csi_recv_router/sdkconfig.sample @@ -437,6 +437,7 @@ CONFIG_PARTITION_TABLE_MD5=y CONFIG_CSI_UDP_TARGET_IP="192.168.129.11" CONFIG_CSI_UDP_TARGET_PORT=5500 CONFIG_CSI_CMD_PORT=5501 +CONFIG_CSI_HOSTNAME="your-hostname-here" # end of CSI UDP Configuration # diff --git a/tools/esp-cmd b/tools/esp-cmd index 430c044..08caf7a 100755 --- a/tools/esp-cmd +++ b/tools/esp-cmd @@ -8,7 +8,9 @@ DEFAULT_PORT = 5501 TIMEOUT = 2.0 USAGE = """\ -Usage: esp-cmd [args...] +Usage: esp-cmd [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) diff --git a/tools/esp-fleet b/tools/esp-fleet new file mode 100755 index 0000000..d9bd9b9 --- /dev/null +++ b/tools/esp-fleet @@ -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 [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()