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:
@@ -23,30 +23,38 @@ idf.py reconfigure # Re-fetch managed components
|
|||||||
|
|
||||||
## Deployed Sensors
|
## Deployed Sensors
|
||||||
|
|
||||||
| Name | IP | Location |
|
| Name | IP | mDNS | Location |
|
||||||
|------|-----|----------|
|
|------|-----|------|----------|
|
||||||
| muddy-storm | 192.168.129.29 | Living Room |
|
| muddy-storm | 192.168.129.29 | muddy-storm.local | Living Room |
|
||||||
| amber-maple | 192.168.129.30 | Office |
|
| amber-maple | 192.168.129.30 | amber-maple.local | Office |
|
||||||
| hollow-acorn | 192.168.129.31 | Kitchen |
|
| hollow-acorn | 192.168.129.31 | hollow-acorn.local | Kitchen |
|
||||||
|
|
||||||
**Target:** `192.168.129.11:5500` (Pi) | **Cmd port:** `5501`
|
**Target:** `192.168.129.11:5500` (Pi) | **Cmd port:** `5501`
|
||||||
|
|
||||||
## Remote Management (esp-cmd)
|
## Remote Management (esp-cmd)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
esp-cmd <ip> STATUS # Uptime, heap, RSSI, tx_power, rate
|
esp-cmd <host> STATUS # Uptime, heap, RSSI, tx_power, rate
|
||||||
esp-cmd <ip> IDENTIFY # LED solid 5s (find the device)
|
esp-cmd <host> IDENTIFY # LED solid 5s (find the device)
|
||||||
esp-cmd <ip> RATE 50 # Set ping rate to 50 Hz (NVS saved)
|
esp-cmd <host> RATE 50 # Set ping rate to 50 Hz (NVS saved)
|
||||||
esp-cmd <ip> POWER 15 # Set TX power to 15 dBm (NVS saved)
|
esp-cmd <host> POWER 15 # Set TX power to 15 dBm (NVS saved)
|
||||||
esp-cmd <ip> REBOOT # Restart device
|
esp-cmd <host> REBOOT # Restart device
|
||||||
```
|
```
|
||||||
|
|
||||||
Examples with deployed sensors:
|
Host can be an IP or mDNS name (`amber-maple.local`).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
esp-cmd 192.168.129.29 STATUS # muddy-storm
|
esp-cmd amber-maple.local STATUS # Single device via mDNS
|
||||||
esp-cmd 192.168.129.30 IDENTIFY # amber-maple
|
esp-cmd 192.168.129.29 IDENTIFY # Single device via IP
|
||||||
esp-cmd 192.168.129.31 RATE 25 # hollow-acorn
|
```
|
||||||
|
|
||||||
|
## 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
|
### LED States
|
||||||
@@ -83,6 +91,8 @@ nc -lu 5500 | wc -l # Count packets/sec (Ctrl+C)
|
|||||||
| Example Connection Configuration → Password | WiFi password |
|
| Example Connection Configuration → Password | WiFi password |
|
||||||
| CSI UDP Configuration → IP | `192.168.129.11` |
|
| CSI UDP Configuration → IP | `192.168.129.11` |
|
||||||
| CSI UDP Configuration → Port | `5500` |
|
| CSI UDP Configuration → Port | `5500` |
|
||||||
|
| CSI UDP Configuration → Cmd port | `5501` |
|
||||||
|
| CSI UDP Configuration → Hostname | mDNS name (e.g., `amber-maple`) |
|
||||||
|
|
||||||
## CSI Data Format
|
## 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/app_main.c` | Main firmware source |
|
||||||
| `get-started/csi_recv_router/main/Kconfig.projbuild` | UDP/cmd port config |
|
| `get-started/csi_recv_router/main/Kconfig.projbuild` | UDP/cmd port config |
|
||||||
| `tools/esp-cmd` | Pi-side management CLI |
|
| `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/sdkconfig.defaults` | SDK defaults |
|
||||||
| `get-started/csi_recv_router/main/idf_component.yml` | Dependencies |
|
| `get-started/csi_recv_router/main/idf_component.yml` | Dependencies |
|
||||||
| `get-started/csi_recv_router/CMakeLists.txt` | Build config |
|
| `get-started/csi_recv_router/CMakeLists.txt` | Build config |
|
||||||
|
|||||||
@@ -20,4 +20,10 @@ menu "CSI UDP Configuration"
|
|||||||
help
|
help
|
||||||
UDP port for receiving management commands (STATUS, REBOOT, etc.).
|
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 <hostname>.local on the network.
|
||||||
|
|
||||||
endmenu
|
endmenu
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "esp_now.h"
|
#include "esp_now.h"
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
|
#include "esp_task_wdt.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
|
#include "mdns.h"
|
||||||
|
|
||||||
#include "lwip/inet.h"
|
#include "lwip/inet.h"
|
||||||
#include "lwip/netdb.h"
|
#include "lwip/netdb.h"
|
||||||
@@ -400,7 +402,10 @@ static int cmd_handle(const char *cmd, char *reply, size_t reply_size)
|
|||||||
|
|
||||||
/* STATUS */
|
/* STATUS */
|
||||||
if (strncmp(cmd, "STATUS", 6) == 0) {
|
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();
|
uint32_t heap = esp_get_free_heap_size();
|
||||||
|
|
||||||
wifi_ap_record_t ap;
|
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;
|
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,
|
snprintf(reply, reply_size,
|
||||||
"OK STATUS uptime=%lld heap=%lu rssi=%d tx_power=%d rate=%d",
|
"OK STATUS uptime=%s heap=%lu rssi=%d tx_power=%d rate=%d hostname=%s",
|
||||||
uptime_s, (unsigned long)heap, rssi, (int)s_tx_power_dbm, s_send_frequency);
|
uptime_str, (unsigned long)heap, rssi, (int)s_tx_power_dbm,
|
||||||
|
s_send_frequency, CONFIG_CSI_HOSTNAME);
|
||||||
return strlen(reply);
|
return strlen(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,6 +541,21 @@ void app_main()
|
|||||||
esp_wifi_set_max_tx_power(s_tx_power_dbm * 4);
|
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);
|
ESP_LOGI(TAG, "TX power set to %d dBm", (int)s_tx_power_dbm);
|
||||||
|
|
||||||
|
/* mDNS: announce as <hostname>.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;
|
s_led_mode = LED_SLOW_BLINK;
|
||||||
|
|
||||||
udp_socket_init();
|
udp_socket_init();
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
idf: ">=4.4.1"
|
idf: ">=4.4.1"
|
||||||
esp_csi_gain_ctrl: ">=0.1.4"
|
esp_csi_gain_ctrl: ">=0.1.4"
|
||||||
|
espressif/mdns: ">=1.0.0"
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ CONFIG_PARTITION_TABLE_MD5=y
|
|||||||
CONFIG_CSI_UDP_TARGET_IP="192.168.129.11"
|
CONFIG_CSI_UDP_TARGET_IP="192.168.129.11"
|
||||||
CONFIG_CSI_UDP_TARGET_PORT=5500
|
CONFIG_CSI_UDP_TARGET_PORT=5500
|
||||||
CONFIG_CSI_CMD_PORT=5501
|
CONFIG_CSI_CMD_PORT=5501
|
||||||
|
CONFIG_CSI_HOSTNAME="your-hostname-here"
|
||||||
# end of CSI UDP Configuration
|
# end of CSI UDP Configuration
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ DEFAULT_PORT = 5501
|
|||||||
TIMEOUT = 2.0
|
TIMEOUT = 2.0
|
||||||
|
|
||||||
USAGE = """\
|
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:
|
Commands:
|
||||||
STATUS Query device state (uptime, heap, RSSI, tx_power, rate)
|
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)
|
POWER <2-20> Set TX power in dBm (saved to NVS)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
esp-cmd 192.168.1.50 STATUS
|
esp-cmd amber-maple.local STATUS
|
||||||
esp-cmd 192.168.1.50 RATE 50
|
esp-cmd 192.168.129.30 RATE 50
|
||||||
esp-cmd 192.168.1.50 POWER 10
|
esp-cmd amber-maple.local IDENTIFY"""
|
||||||
esp-cmd 192.168.1.50 REBOOT
|
|
||||||
esp-cmd 192.168.1.50 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():
|
def main():
|
||||||
@@ -30,8 +40,9 @@ def main():
|
|||||||
print(USAGE)
|
print(USAGE)
|
||||||
sys.exit(0 if sys.argv[1:] and sys.argv[1] in ("-h", "--help") else 2)
|
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()
|
cmd = " ".join(sys.argv[2:]).strip()
|
||||||
|
ip = resolve(host)
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
sock.settimeout(TIMEOUT)
|
sock.settimeout(TIMEOUT)
|
||||||
@@ -41,7 +52,7 @@ def main():
|
|||||||
data, _ = sock.recvfrom(512)
|
data, _ = sock.recvfrom(512)
|
||||||
print(data.decode().strip())
|
print(data.decode().strip())
|
||||||
except socket.timeout:
|
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)
|
sys.exit(1)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"ERR: {e}", file=sys.stderr)
|
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