Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions FLASHING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Flashing Notes

## RAK4631 + RAK13800/W5100S Ethernet Companion

This board-support layer is a follow-up to the Ethernet foundation merged in #1983. It keeps the framed MeshCore Companion TCP API and does not switch to raw line mode, MQTT, or the repeater CLI.

Use the canonical Ethernet companion targets when flashing Crow or validating the RAK13800 wiring:

- `RAK_RAK13800_companion_radio_eth`
- `RAK_RAK13800_companion_radio_eth_static_diag`

Compatibility aliases are still present for older workflows:

- `RAK_4631_companion_radio_ethernet`
- `RAK_4631_companion_radio_eth_clean`
- `RAK_4631_companion_radio_eth_static_diag`

## Build Targets

```bash
pio run -e RAK_RAK13800_companion_radio_eth
pio run -e RAK_RAK13800_companion_radio_eth_static_diag
```

## Connection Details

- Ethernet uses the framed MeshCore Companion TCP protocol.
- Do not use the repeater CLI target for companion validation.
- Do not use raw line mode.
- Crow-compatible target port: `4403`
- Upstream #1983 default port: `5000`

## Static Diagnostic Target

`RAK_RAK13800_companion_radio_eth_static_diag` boots with:

- IP `10.245.94.47`
- Gateway `10.245.94.33`
- DNS `10.245.94.33`
- Subnet `255.255.255.224`

