Compare commits
3 Commits
122e46e862
...
b2c5496057
| Author | SHA1 | Date | |
|---|---|---|---|
| b2c5496057 | |||
| b36419477c | |||
| c4468c4af0 |
@@ -73,7 +73,7 @@ Repo: https://gitea.apointless.space/bsncubed/aes67-ESP32-P4
|
|||||||
- [ ] 8. Mono sum, gain, polish.
|
- [ ] 8. Mono sum, gain, polish.
|
||||||
|
|
||||||
## Phase 2 (parked)
|
## Phase 2 (parked)
|
||||||
- [ ] VLAN split: AES67 untagged + internet tagged (inet.vlan_id/pcp), per aes67-core-base.md "Network". Needs a tagged VLAN with DHCP on the switch port. Config group `inet` and the UI fields exist already; nothing is applied yet.
|
- [ ] VLAN split: AES67 untagged + internet tagged (inet.vlan_id/pcp), per aes67-core-base.md "Network". Needs a tagged VLAN with DHCP on the switch port. Config group `inet` and the UI fields exist already; nothing is applied yet. The "VLAN split" checkbox is hidden in web/index.html (`<label hidden>`); unhide it when this is done.
|
||||||
- [ ] NTP (SNTP): servers as hostnames or IPs (e.g. `pool.ntp.org`, several allowed; names resolved via DNS, re-resolved on failure). Uses: seed the PTP clock with real time before becoming GM (today it starts at 1970), syslog timestamps. Needs a `time` config group + UI fields (change doc and page together).
|
- [ ] NTP (SNTP): servers as hostnames or IPs (e.g. `pool.ntp.org`, several allowed; names resolved via DNS, re-resolved on failure). Uses: seed the PTP clock with real time before becoming GM (today it starts at 1970), syslog timestamps. Needs a `time` config group + UI fields (change doc and page together).
|
||||||
|
|
||||||
## Phase 3 (parked)
|
## Phase 3 (parked)
|
||||||
|
|||||||
@@ -11,10 +11,13 @@
|
|||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_netif.h"
|
#include "esp_netif.h"
|
||||||
#include "lldp.h"
|
#include "lldp.h"
|
||||||
|
#include "lwip/inet.h"
|
||||||
#include "mdns.h"
|
#include "mdns.h"
|
||||||
|
|
||||||
static const char *TAG = "net";
|
static const char *TAG = "net";
|
||||||
|
|
||||||
|
static const char *const ADDR_KEYS[] = { "dhcp", "ip", "mask", "gw", "dns" };
|
||||||
|
|
||||||
// AES67 / untagged interface.
|
// AES67 / untagged interface.
|
||||||
static const char NET_DEFAULTS[] =
|
static const char NET_DEFAULTS[] =
|
||||||
"{\"hostname\":\"aes67\",\"vlan\":false,\"web_on\":\"both\",\"dhcp\":true,"
|
"{\"hostname\":\"aes67\",\"vlan\":false,\"web_on\":\"both\",\"dhcp\":true,"
|
||||||
@@ -25,6 +28,48 @@ static const char INET_DEFAULTS[] =
|
|||||||
|
|
||||||
static esp_netif_t *s_netif;
|
static esp_netif_t *s_netif;
|
||||||
static char s_link[24] = "down";
|
static char s_link[24] = "down";
|
||||||
|
static cJSON *s_addr; // addressing in use since boot (ADDR_KEYS of "net")
|
||||||
|
|
||||||
|
// The keys that define the IP setup. With DHCP the static fields don't matter (the UI fills them
|
||||||
|
// with the lease), so they don't count as a change.
|
||||||
|
static cJSON *addressing(const cJSON *g)
|
||||||
|
{
|
||||||
|
cJSON *a = cJSON_CreateObject();
|
||||||
|
int n = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(g, "dhcp")) ? 1 : sizeof(ADDR_KEYS) / sizeof(ADDR_KEYS[0]);
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
cJSON_AddItemToObject(a, ADDR_KEYS[i], cJSON_Duplicate(cJSON_GetObjectItemCaseSensitive(g, ADDR_KEYS[i]), true));
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t addr_of(const cJSON *g, const char *key)
|
||||||
|
{
|
||||||
|
const cJSON *v = cJSON_GetObjectItemCaseSensitive(g, key);
|
||||||
|
return cJSON_IsString(v) && v->valuestring[0] ? inet_addr(v->valuestring) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static addressing: stop the DHCP client and set ours. On link up, like IDF's static IP example.
|
||||||
|
static void set_static(void)
|
||||||
|
{
|
||||||
|
if (cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(s_addr, "dhcp"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_err_t err = esp_netif_dhcpc_stop(s_netif);
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
|
||||||
|
ESP_LOGE(TAG, "DHCP client stop: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
esp_netif_ip_info_t ip = {
|
||||||
|
.ip.addr = addr_of(s_addr, "ip"), .netmask.addr = addr_of(s_addr, "mask"), .gw.addr = addr_of(s_addr, "gw"),
|
||||||
|
};
|
||||||
|
err = esp_netif_set_ip_info(s_netif, &ip);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "static IP: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
esp_netif_dns_info_t dns = { .ip.type = ESP_IPADDR_TYPE_V4, .ip.u_addr.ip4.addr = addr_of(s_addr, "dns") };
|
||||||
|
if (dns.ip.u_addr.ip4.addr) {
|
||||||
|
esp_netif_set_dns_info(s_netif, ESP_NETIF_DNS_MAIN, &dns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data)
|
static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data)
|
||||||
{
|
{
|
||||||
@@ -43,6 +88,7 @@ static void eth_event(void *arg, esp_event_base_t base, int32_t id, void *data)
|
|||||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||||
snprintf(s_link, sizeof(s_link), "up, %s Mbps %s", speed == ETH_SPEED_100M ? "100" : "10",
|
snprintf(s_link, sizeof(s_link), "up, %s Mbps %s", speed == ETH_SPEED_100M ? "100" : "10",
|
||||||
duplex == ETH_DUPLEX_FULL ? "full" : "half");
|
duplex == ETH_DUPLEX_FULL ? "full" : "half");
|
||||||
|
set_static();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ETHERNET_EVENT_DISCONNECTED:
|
case ETHERNET_EVENT_DISCONNECTED:
|
||||||
@@ -66,10 +112,27 @@ static void ip_event(void *arg, esp_event_base_t base, int32_t id, void *data)
|
|||||||
static bool check_addr(const cJSON *g, char *err, size_t n)
|
static bool check_addr(const cJSON *g, char *err, size_t n)
|
||||||
{
|
{
|
||||||
bool dhcp = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(g, "dhcp"));
|
bool dhcp = cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(g, "dhcp"));
|
||||||
return cfg_check_ipv4(g, "ip", dhcp, err, n) &&
|
if (!cfg_check_ipv4(g, "ip", dhcp, err, n) || !cfg_check_ipv4(g, "mask", dhcp, err, n) ||
|
||||||
cfg_check_ipv4(g, "mask", dhcp, err, n) &&
|
!cfg_check_ipv4(g, "gw", true, err, n) || !cfg_check_ipv4(g, "dns", true, err, n)) {
|
||||||
cfg_check_ipv4(g, "gw", true, err, n) &&
|
return false;
|
||||||
cfg_check_ipv4(g, "dns", true, err, n);
|
}
|
||||||
|
if (dhcp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
uint32_t ip = ntohl(addr_of(g, "ip")), mask = ntohl(addr_of(g, "mask")), gw = ntohl(addr_of(g, "gw"));
|
||||||
|
if (mask == 0 || (~mask & (~mask + 1)) != 0 || mask == 0xffffffff) {
|
||||||
|
snprintf(err, n, "mask: not a valid netmask");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ((ip & ~mask) == 0 || (ip & ~mask) == ~mask) {
|
||||||
|
snprintf(err, n, "ip: network or broadcast address of the subnet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (gw && ((gw & mask) != (ip & mask) || gw == ip)) {
|
||||||
|
snprintf(err, n, "gw: must be another address in the IP's subnet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 1123 label: letters, digits, hyphen; not starting or ending with a hyphen.
|
// RFC 1123 label: letters, digits, hyphen; not starting or ending with a hyphen.
|
||||||
@@ -134,14 +197,30 @@ static void net_apply(const cJSON *g)
|
|||||||
ESP_LOGI(TAG, "mDNS: now %s.local", host);
|
ESP_LOGI(TAG, "mDNS: now %s.local", host);
|
||||||
}
|
}
|
||||||
lldp_changed();
|
lldp_changed();
|
||||||
|
// New addressing: PTP/TX/SAP sockets are bound to the address, so it takes a reboot.
|
||||||
|
cJSON *a = addressing(g);
|
||||||
|
if (!cJSON_Compare(a, s_addr, true)) {
|
||||||
|
ESP_LOGW(TAG, "IP setup changed: rebooting to apply it");
|
||||||
|
web_reboot_later(1500);
|
||||||
|
}
|
||||||
|
cJSON_Delete(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void net_status(cJSON *st)
|
static void net_status(cJSON *st)
|
||||||
{
|
{
|
||||||
char ip[16] = "", mac_s[18] = "";
|
char ip[16] = "", mask[16] = "", gw[16] = "", dns[16] = "", mac_s[18] = "";
|
||||||
esp_netif_ip_info_t info;
|
esp_netif_ip_info_t info;
|
||||||
if (esp_netif_get_ip_info(s_netif, &info) == ESP_OK && info.ip.addr) {
|
if (esp_netif_get_ip_info(s_netif, &info) == ESP_OK && info.ip.addr) {
|
||||||
snprintf(ip, sizeof(ip), IPSTR, IP2STR(&info.ip));
|
snprintf(ip, sizeof(ip), IPSTR, IP2STR(&info.ip));
|
||||||
|
snprintf(mask, sizeof(mask), IPSTR, IP2STR(&info.netmask));
|
||||||
|
if (info.gw.addr) {
|
||||||
|
snprintf(gw, sizeof(gw), IPSTR, IP2STR(&info.gw));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
esp_netif_dns_info_t d;
|
||||||
|
if (esp_netif_get_dns_info(s_netif, ESP_NETIF_DNS_MAIN, &d) == ESP_OK && d.ip.type == ESP_IPADDR_TYPE_V4 &&
|
||||||
|
d.ip.u_addr.ip4.addr) {
|
||||||
|
snprintf(dns, sizeof(dns), IPSTR, IP2STR(&d.ip.u_addr.ip4));
|
||||||
}
|
}
|
||||||
uint8_t mac[6];
|
uint8_t mac[6];
|
||||||
if (esp_netif_get_mac(s_netif, mac) == ESP_OK) {
|
if (esp_netif_get_mac(s_netif, mac) == ESP_OK) {
|
||||||
@@ -151,13 +230,16 @@ static void net_status(cJSON *st)
|
|||||||
// Single untagged interface until the VLAN split: AES67 and web share it.
|
// Single untagged interface until the VLAN split: AES67 and web share it.
|
||||||
cJSON_AddStringToObject(st, "ip", ip);
|
cJSON_AddStringToObject(st, "ip", ip);
|
||||||
cJSON_AddStringToObject(st, "aes67_ip", ip);
|
cJSON_AddStringToObject(st, "aes67_ip", ip);
|
||||||
|
cJSON_AddStringToObject(st, "mask", mask); // in use (from DHCP or static)
|
||||||
|
cJSON_AddStringToObject(st, "gw", gw);
|
||||||
|
cJSON_AddStringToObject(st, "dns", dns);
|
||||||
cJSON_AddStringToObject(st, "mac", mac_s);
|
cJSON_AddStringToObject(st, "mac", mac_s);
|
||||||
cJSON_AddStringToObject(st, "link", s_link);
|
cJSON_AddStringToObject(st, "link", s_link);
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t aes67_net_init(esp_eth_handle_t eth)
|
esp_err_t aes67_net_init(esp_eth_handle_t eth)
|
||||||
{
|
{
|
||||||
// Applied so far: hostname (DHCP, mDNS, LLDP). Static IP and VLAN come later.
|
// Applied: hostname (DHCP, mDNS, LLDP) live; DHCP/static addressing at boot. VLAN comes later.
|
||||||
ESP_ERROR_CHECK(cfg_register("net", NET_DEFAULTS, net_validate, net_apply));
|
ESP_ERROR_CHECK(cfg_register("net", NET_DEFAULTS, net_validate, net_apply));
|
||||||
ESP_ERROR_CHECK(cfg_register("inet", INET_DEFAULTS, inet_validate, NULL));
|
ESP_ERROR_CHECK(cfg_register("inet", INET_DEFAULTS, inet_validate, NULL));
|
||||||
|
|
||||||
@@ -177,6 +259,13 @@ esp_err_t aes67_net_init(esp_eth_handle_t eth)
|
|||||||
// Hostname in DHCP requests (option 12), so the router's lease list shows it.
|
// Hostname in DHCP requests (option 12), so the router's lease list shows it.
|
||||||
cJSON *net = cfg_get("net");
|
cJSON *net = cfg_get("net");
|
||||||
ESP_ERROR_CHECK(esp_netif_set_hostname(s_netif, cJSON_GetObjectItemCaseSensitive(net, "hostname")->valuestring));
|
ESP_ERROR_CHECK(esp_netif_set_hostname(s_netif, cJSON_GetObjectItemCaseSensitive(net, "hostname")->valuestring));
|
||||||
|
s_addr = addressing(net);
|
||||||
|
if (!cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(net, "dhcp"))) {
|
||||||
|
ESP_LOGI(TAG, "static IP %s/%s gw %s dns %s", cJSON_GetObjectItemCaseSensitive(net, "ip")->valuestring,
|
||||||
|
cJSON_GetObjectItemCaseSensitive(net, "mask")->valuestring,
|
||||||
|
cJSON_GetObjectItemCaseSensitive(net, "gw")->valuestring,
|
||||||
|
cJSON_GetObjectItemCaseSensitive(net, "dns")->valuestring);
|
||||||
|
}
|
||||||
cJSON_Delete(net);
|
cJSON_Delete(net);
|
||||||
mdns_setup();
|
mdns_setup();
|
||||||
ESP_ERROR_CHECK(lldp_start(eth, s_netif));
|
ESP_ERROR_CHECK(lldp_start(eth, s_netif));
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ Reference devices for UI and defaults: Riedel Bolero (PTP status), Riedel Direct
|
|||||||
## Core API
|
## Core API
|
||||||
- GET/POST /api/config (full JSON; POST validates, stores to NVS, applies live where possible, returns 4xx with message on bad values)
|
- GET/POST /api/config (full JSON; POST validates, stores to NVS, applies live where possible, returns 4xx with message on bad values)
|
||||||
- Write-only keys (`cfg_mark_secret`, e.g. passwords/client secrets): GET returns "" for them; a POST with "" keeps the stored value, null clears it. Stored in NVS in plain text (NVS encryption is not enabled).
|
- Write-only keys (`cfg_mark_secret`, e.g. passwords/client secrets): GET returns "" for them; a POST with "" keeps the stored value, null clears it. Stored in NVS in plain text (NVS encryption is not enabled).
|
||||||
- GET /api/status -> {model?, fw?, power?, ip, aes67_ip, inet_ip, inet_vlan, mac, link, tx_packets, underruns, uptime_s, heap_free, psram_free, temps:[…], ptp:{…}, …project fields}
|
- GET /api/status -> {model?, fw?, power?, ip, aes67_ip, mask, gw, dns, inet_ip, inet_vlan, mac, link, tx_packets, underruns, uptime_s, heap_free, psram_free, temps:[…], ptp:{…}, …project fields}
|
||||||
- model, fw, power are optional; rows only appear when present (power e.g. "PoE" / "USB" if the board can detect it).
|
- model, fw, power are optional; rows only appear when present (power e.g. "PoE" / "USB" if the board can detect it).
|
||||||
- GET /stream.sdp, POST /api/reboot, POST /api/log/test
|
- GET /stream.sdp, POST /api/reboot, POST /api/log/test
|
||||||
- Firmware: GET /api/ota, POST /api/ota, POST /api/ota/confirm, POST /api/ota/rollback (see Firmware update)
|
- Firmware: GET /api/ota, POST /api/ota, POST /api/ota/confirm, POST /api/ota/rollback (see Firmware update)
|
||||||
|
|
||||||
## Temperatures
|
## Temperatures
|
||||||
|
- `status.ip/mask/gw/dns` = the addresses in use (from DHCP or static; "" when none). With DHCP on, the UI shows them in the greyed-out static fields; unticking DHCP keeps them there as the starting point for a static setup.
|
||||||
- `status.temps` = array of {name, c, min_c, max_c, warn_c}; min/max since boot, warn_c optional. Any number of sensors; the UI renders one row per sensor and marks HIGH (red) at or above warn_c.
|
- `status.temps` = array of {name, c, min_c, max_c, warn_c}; min/max since boot, warn_c optional. Any number of sensors; the UI renders one row per sensor and marks HIGH (red) at or above warn_c.
|
||||||
- ESP32-P4: on-die sensor via the `temperature_sensor` driver (driver/temperature_sensor.h). Pick the measurement range to suit (e.g. -10..80 °C), sample every ~2 s in the health task, and report it as "SoC". It measures the die, not ambient: expect it to read well above room temperature.
|
- ESP32-P4: on-die sensor via the `temperature_sensor` driver (driver/temperature_sensor.h). Pick the measurement range to suit (e.g. -10..80 °C), sample every ~2 s in the health task, and report it as "SoC". It measures the die, not ambient: expect it to read well above room temperature.
|
||||||
- Implemented with range 20..100 °C (±2 °C; the warning level must lie inside the range), warn_c 85 °C. On the test board the die reads ~27 °C at idle in a cool room.
|
- Implemented with range 20..100 °C (±2 °C; the warning level must lie inside the range), warn_c 85 °C. On the test board the die reads ~27 °C at idle in a cool room.
|
||||||
|
|||||||
+19
-1
@@ -147,7 +147,8 @@ e.g. curl -X POST http://p4-aes67.local/api/player/next
|
|||||||
|
|
||||||
<fieldset><legend>Network</legend>
|
<fieldset><legend>Network</legend>
|
||||||
<label><span>Hostname</span><input name="net.hostname"></label>
|
<label><span>Hostname</span><input name="net.hostname"></label>
|
||||||
<label><span>VLAN split</span><input name="net.vlan" type="checkbox">
|
<!-- VLAN split: not implemented yet (phase 2); hidden until the firmware applies it. -->
|
||||||
|
<label hidden><span>VLAN split</span><input name="net.vlan" type="checkbox">
|
||||||
<small>off: one untagged interface for everything</small></label>
|
<small>off: one untagged interface for everything</small></label>
|
||||||
<p id="netlbl" hidden><b>AES67 interface (untagged)</b></p>
|
<p id="netlbl" hidden><b>AES67 interface (untagged)</b></p>
|
||||||
<label><span>DHCP</span><input name="net.dhcp" type="checkbox"></label>
|
<label><span>DHCP</span><input name="net.dhcp" type="checkbox"></label>
|
||||||
@@ -341,6 +342,13 @@ function read() {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// With DHCP on, the greyed-out static fields show what DHCP gave us. Unticking DHCP leaves the values
|
||||||
|
// there (a starting point for static addressing) until they are edited.
|
||||||
|
function netLive() {
|
||||||
|
if (!f['net.dhcp'].checked || !st.ip) return;
|
||||||
|
for (const k of ['ip','mask','gw','dns']) f['net.' + k].value = st[k] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
PROJECT.toggle?.(f);
|
PROJECT.toggle?.(f);
|
||||||
const role = f['ptp.role'].value;
|
const role = f['ptp.role'].value;
|
||||||
@@ -354,6 +362,7 @@ function toggle() {
|
|||||||
}
|
}
|
||||||
// VLAN split: default route lives on the internet VLAN, so the AES67 side gets no gateway/DNS.
|
// VLAN split: default route lives on the internet VLAN, so the AES67 side gets no gateway/DNS.
|
||||||
if (vlan) f['net.gw'].disabled = f['net.dns'].disabled = true;
|
if (vlan) f['net.gw'].disabled = f['net.dns'].disabled = true;
|
||||||
|
netLive();
|
||||||
// RTP multicast must be 224.0.2.0 – 239.255.255.255 (same range Riedel Director enforces).
|
// RTP multicast must be 224.0.2.0 – 239.255.255.255 (same range Riedel Director enforces).
|
||||||
const m = f['aes67.mcast'], o = m.value.split('.').map(Number);
|
const m = f['aes67.mcast'], o = m.value.split('.').map(Number);
|
||||||
const ok = o.length === 4 && o.every(x => Number.isInteger(x) && x >= 0 && x <= 255) &&
|
const ok = o.length === 4 && o.every(x => Number.isInteger(x) && x >= 0 && x <= 255) &&
|
||||||
@@ -409,7 +418,15 @@ async function save(e) {
|
|||||||
const c = read();
|
const c = read();
|
||||||
c.aes67.session_ver = (Number(cfg.aes67?.session_ver) || 0) + 1; // SDP version bump per RFC 4566
|
c.aes67.session_ver = (Number(cfg.aes67?.session_ver) || 0) + 1; // SDP version bump per RFC 4566
|
||||||
await api('POST', '/api/config', c);
|
await api('POST', '/api/config', c);
|
||||||
|
// A changed IP setup reboots the device: follow it to the new address.
|
||||||
|
const addr = n => n.dhcp ? 'dhcp' : [n.ip, n.mask, n.gw, n.dns].join('/');
|
||||||
|
const moved = addr(cfg.net || {}) !== addr(c.net);
|
||||||
cfg = c; fill(); msg.textContent = 'Saved.';
|
cfg = c; fill(); msg.textContent = 'Saved.';
|
||||||
|
if (moved) {
|
||||||
|
const to = c.net.dhcp ? location.host : c.net.ip;
|
||||||
|
msg.textContent = `Rebooting with the new IP setup. Opening http://${to}/ in 20 s.`;
|
||||||
|
setTimeout(() => location.href = `http://${to}/`, 20000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PTP display follows the Riedel (Bolero/Artist) status layout, IEEE 1588-2019 terms.
|
// PTP display follows the Riedel (Bolero/Artist) status layout, IEEE 1588-2019 terms.
|
||||||
@@ -497,6 +514,7 @@ async function poll() {
|
|||||||
...temps(st.temps)
|
...temps(st.temps)
|
||||||
});
|
});
|
||||||
showPtp(st.ptp);
|
showPtp(st.ptp);
|
||||||
|
netLive();
|
||||||
sdp();
|
sdp();
|
||||||
} catch { table($('status'), {'Device': 'offline'}); showPtp(null); }
|
} catch { table($('status'), {'Device': 'offline'}); showPtp(null); }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user