Skip to content

Commit 4b40071

Browse files
committed
chore: commit remaining tracked updates
1 parent c1be741 commit 4b40071

6 files changed

Lines changed: 172 additions & 38 deletions

File tree

src/net/discovery.c

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,14 @@ static void derive_fallback_mac(uint8_t net_id, uint8_t node_id, uint8_t out[6])
6868
out[5] = 0x01;
6969
}
7070

71+
static uint8_t derive_auto_node_id_from_mac(const uint8_t mac[6])
72+
{
73+
uint8_t id = (uint8_t)(((uint16_t)mac[4] << 1) ^ mac[5]);
74+
id = (uint8_t)(2u + (id % 252u)); /* 0x02..0xFD */
75+
if (id == UMESH_ADDR_COORDINATOR) id = 0x02;
76+
return id;
77+
}
78+
7179
static uint32_t gradient_jitter_delay_ms(void)
7280
{
7381
uint32_t max = s_gradient_jitter_max_ms;
@@ -238,6 +246,16 @@ umesh_result_t discovery_start_election(void)
238246
s_local_mac, 6, UMESH_FLAG_PRIO_HIGH);
239247
}
240248

249+
umesh_result_t discovery_broadcast_election_result(void)
250+
{
251+
static const uint8_t zero_mac[6] = {0};
252+
if (memcmp(s_auto_winner_mac, zero_mac, 6) == 0) {
253+
memcpy(s_auto_winner_mac, s_local_mac, 6);
254+
}
255+
return send_frame(UMESH_ADDR_BROADCAST, UMESH_CMD_ELECTION_RESULT,
256+
s_auto_winner_mac, 6, UMESH_FLAG_PRIO_HIGH);
257+
}
258+
241259
void discovery_leave(void)
242260
{
243261
send_frame(UMESH_ADDR_BROADCAST, UMESH_CMD_LEAVE,
@@ -291,9 +309,7 @@ void discovery_auto_promote_to_coordinator(void)
291309
s_joined = true;
292310
s_next_assign_id = 0x02;
293311
memcpy(s_auto_winner_mac, s_local_mac, 6);
294-
295-
send_frame(UMESH_ADDR_BROADCAST, UMESH_CMD_ELECTION_RESULT,
296-
s_auto_winner_mac, 6, UMESH_FLAG_PRIO_HIGH);
312+
discovery_broadcast_election_result();
297313
}
298314

299315
void discovery_gradient_reset(void)
@@ -355,7 +371,7 @@ void discovery_on_frame(const umesh_frame_t *frame, int8_t rssi)
355371

356372
send_frame(UMESH_ADDR_BROADCAST, UMESH_CMD_ASSIGN,
357373
&new_id, 1, UMESH_FLAG_PRIO_HIGH);
358-
routing_add(new_id, new_id, 1, rssi, 0);
374+
routing_add(new_id, new_id, 1, rssi, s_now_ms);
359375
send_frame(UMESH_ADDR_BROADCAST, UMESH_CMD_NODE_JOINED,
360376
&new_id, 1, UMESH_FLAG_PRIO_NORMAL);
361377
}
@@ -371,7 +387,7 @@ void discovery_on_frame(const umesh_frame_t *frame, int8_t rssi)
371387
s_assigned_id = new_id;
372388
s_joined = true;
373389
routing_add(UMESH_ADDR_COORDINATOR,
374-
UMESH_ADDR_COORDINATOR, 1, rssi, 0);
390+
UMESH_ADDR_COORDINATOR, 1, rssi, s_now_ms);
375391
}
376392
break;
377393

@@ -387,7 +403,7 @@ void discovery_on_frame(const umesh_frame_t *frame, int8_t rssi)
387403
case UMESH_CMD_NODE_JOINED:
388404
if (frame->payload_len >= 1) {
389405
routing_add(frame->payload[0],
390-
UMESH_ADDR_COORDINATOR, 1, rssi, 0);
406+
UMESH_ADDR_COORDINATOR, 1, rssi, s_now_ms);
391407
}
392408
break;
393409

@@ -402,7 +418,7 @@ void discovery_on_frame(const umesh_frame_t *frame, int8_t rssi)
402418
s_auto_seen_coordinator = true;
403419
}
404420
if (s_role != UMESH_ROLE_END_NODE) {
405-
routing_add(frame->src, frame->src, 1, rssi, 0);
421+
routing_add(frame->src, frame->src, 1, rssi, s_now_ms);
406422
}
407423
break;
408424