It is intended for repeatable Ethernet bring-up and Companion protocol validation.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ For developers:
- Install [PlatformIO](https://docs.platformio.org) in [Visual Studio Code](https://code.visualstudio.com).
- Clone and open the MeshCore repository in Visual Studio Code.
- See the example applications you can modify and run:
- [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or Wi-Fi.
- [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB, Wi-Fi, or framed Ethernet on RAK4631 + RAK13800/W5100S. This is MeshCore Companion support, not Meshtastic.
- [KISS Modem](./examples/kiss_modem) - Serial KISS protocol bridge for host applications. ([protocol docs](./docs/kiss_modem_protocol.md))
- [Simple Repeater](./examples/simple_repeater) - Extends network coverage by relaying messages.
- [Simple Room Server](./examples/simple_room_server) - A simple BBS server for shared Posts.
Expand Down
48 changes: 48 additions & 0 deletions VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Validation Notes

## Build Checks

Run:

```bash
pio run -e RAK_RAK13800_companion_radio_eth
pio run -e RAK_RAK13800_companion_radio_eth_static_diag
pio run -e RAK_4631_companion_radio_ethernet || true
pio run -e RAK_4631_repeater_ethernet || true
pio run -e RAK_4631_room_server_ethernet || true
```

## Static Checks

```bash
grep -R "server.available" -n src/helpers/nrf52 examples variants || true
grep -R "server.accept" -n src/helpers/nrf52 examples variants || true
grep -R "ETHERNET_RAW_LINE" -n variants examples src || true
```

Expected:

- Companion Ethernet accept paths use `server.accept()`
- No Companion Ethernet target defines `ETHERNET_RAW_LINE`
- `server.available()` is not used for TCP client acceptance
- The canonical companion target uses TCP port `4403`

## Smoke Tests

```bash
python3 scripts/meshcore_companion_tcp_smoke_test.py DEVICE_IP 4403
python3 scripts/meshcore_companion_multi_client_test.py DEVICE_IP 4403
```

## Static Diagnostic Hardware Test

Flash `RAK_RAK13800_companion_radio_eth_static_diag` and verify:

- Serial output shows Ethernet initialized
- IP `10.245.94.47`
- Subnet `255.255.255.224`
- Gateway `10.245.94.33`
- TCP server listening on `4403`
- Ping, TCP connect, and neighbor resolution succeed
- Companion smoke tests pass
- Both TCP clients remain connected during the multi-client test
14 changes: 12 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,7 @@ For companion radios, you can set these radios' transmit power in the smartphone
### 7.8. Q: How do I use Ethernet with a RAK4631?
**A:**
MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.rakwireless.com/product-categories/wisblock/rak13800/datasheet/) WisBlock Ethernet module (based on the W5100S chip).
This companion Ethernet support builds on the Ethernet foundation merged in pull request #1983.

**Hardware required:**
- RAK4631 WisBlock Core
Expand All @@ -838,12 +839,21 @@ MeshCore supports Ethernet on RAK4631 boards using the [RAK13800](https://docs.r
Flash one of the Ethernet-enabled firmware variants:
- `RAK_4631_repeater_ethernet` - Repeater with Ethernet CLI access
- `RAK_4631_room_server_ethernet` - Room server with Ethernet CLI access
- `RAK_4631_companion_radio_ethernet` - Companion radio over Ethernet (replaces BLE)
- `RAK_RAK13800_companion_radio_eth` - Companion radio over Ethernet using the framed MeshCore Companion TCP API
- `RAK_RAK13800_companion_radio_eth_static_diag` - Companion radio with a repeatable static validation IP
- `RAK_4631_companion_radio_ethernet` - Compatibility alias for the canonical RAK13800 target

**Important**
- This is not Meshtastic.
- Companion Ethernet is not the repeater CLI target.
- Companion Ethernet is not MQTT.
- Companion Ethernet does not use raw line mode.

**Connecting:**
- The device obtains an IP address via DHCP automatically on boot.
- For repeaters and room servers, connect to the device on TCP port 23 using any TCP client (e.g. `nc <ip> 23` or PuTTY in raw mode). This gives you the same CLI available over serial/USB.
- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect on TCP port 5000 (same as the WiFi companion radio).
- For companion radio firmware, the Ethernet interface replaces BLE as the transport to companion apps. Connect to `DEVICE_IP:4403` for the MeshCore Crow-compatible target. Upstream #1983 defaults to TCP port 5000, but this repo's Crow validation target overrides it to 4403.
- The framed Companion TCP transport uses `'<'+uint16_le_length+payload` from client to radio and `'>'+uint16_le_length+payload` from radio to client.
- Use the `eth.status` CLI command to check connection status and see the assigned IP address.

---
4 changes: 4 additions & 0 deletions examples/companion_radio/MyMesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,10 @@ void MyMesh::begin(bool has_display) {
// load persisted prefs
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);

#ifdef FORCE_CLIENT_REPEAT
_prefs.client_repeat = FORCE_CLIENT_REPEAT;
#endif

// sanitise bad pref values
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
Expand Down
91 changes: 91 additions & 0 deletions scripts/meshcore_companion_multi_client_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env python3

import argparse
import socket
import struct
import sys


def recv_exact(sock, size):
buf = bytearray()
while len(buf) < size:
chunk = sock.recv(size - len(buf))
if not chunk:
raise RuntimeError("connection closed while waiting for data")
buf.extend(chunk)
return bytes(buf)


def send_frame(sock, payload):
sock.sendall(b"<" + struct.pack("<H", len(payload)) + payload)


def recv_frame(sock, timeout):
sock.settimeout(timeout)
header = recv_exact(sock, 3)
if header[0:1] != b">":
raise RuntimeError(f"unexpected frame header {header!r}")
payload_len = struct.unpack("<H", header[1:3])[0]
payload = recv_exact(sock, payload_len)
return payload


def assert_socket_open(sock, label):
sock.settimeout(0.1)
try:
peek = sock.recv(1, socket.MSG_PEEK)
except socket.timeout:
return
if peek == b"":
raise RuntimeError(f"{label} was closed")


def main():
parser = argparse.ArgumentParser()
parser.add_argument("host")
parser.add_argument("port", type=int)
parser.add_argument("--timeout", type=float, default=5.0)
args = parser.parse_args()

client_a = socket.create_connection((args.host, args.port), timeout=args.timeout)
client_b = socket.create_connection((args.host, args.port), timeout=args.timeout)

try:
send_frame(client_a, bytes([0x01] + [0x00] * 7))
recv_frame(client_a, args.timeout)
send_frame(client_b, bytes([0x01] + [0x00] * 7))
recv_frame(client_b, args.timeout)

send_frame(client_a, bytes([0x16, 0x03]))
reply_a = recv_frame(client_a, args.timeout)
if not reply_a or reply_a[0] != 0x0D:
raise RuntimeError(f"client A expected DEVICE_QUERY response 0x0d, got {reply_a[:1]!r}")

assert_socket_open(client_a, "client A before client B command")

send_frame(client_b, bytes([0x16, 0x03]))
reply_b = recv_frame(client_b, args.timeout)
if not reply_b or reply_b[0] != 0x0D:
raise RuntimeError(f"client B expected DEVICE_QUERY response 0x0d, got {reply_b[:1]!r}")

assert_socket_open(client_a, "client A after client B command")
assert_socket_open(client_b, "client B after client B command")

send_frame(client_a, bytes([0x16, 0x03]))
reply_a_again = recv_frame(client_a, args.timeout)
if not reply_a_again or reply_a_again[0] != 0x0D:
raise RuntimeError(f"client A second DEVICE_QUERY failed, got {reply_a_again[:1]!r}")

print("PASS")
return 0
finally:
client_a.close()
client_b.close()


if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(f"FAIL: {exc}", file=sys.stderr)
raise SystemExit(1)
76 changes: 76 additions & 0 deletions scripts/meshcore_companion_tcp_smoke_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env python3

import argparse
import socket
import struct
import sys


def recv_exact(sock, size):
buf = bytearray()
while len(buf) < size:
chunk = sock.recv(size - len(buf))
if not chunk:
raise RuntimeError("connection closed while waiting for data")
buf.extend(chunk)
return bytes(buf)


def send_frame(sock, payload):
sock.sendall(b"<" + struct.pack("<H", len(payload)) + payload)


def recv_frame(sock, timeout):
sock.settimeout(timeout)
header = recv_exact(sock, 3)
if header[0:1] != b">":
raise RuntimeError(f"unexpected frame header {header!r}")
payload_len = struct.unpack("<H", header[1:3])[0]
payload = recv_exact(sock, payload_len)
return payload


def read_device_info(payload):
if len(payload) < 82 or payload[0] != 0x0D:
return None
manufacturer = payload[20:60].split(b"\x00", 1)[0].decode("utf-8", "replace")
version = payload[60:80].split(b"\x00", 1)[0].decode("utf-8", "replace")
return manufacturer, version


def main():
parser = argparse.ArgumentParser()
parser.add_argument("host")
parser.add_argument("port", type=int)
parser.add_argument("--timeout", type=float, default=5.0)
args = parser.parse_args()

with socket.create_connection((args.host, args.port), timeout=args.timeout) as sock:
send_frame(sock, bytes([0x01] + [0x00] * 7))
start_reply = recv_frame(sock, args.timeout)
if not start_reply or start_reply[0] != 0x05:
raise RuntimeError(f"expected APP_START response 0x05, got {start_reply[:1]!r}")

send_frame(sock, bytes([0x16, 0x03]))
device_reply = recv_frame(sock, args.timeout)
if not device_reply or device_reply[0] != 0x0D:
raise RuntimeError(f"expected DEVICE_QUERY response 0x0d, got {device_reply[:1]!r}")

decoded = read_device_info(device_reply)
if decoded:
manufacturer, version = decoded
print(f"manufacturer={manufacturer}")
print(f"firmware={version}")
else:
print("DEVICE_QUERY response decoded partially")

print("PASS")
return 0


if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as exc:
print(f"FAIL: {exc}", file=sys.stderr)
raise SystemExit(1)
Loading