@@ -461,13 +477,30 @@ void discovery_on_frame(const umesh_frame_t *frame, int8_t rssi)
461477
if (frame->payload_len < 6) break;
462478
s_auto_seen_result = true;
463479
memcpy(s_auto_winner_mac, frame->payload, 6);
464-
if (mac_compare(s_auto_winner_mac, s_local_mac) != 0) {
465-
s_role = UMESH_ROLE_ROUTER;
466-
if (s_node_id == UMESH_ADDR_COORDINATOR) {
467-
s_node_id = UMESH_ADDR_UNASSIGNED;
468-
s_assigned_id = UMESH_ADDR_UNASSIGNED;
480+
{
481+
int cmp = mac_compare(s_auto_winner_mac, s_local_mac);
482+
if (cmp < 0) {
483+
/* Lower-MAC winner overrides local role. */
484+
s_role = UMESH_ROLE_ROUTER;
485+
if (s_node_id == UMESH_ADDR_COORDINATOR) {
486+
s_node_id = UMESH_ADDR_UNASSIGNED;
487+
s_assigned_id = UMESH_ADDR_UNASSIGNED;
488+
}
489+
if (s_node_id == UMESH_ADDR_UNASSIGNED) {
490+
s_node_id = derive_auto_node_id_from_mac(s_local_mac);
491+
s_assigned_id = s_node_id;
492+
}
493+
s_joined = true;
494+
} else if (cmp == 0) {
495+
/* Result confirms we are the elected coordinator. */
496+
s_role = UMESH_ROLE_COORDINATOR;
497+
s_node_id = UMESH_ADDR_COORDINATOR;
498+
s_assigned_id = UMESH_ADDR_COORDINATOR;
499+
s_joined = true;
500+
s_next_assign_id = 0x02;
501+
} else {
502+
/* Higher-MAC winner is ignored; local node may still win. */
469503
}
470-
s_joined = (s_node_id != UMESH_ADDR_UNASSIGNED);
471504
}
472505
break;
473506

src/net/discovery.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ void discovery_set_node_id(uint8_t node_id);
2222
bool discovery_auto_seen_coordinator(void);
2323
void discovery_auto_clear_scan_flag(void);
2424
umesh_result_t discovery_start_election(void);
25+
umesh_result_t discovery_broadcast_election_result(void);
2526
void discovery_auto_clear_election_flags(void);
2627
bool discovery_auto_saw_lower_mac(void);
2728
bool discovery_auto_seen_election_result(void);

tests/hardware/dashboard.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,15 @@ def _device_panel(self, role: str) -> Panel:
283283
def tick(ok: bool) -> str:
284284
return "[green]✓[/green]" if ok else "[dim]·[/dim]"
285285

286-
tbl.add_row(
287-
"JOIN seq",
288-
f"{tick(s.join_sent)} sent "
289-
f"{tick(s.assign_recvd)} assign "
290-
f"{tick(s.joined)} joined",
291-
)
286+
if s.joined and not s.join_sent and not s.assign_recvd:
287+
tbl.add_row("JOIN seq", "[cyan]fixed-id[/cyan]")
288+
else:
289+
tbl.add_row(
290+
"JOIN seq",
291+
f"{tick(s.join_sent)} sent "
292+
f"{tick(s.assign_recvd)} assign "
293+
f"{tick(s.joined)} joined",
294+
)
292295

293296
border = rc
294297

@@ -369,7 +372,7 @@ def list_serial_ports() -> None:
369372
@click.option("--request-ready", is_flag=True, default=False,
370373
help="Send READY command to connected devices after opening ports")
371374
@click.option("--start-tests", is_flag=True, default=False,
372-
help="Send START command to coordinator after opening ports")
375+
help="Send START command to coordinator after opening ports (deprecated, now automatic)")
373376
@click.option("--list-ports", is_flag=True, default=False,
374377
help="List available serial ports and exit")
375378
def main(coordinator: str, router: str, end_node: str,
@@ -455,16 +458,16 @@ def main(coordinator: str, router: str, end_node: str,
455458
except Exception as exc:
456459
click.echo(f" {role:12s} READY FAILED: {exc}", err=True)
457460

458-
if start_tests:
459-
coord = devices.get("coordinator")
460-
if not coord:
461-
click.echo("Cannot send START: coordinator port is not connected.", err=True)
462-
else:
463-
try:
464-
coord.send_command("START")
465-
click.echo("Sent START to coordinator.")
466-
except Exception as exc:
467-
click.echo(f"Failed to send START: {exc}", err=True)
461+
# Auto-start tests by default when coordinator is connected.
462+
coord = devices.get("coordinator")
463+
if not coord:
464+
click.echo("Cannot send START: coordinator port is not connected.", err=True)
465+
else:
466+
try:
467+
coord.send_command("START")
468+
click.echo("Sent START to coordinator (auto).")
469+
except Exception as exc:
470+
click.echo(f"Failed to send START: {exc}", err=True)
468471

469472
click.echo(f"\nMonitoring {len(devices)} device(s)...\n")
470473
time.sleep(0.3)

tests/hardware/firmware/auto_mesh_node/auto_mesh_node.ino

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
#define NET_ID 0x01
1313
#define CHANNEL 6
1414
#define TX_POWER 52
15+
#define AUTO_SCAN_MS 5000
16+
#define AUTO_ELECTION_MS 3000
17+
#define BOOT_JITTER_MAX_MS 800
1518

1619
static const uint8_t MASTER_KEY[16] = {
1720
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6,
@@ -22,24 +25,61 @@ static volatile bool s_send_pong = false;
2225
static volatile uint8_t s_pong_dst = 0;
2326
static char s_cmd_buf[16];
2427
static uint8_t s_cmd_len = 0;
28+
static uint32_t s_last_ping_ms = 0;
29+
static const uint32_t HEARTBEAT_MS = 3000;
30+
31+
static const char* role_str(umesh_role_t role) {
32+
switch (role) {
33+
case UMESH_ROLE_COORDINATOR: return "coordinator";
34+
case UMESH_ROLE_ROUTER: return "router";
35+
case UMESH_ROLE_END_NODE: return "end_node";
36+
case UMESH_ROLE_AUTO: return "auto";
37+
default: return "unknown";
38+
}
39+
}
40+
41+
static const char* state_str(umesh_state_t state) {
42+
switch (state) {
43+
case UMESH_STATE_UNINIT: return "uninit";
44+
case UMESH_STATE_SCANNING: return "scanning";
45+
case UMESH_STATE_ELECTION: return "election";
46+
case UMESH_STATE_JOINING: return "joining";
47+
case UMESH_STATE_CONNECTED: return "connected";
48+
case UMESH_STATE_DISCONNECTED: return "disconnected";
49+
default: return "unknown";
50+
}
51+
}
2552

2653
static void json_ready(void) {
54+
umesh_info_t info = umesh_get_info();
2755
Serial.printf("{\"event\":\"ready\","
28-
"\"data\":{\"mode\":\"auto\",\"state\":\"connected\","
56+
"\"data\":{\"mode\":\"auto\",\"role\":\"%s\",\"state\":\"%s\","
2957
"\"node_id\":%u,\"channel\":%u,\"net_id\":%u}}\n",
30-
umesh_get_info().node_id,
31-
umesh_get_info().channel,
32-
umesh_get_info().net_id);
58+
role_str(info.role), state_str(info.state),
59+
info.node_id, info.channel, info.net_id);
3360
}
3461

3562
static void json_status(void) {
3663
umesh_info_t info = umesh_get_info();
3764
Serial.printf("{\"event\":\"status\","
38-
"\"data\":{\"mode\":\"auto\",\"state\":\"connected\","
65+
"\"data\":{\"mode\":\"auto\",\"role\":\"%s\",\"state\":\"%s\","
3966
"\"node_id\":%u,\"channel\":%u,\"net_id\":%u}}\n",
67+
role_str(info.role), state_str(info.state),
4068
info.node_id, info.channel, info.net_id);
4169
}
4270

71+
static void json_tx(uint8_t dst, uint8_t cmd, uint8_t size) {
72+
Serial.printf("{\"event\":\"tx\","
73+
"\"data\":{\"dst\":%u,\"cmd\":\"0x%02X\",\"size\":%u}}\n",
74+
dst, cmd, size);
75+
}
76+
77+
static void json_rx(uint8_t src, uint8_t cmd, int8_t rssi) {
78+
Serial.printf("{\"event\":\"rx\","
79+
"\"data\":{\"src\":%u,\"cmd\":\"0x%02X\",\"rssi\":%d}}\n",
80+
src, cmd, rssi);
81+
}
82+
4383
static void json_elected(umesh_role_t role) {
4484
const char *name = "router";
4585
if (role == UMESH_ROLE_COORDINATOR) name = "coordinator";
@@ -59,6 +99,7 @@ static void on_role_elected(umesh_role_t role) {
5999

60100
static void on_receive(umesh_pkt_t *pkt) {
61101
if (!pkt) return;
102+
json_rx(pkt->src, pkt->cmd, pkt->rssi);
62103
if (pkt->cmd == UMESH_CMD_PING) {
63104
s_pong_dst = pkt->src;
64105
s_send_pong = true;
@@ -68,6 +109,7 @@ static void on_receive(umesh_pkt_t *pkt) {
68109
void setup(void) {
69110
Serial.begin(115200);
70111
delay(200);
112+
randomSeed((uint32_t)micros());
71113

72114
umesh_cfg_t cfg = {
73115
.net_id = NET_ID,
@@ -77,8 +119,8 @@ void setup(void) {
77119
.security = UMESH_SEC_FULL,
78120
.channel = CHANNEL,
79121
.tx_power = TX_POWER,
80-
.scan_ms = 2000,
81-
.election_ms = 1000,
122+
.scan_ms = AUTO_SCAN_MS,
123+
.election_ms = AUTO_ELECTION_MS,
82124
.on_role_elected = on_role_elected,
83125
};
84126

@@ -87,13 +129,17 @@ void setup(void) {
87129

88130
umesh_on_receive(on_receive);
89131

132+
delay((uint32_t)random(0, BOOT_JITTER_MAX_MS + 1));
133+
90134
r = umesh_start();
91135
if (r != UMESH_OK) { json_error(r); return; }
92136

93137
json_ready();
94138
}
95139

96140
void loop(void) {
141+
umesh_info_t info;
142+
97143
while (Serial.available() > 0) {
98144
char c = (char)Serial.read();
99145
if (c == '\n' || c == '\r') {
@@ -114,12 +160,26 @@ void loop(void) {
114160
}
115161

116162
umesh_tick(millis());
163+
info = umesh_get_info();
117164

118165
if (s_send_pong) {
119166
s_send_pong = false;
167+
json_tx(s_pong_dst, UMESH_CMD_PONG, 0);
120168
umesh_result_t r = umesh_send_cmd(s_pong_dst, UMESH_CMD_PONG, 0);
121169
if (r != UMESH_OK) json_error(r);
122170
}
123171

172+
if (info.state == UMESH_STATE_CONNECTED &&
173+
info.role != UMESH_ROLE_COORDINATOR &&
174+
(millis() - s_last_ping_ms) >= HEARTBEAT_MS) {
175+
s_last_ping_ms = millis();
176+
json_tx(UMESH_ADDR_COORDINATOR, UMESH_CMD_PING, 0);
177+
{
178+
umesh_result_t r = umesh_send_cmd(UMESH_ADDR_COORDINATOR,
179+
UMESH_CMD_PING, 0);
180+
if (r != UMESH_OK) json_error(r);
181+
}
182+
}
183+
124184
delay(5);
125185
}

tests/hardware/runner/device.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@ def on_any(self, callback: Callable) -> None:
5757

5858
def start(self) -> None:
5959
import serial # imported here so import errors are clear
60-
self._serial = serial.Serial(self.port, self.baud, timeout=1)
60+
self._serial = serial.Serial(
61+
self.port,
62+
self.baud,
63+
timeout=1,
64+
write_timeout=1,
65+
)
6166
self._running = True
6267
self._thread = threading.Thread(
6368
target=self._read_loop,

tests/test_election.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,45 @@ static void test_role_auto_backward_compat(void)
131131
"compat: explicit coordinator skips election");
132132
}
133133

134+
static void test_split_brain_converges_to_lower_result(void)
135+
{
136+
uint8_t high_mac[6] = {0x40, 0, 0, 0, 0, 1};
137+
umesh_frame_t frame;
138+
139+
init_posix_stack(UMESH_ADDR_UNASSIGNED, UMESH_ROLE_AUTO);
140+
net_config_auto(10, 20, high_mac);
141+
net_join();
142+
143+
/* Become coordinator first. */
144+
net_tick(0);
145+
net_tick(11);
146+
net_tick(35);
147+
TEST_ASSERT(net_get_role() == UMESH_ROLE_COORDINATOR,
148+
"split-brain: local node initially coordinator");
149+
150+
/* Receive election result from lower-MAC winner. */
151+
memset(&frame, 0, sizeof(frame));
152+
frame.net_id = 0x01;
153+
frame.src = UMESH_ADDR_COORDINATOR;
154+
frame.dst = UMESH_ADDR_BROADCAST;
155+
frame.cmd = UMESH_CMD_ELECTION_RESULT;
156+
frame.payload_len = 6;
157+
frame.payload[0] = 0x10; /* lower winner MAC */
158+
net_on_frame(&frame, -60);
159+
net_tick(36);
160+
161+
TEST_ASSERT(net_get_role() == UMESH_ROLE_ROUTER,
162+
"split-brain: lower winner demotes local coordinator");
163+
}
164+
134165
int main(void)
135166
{
136167
printf("=== test_election ===\n");
137168
test_election_lowest_mac_wins();
138169
test_election_single_node();
139170
test_election_coordinator_failover();
140171
test_role_auto_backward_compat();
172+
test_split_brain_converges_to_lower_result();
141173
printf("Result: %d passed, %d failed\n", s_pass, s_fail);
142174
return (s_fail == 0) ? 0 : 1;
143175
}

0 commit comments

Comments
 (0)