diff --git a/README.md b/README.md index 5a0941753..c32baf4d2 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,52 @@ For developers; The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android. +## Build And Flash This Fork (Heltec V3 / Heltec V2) + +If you want to build and flash this fork directly from terminal: + +```bash +git clone https://github.com/just-stuff-tm/MeshCore.git +cd MeshCore +git checkout feat/heltec-v3-runtime-transport-dev +``` + +Install PlatformIO Core (if not already installed): + +```bash +python3 -m pip install -U platformio +``` + +Build firmware: + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v3_companion_radio_tcp_usb_ble +``` + +For Heltec V2: + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v2_companion_radio_tcp_usb_ble +``` + +Flash firmware (USB serial example): + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v3_companion_radio_tcp_usb_ble -t upload --upload-port /dev/ttyUSB0 +``` + +For Heltec V2: + +```bash +~/.platformio/penv/bin/pio run -e Heltec_v2_companion_radio_tcp_usb_ble -t upload --upload-port /dev/ttyUSB0 +``` + +Optional serial companion check: + +```bash +meshcli -s /dev/ttyUSB0 +``` + ## ⚡️ MeshCore Flasher We have prebuilt firmware ready to flash on supported devices. diff --git a/build.sh b/build.sh index 313c4c47a..ff0ff3970 100755 --- a/build.sh +++ b/build.sh @@ -225,6 +225,7 @@ build_companion_firmwares() { # build all companion firmwares build_all_firmwares_by_suffix "_companion_radio_usb" build_all_firmwares_by_suffix "_companion_radio_ble" + build_all_firmwares_by_suffix "_companion_radio_tcp_usb_ble" } diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 1d3430db2..3747d4b2a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -63,6 +63,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +### Send a zero-hop advert +**Usage:** +- `advert.zerohop` + +--- + ### Start an Over-The-Air (OTA) firmware update **Usage:** - `start ota` @@ -355,13 +361,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View this node's public key +**Usage:** `get public.key` + +--- + +#### View this node's configured role +**Usage:** `get role` + +--- + #### View or change this node's power saving flag (Repeater Only) **Usage:** -- `powersaving ` - `powersaving` +- `powersaving on` +- `powersaving off` **Parameters:** -- `state`: `on`|`off` +- `on`: enable power saving +- `off`: disable power saving **Default:** `on` @@ -383,6 +401,46 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- +#### View or change this node's advert path hash size +**Usage:** +- `get path.hash.mode` +- `set path.hash.mode ` + +**Parameters:** +- `value`: Path hash size (0-2) + - `0`: 1 Byte hash size (256 unique ids)[64 max flood] + - `1`: 2 Byte hash size (65,536 unique ids)[32 max flood] + - `2`: 3 Byte hash size (16,777,216 unique ids)[21 max flood] + - `3`: DO NOT USE (Reserved) + +**Default:** `0` + +**Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14 + +**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propogation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are suppored. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. + +--- + +#### View or change this node's loop detection +**Usage:** +- `get loop.detect` +- `set loop.detect ` + +**Parameters:** +- `state`: + - `off`: no loop detection is performed + - `minimal`: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte) + - `moderate`: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) + - `strict`: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) + +**Default:** `off` + +**Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (prob some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14 + +**Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. + +--- + #### View or change the retransmit delay factor for flood traffic **Usage:** - `get txdelay` @@ -804,6 +862,11 @@ region save ### Bridge (When bridge support is compiled in) +#### View the compiled bridge type +**Usage:** `get bridge.type` + +--- + #### View or change the bridge enabled flag **Usage:** - `get bridge.enabled` @@ -841,10 +904,10 @@ region save **Parameters:** - `source`: - - `rx`: bridges received packets - - `tx`: bridges transmitted packets + - `logRx`: bridges received packets + - `logTx`: bridges transmitted packets -**Default:** `tx` +**Default:** `logTx` --- @@ -876,8 +939,39 @@ region save - `set bridge.secret ` **Parameters:** -- `secret`: 16-character encryption secret +- `secret`: ESP-NOW bridge secret, up to 15 characters **Default:** Varies by board --- + +#### View the bootloader version (nRF52 only) +**Usage:** `get bootloader.ver` + +--- + +#### View power management support +**Usage:** `get pwrmgt.support` + +--- + +#### View the current power source +**Usage:** `get pwrmgt.source` + +**Note:** Returns an error on boards without power management support. + +--- + +#### View the boot reset and shutdown reasons +**Usage:** `get pwrmgt.bootreason` + +**Note:** Returns an error on boards without power management support. + +--- + +#### View the boot voltage +**Usage:** `get pwrmgt.bootmv` + +**Note:** Returns an error on boards without power management support. + +--- diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 9d45b59ef..11ba0ab24 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -1,6 +1,6 @@ # Companion Protocol -- **Last Updated**: 2026-01-03 +- **Last Updated**: 2026-03-08 - **Protocol Version**: Companion Firmware v1.12.0+ > NOTE: This document is still in development. Some information may be inaccurate. @@ -100,7 +100,7 @@ When writing commands to the RX characteristic, specify the write type: ### MTU (Maximum Transmission Unit) -The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (66 bytes), you may need to: +The default BLE MTU is 23 bytes (20 bytes payload). For larger commands like `SET_CHANNEL` (50 bytes), you may need to: 1. **Request Larger MTU**: Request MTU of 512 bytes if supported - Android: `gatt.requestMtu(512)` @@ -167,16 +167,16 @@ The first byte indicates the packet type (see [Response Parsing](#response-parsi **Command Format**: ``` Byte 0: 0x01 -Byte 1: 0x03 -Bytes 2-10: "mccli" (ASCII, null-padded to 9 bytes) +Bytes 1-7: Reserved (currently ignored by firmware) +Bytes 8+: Application name (UTF-8, optional) ``` **Example** (hex): ``` -01 03 6d 63 63 6c 69 00 00 00 00 +01 00 00 00 00 00 00 00 6d 63 63 6c 69 ``` -**Response**: `PACKET_OK` (0x00) +**Response**: `PACKET_SELF_INFO` (0x05) --- @@ -216,8 +216,6 @@ Byte 1: Channel Index (0-7) **Response**: `PACKET_CHANNEL_INFO` (0x12) with channel details -**Note**: The device does not return channel secrets for security reasons. Store secrets locally when creating channels. - --- ### 4. Set Channel @@ -229,10 +227,10 @@ Byte 1: Channel Index (0-7) Byte 0: 0x20 Byte 1: Channel Index (0-7) Bytes 2-33: Channel Name (32 bytes, UTF-8, null-padded) -Bytes 34-65: Secret (32 bytes) +Bytes 34-49: Secret (16 bytes) ``` -**Total Length**: 66 bytes +**Total Length**: 50 bytes **Channel Index**: - Index 0: Reserved for public channels (no secret) @@ -243,16 +241,18 @@ Bytes 34-65: Secret (32 bytes) - Maximum 32 bytes - Padded with null bytes (0x00) if shorter -**Secret Field** (32 bytes): -- For **private channels**: 32-byte secret +**Secret Field** (16 bytes): +- For **private channels**: 16-byte secret - For **public channels**: All zeros (0x00) **Example** (create channel "YourChannelName" at index 1 with secret): ``` 20 01 53 4D 53 00 00 ... (name padded to 32 bytes) - [32 bytes of secret] + [16 bytes of secret] ``` +**Note**: The 32-byte secret variant is unsupported and returns `PACKET_ERROR`. + **Response**: `PACKET_OK` (0x00) on success, `PACKET_ERROR` (0x01) on failure --- @@ -304,9 +304,9 @@ Byte 0: 0x0A --- -### 7. Get Battery +### 7. Get Battery and Storage -**Purpose**: Query device battery level. +**Purpose**: Query device battery voltage and storage usage. **Command Format**: ``` @@ -318,7 +318,7 @@ Byte 0: 0x14 14 ``` -**Response**: `PACKET_BATTERY` (0x0C) with battery percentage +**Response**: `PACKET_BATTERY` (0x0C) with battery millivolts and storage information --- @@ -346,7 +346,7 @@ Byte 0: 0x14 1. **Set Channel**: - Fetch all channel slots, and find one with empty name and all-zero secret - Generate or provide a 16-byte secret - - Send `CMD_SET_CHANNEL` with name and secret + - Send `CMD_SET_CHANNEL` with name and a 16-byte secret 2. **Get Channel**: - Send `CMD_GET_CHANNEL` with channel index - Parse `RESP_CODE_CHANNEL_INFO` response @@ -360,7 +360,7 @@ Byte 0: 0x14 ### Receiving Messages -Messages are received via the RX characteristic (notifications). The device sends: +Messages are received via the TX characteristic (notifications). The device sends: 1. **Channel Messages**: - `PACKET_CHANNEL_MSG_RECV` (0x08) - Standard format @@ -544,10 +544,10 @@ Byte 1: Error code (optional) Byte 0: 0x12 Byte 1: Channel Index Bytes 2-33: Channel Name (32 bytes, null-terminated) -Bytes 34-65: Secret (32 bytes, but device typically only returns 20 bytes total) +Bytes 34-49: Secret (16 bytes) ``` -**Note**: The device may not return the full 66-byte packet. Parse what is available. The secret field is typically not returned for security reasons. +**Note**: The device returns the 16-byte channel secret in this response. **PACKET_DEVICE_INFO** (0x0D): ``` @@ -562,6 +562,8 @@ Bytes 4-7: BLE PIN (32-bit little-endian) Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded) Bytes 20-59: Model (40 bytes, UTF-8, null-padded) Bytes 60-79: Version (20 bytes, UTF-8, null-padded) +Byte 80: Client repeat enabled/preferred (firmware v9+) +Byte 81: Path hash mode (firmware v10+) ``` **Parsing Pseudocode**: @@ -587,9 +589,7 @@ def parse_device_info(data): **PACKET_BATTERY** (0x0C): ``` Byte 0: 0x0C -Bytes 1-2: Battery Level (16-bit little-endian, percentage 0-100) - -Optional (if data size > 3): +Bytes 1-2: Battery Voltage (16-bit little-endian, millivolts) Bytes 3-6: Used Storage (32-bit little-endian, KB) Bytes 7-10: Total Storage (32-bit little-endian, KB) ``` @@ -600,14 +600,12 @@ def parse_battery(data): if len(data) < 3: return None - level = int.from_bytes(data[1:3], 'little') - info = {'level': level} + mv = int.from_bytes(data[1:3], 'little') + info = {'battery_mv': mv} - if len(data) > 3: - used_kb = int.from_bytes(data[3:7], 'little') - total_kb = int.from_bytes(data[7:11], 'little') - info['used_kb'] = used_kb - info['total_kb'] = total_kb + if len(data) >= 11: + info['used_kb'] = int.from_bytes(data[3:7], 'little') + info['total_kb'] = int.from_bytes(data[7:11], 'little') return info ``` @@ -629,7 +627,7 @@ Bytes 48-51: Radio Frequency (32-bit little-endian, divided by 1000.0) Bytes 52-55: Radio Bandwidth (32-bit little-endian, divided by 1000.0) Byte 56: Radio Spreading Factor Byte 57: Radio Coding Rate -Bytes 58+: Device Name (UTF-8, variable length, null-terminated) +Bytes 58+: Device Name (UTF-8, variable length, no null terminator required) ``` **Parsing Pseudocode**: @@ -680,9 +678,9 @@ def parse_self_info(data): **PACKET_MSG_SENT** (0x06): ``` Byte 0: 0x06 -Byte 1: Message Type -Bytes 2-5: Expected ACK (4 bytes, hex) -Bytes 6-9: Suggested Timeout (32-bit little-endian, seconds) +Byte 1: Route Flag (0 = direct, 1 = flood) +Bytes 2-5: Tag / Expected ACK (4 bytes, little-endian) +Bytes 6-9: Suggested Timeout (32-bit little-endian, milliseconds) ``` **PACKET_ACK** (0x82): @@ -710,89 +708,32 @@ Bytes 1-6: ACK Code (6 bytes, hex) **Note**: Error codes may vary by firmware version. Always check byte 1 of `PACKET_ERROR` response. -### Partial Packet Handling - -BLE notifications may arrive in chunks, especially for larger packets. Implement buffering: - -**Implementation**: -```python -class PacketBuffer: - def __init__(self): - self.buffer = bytearray() - self.expected_length = None - - def add_data(self, data): - self.buffer.extend(data) - - # Check if we have a complete packet - if len(self.buffer) >= 1: - packet_type = self.buffer[0] - - # Determine expected length based on packet type - expected = self.get_expected_length(packet_type) - - if expected is not None and len(self.buffer) >= expected: - # Complete packet - packet = bytes(self.buffer[:expected]) - self.buffer = self.buffer[expected:] - return packet - elif expected is None: - # Variable length packet - try to parse what we have - # Some packets have minimum length requirements - if self.can_parse_partial(packet_type): - return self.try_parse_partial() - - return None # Incomplete packet - - def get_expected_length(self, packet_type): - # Fixed-length packets - fixed_lengths = { - 0x00: 5, # PACKET_OK (minimum) - 0x01: 2, # PACKET_ERROR (minimum) - 0x0A: 1, # PACKET_NO_MORE_MSGS - 0x14: 3, # PACKET_BATTERY (minimum) - } - return fixed_lengths.get(packet_type) - - def can_parse_partial(self, packet_type): - # Some packets can be parsed partially - return packet_type in [0x12, 0x08, 0x11, 0x07, 0x10, 0x05, 0x0D] - - def try_parse_partial(self): - # Try to parse with available data - # Return packet if successfully parsed, None otherwise - # This is packet-type specific - pass -``` +### Frame Handling -**Usage**: -```python -buffer = PacketBuffer() +BLE implementations enqueue and deliver one protocol frame per BLE write/notification at the firmware layer. -def on_notification_received(data): - packet = buffer.add_data(data) - if packet: - parse_and_handle_packet(packet) -``` +- Apps should treat each characteristic write/notification as exactly one companion protocol frame +- Apps should still validate frame lengths before parsing +- Future transports or firmware revisions may differ, so avoid assuming fixed payload sizes for variable-length responses ### Response Handling 1. **Command-Response Pattern**: - - Send command via TX characteristic - - Wait for response via RX characteristic (notification) + - Send command via RX characteristic + - Wait for response via TX characteristic (notification) - Match response to command using sequence numbers or command type - Handle timeout (typically 5 seconds) - Use command queue to prevent concurrent commands 2. **Asynchronous Messages**: - - Device may send messages at any time via RX characteristic + - Device may send messages at any time via TX characteristic - Handle `PACKET_MESSAGES_WAITING` (0x83) by polling `GET_MESSAGE` command - Parse incoming messages and route to appropriate handlers - - Buffer partial packets until complete + - Validate frame length before decoding 3. **Response Matching**: - Match responses to commands by expected packet type: - - `APP_START` → `PACKET_OK` + - `APP_START` → `PACKET_SELF_INFO` - `DEVICE_QUERY` → `PACKET_DEVICE_INFO` - `GET_CHANNEL` → `PACKET_CHANNEL_INFO` - `SET_CHANNEL` → `PACKET_OK` or `PACKET_ERROR` @@ -825,16 +766,16 @@ device = scan_for_device("MeshCore") gatt = connect_to_device(device) # 3. Discover services and characteristics -service = discover_service(gatt, "0000ff00-0000-1000-8000-00805f9b34fb") -rx_char = discover_characteristic(service, "0000ff01-0000-1000-8000-00805f9b34fb") -tx_char = discover_characteristic(service, "0000ff02-0000-1000-8000-00805f9b34fb") +service = discover_service(gatt, "6E400001-B5A3-F393-E0A9-E50E24DCCA9E") +rx_char = discover_characteristic(service, "6E400002-B5A3-F393-E0A9-E50E24DCCA9E") +tx_char = discover_characteristic(service, "6E400003-B5A3-F393-E0A9-E50E24DCCA9E") -# 4. Enable notifications on RX characteristic -enable_notifications(rx_char, on_notification_received) +# 4. Enable notifications on TX characteristic +enable_notifications(tx_char, on_notification_received) # 5. Send AppStart command -send_command(tx_char, build_app_start()) -wait_for_response(PACKET_OK) +send_command(rx_char, build_app_start()) +wait_for_response(PACKET_SELF_INFO) ``` ### Creating a Private Channel @@ -844,21 +785,16 @@ wait_for_response(PACKET_OK) secret_16_bytes = generate_secret(16) # Use CSPRNG secret_hex = secret_16_bytes.hex() -# 2. Expand secret to 32 bytes using SHA-512 -import hashlib -sha512_hash = hashlib.sha512(secret_16_bytes).digest() -secret_32_bytes = sha512_hash[:32] - -# 3. Build SET_CHANNEL command +# 2. Build SET_CHANNEL command channel_name = "YourChannelName" channel_index = 1 # Use 1-7 for private channels -command = build_set_channel(channel_index, channel_name, secret_32_bytes) +command = build_set_channel(channel_index, channel_name, secret_16_bytes) -# 4. Send command -send_command(tx_char, command) +# 3. Send command +send_command(rx_char, command) response = wait_for_response(PACKET_OK) -# 5. Store secret locally (device won't return it) +# 4. Store secret locally store_channel_secret(channel_index, secret_hex) ``` @@ -872,7 +808,7 @@ timestamp = int(time.time()) command = build_channel_message(channel_index, message, timestamp) # 2. Send command -send_command(tx_char, command) +send_command(rx_char, command) response = wait_for_response(PACKET_MSG_SENT) ``` @@ -887,7 +823,7 @@ def on_notification_received(data): handle_channel_message(message) elif packet_type == PACKET_MESSAGES_WAITING: # Poll for messages - send_command(tx_char, build_get_message()) + send_command(rx_char, build_get_message()) ``` --- diff --git a/docs/faq.md b/docs/faq.md index 220b8971c..530f97013 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -221,11 +221,11 @@ MeshCore allows you to manually broadcast your name, position and public encrypt * Zero hop means your advert is broadcasted out to anyone that can hear it, and that's it. * Flooded means it's broadcasted out and then repeated by all the repeaters that hear it. -MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 3 hours by default. This interval can be configured using the following command: +MeshCore clients only advertise themselves when the user initiates it. A repeater sends a flood advert once every 12 hours by default. This interval can be configured using the following command: -`set advert.interval {minutes}` +`set flood.advert.interval {hours}` -As of Aug 20 2025, a pending PR on github will change the flood advert to 12 hours to minimize airtime utilization caused by repeaters' flood adverts. +The separate `set advert.interval {minutes}` command controls the local zero-hop advert timer. ### 2.5. Q: Is there a hop limit? @@ -260,7 +260,9 @@ Repeater or room server can be administered with one of the options below: ### 3.2. Q: Do I need to set the location for a repeater? **A:** While not required, with location set for a repeater it will show up on the MeshCore map in the future. Set location with the following command: -`set lat set long ` +`set lat ` + +`set lon ` You can get the latitude and longitude from Google Maps by right-clicking the location you are at on the map. diff --git a/docs/heltec_v3_button_actions.md b/docs/heltec_v3_button_actions.md new file mode 100644 index 000000000..55f7ffb5a --- /dev/null +++ b/docs/heltec_v3_button_actions.md @@ -0,0 +1,45 @@ +# Heltec V3 Button Actions + +This page documents current button behavior for `Heltec_v3_companion_radio_tcp_usb_ble` UI flows. +The same mapping also applies to `Heltec_v2_companion_radio_tcp_usb_ble`, which uses the same `examples/companion_radio/ui-new` button/UI logic. + +## Global Click Mapping + +On single-button Heltec V3 builds: + +- `single click` = next page +- `double click` = previous page +- `long press` = enter/select action on current page +- `triple click` = `KEY_SELECT` action on current page + +## Startup Special Case + +- Long press within first ~8 seconds after boot enters CLI Rescue mode. + +## Screen Actions + +### BLE Transport Page + +- Long press: switch transport to WiFi/TCP. +- After transport switch, UI shows a 10-second reboot countdown and auto-reboots. + +### WiFi Transport Page + +- Long press: switch transport back to BLE. +- Triple click: toggle WiFi mode (`AP <-> Client`). +- UI footer alternates/scrolls usage hints: + - `Hold: BLE` + - `Triple: AP/Client` + +### Advert Page + +- Long press: send advert. + +### Power Page + +- Long press: start hibernate/power-off flow. + +## Notes + +- Transport switches are runtime-applied, then device auto-reboots after countdown for clean handoff. +- WiFi page shows `IP:PORT` in both AP and client mode, with AP SSID/password rows alternating. diff --git a/docs/index.md b/docs/index.md index 9460a00c5..3fef7e854 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,6 +7,7 @@ Below are a few quick start guides. - [Frequently Asked Questions](./faq.md) - [CLI Commands](./cli_commands.md) - [Companion Protocol](./companion_protocol.md) +- [Heltec V3 Button Actions](./heltec_v3_button_actions.md) - [Packet Format](./packet_format.md) - [QR Codes](./qr_codes.md) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 6a08614fa..8fbb57e45 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -190,7 +190,7 @@ All values little-endian. | Field | Size | Description | |-------|------|-------------| | MAC | 2 bytes | HMAC-SHA256 truncated to 2 bytes | -| Ciphertext | variable | AES-128-CBC encrypted data | +| Ciphertext | variable | AES-128 block-encrypted data with zero padding | ### Airtime (Airtime response) @@ -268,7 +268,7 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs. |-----------|-----------| | Identity / Signing / Verification | Ed25519 | | Key Exchange | X25519 (ECDH) | -| Encryption | AES-128-CBC + HMAC-SHA256 (MAC truncated to 2 bytes) | +| Encryption | AES-128 block encryption with zero padding + HMAC-SHA256 (MAC truncated to 2 bytes) | | Hashing | SHA-256 | ## Notes @@ -279,4 +279,4 @@ Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs. - SNR values in RxMeta are multiplied by 4 for 0.25 dB precision - TxDone is sent as a SetHardware event after each transmission - Standard KISS clients receive only type 0x00 data frames and can safely ignore all SetHardware (0x06) frames -- See [packet_structure.md](./packet_structure.md) for packet format +- See [packet_format.md](./packet_format.md) for packet format diff --git a/docs/payloads.md b/docs/payloads.md index 3648b6557..15fec7578 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -90,23 +90,17 @@ Returned path messages provide a description of the route a packet took from the ## Request -| Field | Size (bytes) | Description | -|--------------|-----------------|----------------------------| -| timestamp | 4 | send time (unix timestamp) | -| request type | 1 | see below | -| request data | rest of payload | depends on request type | +| Field | Size (bytes) | Description | +|--------------|-----------------|------------------------------------------| +| timestamp | 4 | sender time (unix timestamp) | +| request data | rest of payload | application-defined request payload body | -Request type +For the common chat/server helpers in `BaseChatMesh`, the current request type values are: | Value | Name | Description | |--------|----------------------|---------------------------------------| | `0x01` | get stats | get stats of repeater or room server | -| `0x02` | keepalive | (deprecated) | -| `0x03` | get telemetry data | TODO | -| `0x04` | get min,max,avg data | sensor nodes - get min, max, average for given time span | -| `0x05` | get access list | get node's approved access list | -| `0x06` | get neighbors | get repeater node's neighbors | -| `0x07` | get owner info | get repeater firmware-ver/name/owner info | +| `0x02` | keepalive | keep-alive request used for maintained connections | ### Get stats @@ -133,35 +127,36 @@ Gets information about the node, possibly including the following: ### Get telemetry data -Request data about sensors on the node, including battery level. +Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. ### Get Telemetry -TODO +Not defined in `BaseChatMesh`. ### Get Min/Max/Ave (Sensor nodes) -TODO +Not defined in `BaseChatMesh`. ### Get Access List -TODO +Not defined in `BaseChatMesh`. ### Get Neighors -TODO +Not defined in `BaseChatMesh`. ### Get Owner Info -TODO +Not defined in `BaseChatMesh`. ## Response | Field | Size (bytes) | Description | |---------|-----------------|-------------| -| tag | 4 | TODO | -| content | rest of payload | TODO | +| content | rest of payload | application-defined response body | + +Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above. ## Plain text message diff --git a/docs/wifi_transport_command_examples.md b/docs/wifi_transport_command_examples.md new file mode 100644 index 000000000..7164f8c6e --- /dev/null +++ b/docs/wifi_transport_command_examples.md @@ -0,0 +1,258 @@ +# WiFi/Transport Command Examples + +This file shows the same settings commands in three formats: + +1. Raw companion protocol frames (USB/TCP) +2. Python (build/send framed command) +3. Text CLI commands + +## Command Keys + +These are the custom-var keys used by firmware: + +- `transport:ble` +- `transport:wifi` +- `wifi.mode:ap` +- `wifi.mode:client` +- `wifi.ssid:` +- `wifi.pwd:` +- `wifi.ap.ssid:` +- `wifi.ap.pwd:` +- `tcp.port:` +- `ip` / `wifi.ip` / `wifi.status` / `wifi.rssi` / `wifi.gateway` / `wifi.mask` / `wifi.mac` (runtime keys in CLI Rescue) + +## 1) Raw Companion Protocol Form + +Frame format: + +- App -> radio: `0x3c` + `` + `` +- Payload for set custom var: `0x29` + ASCII string + +Example raw bytes (hex): + +- `transport:wifi` + - Payload: `29 74 72 61 6e 73 70 6f 72 74 3a 77 69 66 69` + - Full frame: `3c 0f 00 29 74 72 61 6e 73 70 6f 72 74 3a 77 69 66 69` + +- `wifi.mode:ap` + - Payload: `29 77 69 66 69 2e 6d 6f 64 65 3a 61 70` + - Full frame: `3c 0d 00 29 77 69 66 69 2e 6d 6f 64 65 3a 61 70` + +- `wifi.mode:client` + - Payload: `29 77 69 66 69 2e 6d 6f 64 65 3a 63 6c 69 65 6e 74` + - Full frame: `3c 11 00 29 77 69 66 69 2e 6d 6f 64 65 3a 63 6c 69 65 6e 74` + +## 2) Python Form + +### Helper function + +```python +import os + +def send_custom_var(port, kv): + fd = os.open(port, os.O_WRONLY) + payload = bytes([0x29]) + kv.encode("ascii") + frame = bytes([0x3c, len(payload) & 0xFF, (len(payload) >> 8) & 0xFF]) + payload + os.write(fd, frame) + os.close(fd) +``` + +### Example usage + +```python +send_custom_var("/dev/ttyUSB0", "transport:wifi") +send_custom_var("/dev/ttyUSB0", "wifi.mode:ap") +send_custom_var("/dev/ttyUSB0", "wifi.ap.ssid:MeshCore-SampleAP") +send_custom_var("/dev/ttyUSB0", "wifi.ap.pwd:SampleAPPwd123") + +send_custom_var("/dev/ttyUSB0", "wifi.mode:client") +send_custom_var("/dev/ttyUSB0", "wifi.ssid:SampleSSID") +send_custom_var("/dev/ttyUSB0", "wifi.pwd:SamplePwd123") + +send_custom_var("/dev/ttyUSB0", "transport:ble") +``` + +### One-liner pattern + +```bash +python3 -c "import os;fd=os.open('/dev/ttyUSB0',os.O_WRONLY);s='wifi.mode:ap';p=bytes([0x29])+s.encode();os.write(fd,bytes([0x3c,len(p)&255,(len(p)>>8)&255])+p);os.close(fd)" +``` + +## 3) Text CLI Form + +There are two text-CLI contexts: + +1. `meshcli` companion commands +2. CLI Rescue (device text console) + +### 3.1 meshcli (companion mode) + +Use serial companion mode (no `-r`): + +```bash +meshcli -s /dev/ttyUSB0 -b 115200 infos +``` + +Then set values: + +```bash +meshcli -s /dev/ttyUSB0 -b 115200 set transport tcp +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap +meshcli -s /dev/ttyUSB0 -b 115200 set tcp.port 5000 +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 + +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode client +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 + +meshcli -s /dev/ttyUSB0 -b 115200 set transport ble +``` + +Read back current transport/WiFi values: + +```bash +meshcli -s /dev/ttyUSB0 -b 115200 get transport +meshcli -s /dev/ttyUSB0 -b 115200 get tcp.port +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd +``` + +### 3.2 meshcli interactive mode (recommended for manual testing) + +Start interactive mode: + +```bash +meshcli -s /dev/ttyUSB0 -b 115200 +``` + +Example interactive session: + +```text +KD3CGK|* set wifi.mode client +Var wifi.mode set to client +KD3CGK|* get wifi.mode +client +KD3CGK|* set tcp.port 6001 +Var tcp.port set to 6001 +KD3CGK|* get tcp.port +6001 +KD3CGK|* set transport ble +Var transport set to ble +KD3CGK|* get transport +ble +KD3CGK|* get wifi.ssid +SampleSSID +KD3CGK|* get wifi.pwd +SamplePwd123 +KD3CGK|* get wifi.ap.ssid +MeshCore-SampleAP +KD3CGK|* get wifi.ap.pwd +SampleAPPwd123 +``` + +### 3.2.1 Full sample test sequence + +Run this exact command sequence to validate WiFi/transport set+get behavior with sample values: + +```bash +meshcli -s /dev/ttyUSB0 -b 115200 infos +meshcli -s /dev/ttyUSB0 -b 115200 set transport ble +meshcli -s /dev/ttyUSB0 -b 115200 get transport + +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode ap +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.ssid MeshCore-SampleAP +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ap.pwd SampleAPPwd123 +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd + +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.mode client +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.ssid SampleSSID +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcli -s /dev/ttyUSB0 -b 115200 set wifi.pwd SamplePwd123 +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd + +meshcli -s /dev/ttyUSB0 -b 115200 set transport tcp +meshcli -s /dev/ttyUSB0 -b 115200 get transport +meshcli -s /dev/ttyUSB0 -b 115200 set transport wifi +meshcli -s /dev/ttyUSB0 -b 115200 get transport +meshcli -s /dev/ttyUSB0 -b 115200 set transport ble +meshcli -s /dev/ttyUSB0 -b 115200 get transport + +meshcli -s /dev/ttyUSB0 -b 115200 get tcp.port +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.mode +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ssid +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.pwd +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.ssid +meshcli -s /dev/ttyUSB0 -b 115200 get wifi.ap.pwd +``` + +### 3.2.2 Current `meshcli` custom `get` keys + +In companion interactive mode (`meshcli -s ...`), the custom-var getter supports: + +- `transport` +- `tcp.port` +- `wifi.mode` +- `wifi.ssid` +- `wifi.pwd` +- `wifi.ap.ssid` +- `wifi.ap.pwd` + +### 3.3 CLI Rescue text commands + +Inside CLI Rescue terminal: + +```text +set transport tcp +set wifi.mode ap +set tcp.port 5000 +set wifi.ap.ssid MeshCore-SampleAP +set wifi.ap.pwd SampleAPPwd123 + +set wifi.mode client +set wifi.ssid SampleSSID +set wifi.pwd SamplePwd123 + +set transport ble + +get transport +get tcp.port +get ip +get wifi.ip +get wifi.mode +get wifi.ssid +get wifi.pwd +get wifi.ap.ssid +get wifi.ap.pwd +get ip +get wifi.ip +get wifi.status +get wifi.rssi +get wifi.gateway +get wifi.mask +get wifi.mac +``` + +## TCP Test Example + +If device client IP is known (example `192.168.40.55`): + +```bash +meshcli -t 192.168.40.55 -p 5000 infos +``` + +## Notes + +- `transport:wifi` / `set transport tcp` enables WiFi/TCP transport and disables BLE. +- `transport:ble` / `set transport ble` enables BLE and disables WiFi/TCP. +- After a transport switch, the board UI now shows a 10-second reboot countdown and auto-resets. +- The board UI also shows a temporary WiFi mode transition screen when switching AP/client mode. +- Heltec V3 button mappings for these UI actions: [Heltec V3 Button Actions](./heltec_v3_button_actions.md). +- AP password should be empty (open AP) or at least 8 characters. +- Do not commit real SSIDs/passwords to git. diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index d9ebacb41..22d08f006 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -230,6 +230,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.read((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 + file.read((uint8_t *)&_prefs.transport_mode, sizeof(_prefs.transport_mode)); // 89 + file.read((uint8_t *)_prefs.wifi_ssid, sizeof(_prefs.wifi_ssid)); // 90 + file.read((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd)); // 123 + file.read((uint8_t *)&_prefs.wifi_mode, sizeof(_prefs.wifi_mode)); // 188 + file.read((uint8_t *)_prefs.wifi_ap_ssid, sizeof(_prefs.wifi_ap_ssid)); // 189 + file.read((uint8_t *)_prefs.wifi_ap_pwd, sizeof(_prefs.wifi_ap_pwd)); // 222 file.close(); } @@ -267,6 +273,12 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 + file.write((uint8_t *)&_prefs.transport_mode, sizeof(_prefs.transport_mode)); // 89 + file.write((uint8_t *)_prefs.wifi_ssid, sizeof(_prefs.wifi_ssid)); // 90 + file.write((uint8_t *)_prefs.wifi_pwd, sizeof(_prefs.wifi_pwd)); // 123 + file.write((uint8_t *)&_prefs.wifi_mode, sizeof(_prefs.wifi_mode)); // 188 + file.write((uint8_t *)_prefs.wifi_ap_ssid, sizeof(_prefs.wifi_ap_ssid)); // 189 + file.write((uint8_t *)_prefs.wifi_ap_pwd, sizeof(_prefs.wifi_ap_pwd)); // 222 file.close(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2ca226781..0d1863eea 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2,6 +2,9 @@ #include // needed for PlatformIO #include +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + #include +#endif #define CMD_APP_START 1 #define CMD_SEND_TXT_MSG 2 @@ -119,6 +122,36 @@ #define PUSH_CODE_CONTACT_DELETED 0x8F // used to notify client app of deleted contact when overwriting oldest #define PUSH_CODE_CONTACTS_FULL 0x90 // used to notify client app that contacts storage is full +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) +static bool applyTransportModePref(NodePrefs& prefs) { + auto* transport = CompanionTransportInterface::instance(); + if (!transport) return false; + + auto mode = (prefs.transport_mode == 1) + ? CompanionTransportInterface::WIRELESS_MODE_TCP + : CompanionTransportInterface::WIRELESS_MODE_BLE; + return transport->setWirelessMode(mode); +} + +static bool applyWifiPrefs(NodePrefs& prefs) { + auto* transport = CompanionTransportInterface::instance(); + if (!transport) return false; + + if (!transport->setWifiCredentials(prefs.wifi_ssid, prefs.wifi_pwd)) return false; + if (!transport->setWifiApCredentials(prefs.wifi_ap_ssid, prefs.wifi_ap_pwd)) return false; + auto wifi_mode = (prefs.wifi_mode == 1) + ? CompanionTransportInterface::WIFI_MODE_STA_CLIENT + : CompanionTransportInterface::WIFI_MODE_AP_ONLY; + return transport->setWifiMode(wifi_mode); +} + +static bool applyTcpPortPref(uint16_t tcp_port) { + auto* transport = CompanionTransportInterface::instance(); + if (!transport) return false; + return transport->setTcpPort(tcp_port); +} +#endif + #define ERR_CODE_UNSUPPORTED_CMD 1 #define ERR_CODE_NOT_FOUND 2 #define ERR_CODE_TABLE_FULL 3 @@ -800,6 +833,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) { _iter_started = false; _cli_rescue = false; + _pending_transport_apply = false; + _pending_transport_apply_at = 0; offline_queue_len = 0; app_target_ver = 0; clearPendingReqs(); @@ -820,6 +855,12 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.tx_power_dbm = LORA_TX_POWER; _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default + _prefs.transport_mode = 0; // BLE by default + _prefs.wifi_mode = 0; // WiFi AP mode by default + _prefs.wifi_ssid[0] = 0; + _prefs.wifi_pwd[0] = 0; + _prefs.wifi_ap_ssid[0] = 0; + _prefs.wifi_ap_pwd[0] = 0; //_prefs.rx_delay_base = 10.0f; enable once new algo fixed } @@ -859,6 +900,12 @@ void MyMesh::begin(bool has_display) { _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, -9, MAX_LORA_TX_POWER); _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours + _prefs.transport_mode = constrain(_prefs.transport_mode, 0, 1); + _prefs.wifi_mode = constrain(_prefs.wifi_mode, 0, 1); + _prefs.wifi_ssid[sizeof(_prefs.wifi_ssid) - 1] = 0; + _prefs.wifi_pwd[sizeof(_prefs.wifi_pwd) - 1] = 0; + _prefs.wifi_ap_ssid[sizeof(_prefs.wifi_ap_ssid) - 1] = 0; + _prefs.wifi_ap_pwd[sizeof(_prefs.wifi_ap_pwd) - 1] = 0; #ifdef BLE_PIN_CODE // 123456 by default if (_prefs.ble_pin == 0) { @@ -1650,15 +1697,77 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_GET_CUSTOM_VARS) { out_frame[0] = RESP_CODE_CUSTOM_VARS; char *dp = (char *)&out_frame[1]; - for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < 140; i++) { - if (i > 0) { + const int max_custom_vars_len = 140; + auto escapeCustomVarValue = [](const char* value, char* out, size_t out_len) { + if (!value || !out || out_len == 0) return; + size_t oi = 0; + for (size_t i = 0; value[i] != 0 && oi + 1 < out_len; i++) { + char c = value[i]; + if (c == ',' || c == ':' || c == '%') { + if (oi + 3 >= out_len) break; + out[oi++] = '%'; + if (c == ',') { + out[oi++] = '2'; + out[oi++] = 'C'; + } else if (c == ':') { + out[oi++] = '3'; + out[oi++] = 'A'; + } else { + out[oi++] = '2'; + out[oi++] = '5'; + } + } else { + out[oi++] = c; + } + } + out[oi] = 0; + }; + auto appendKV = [&](const char* key, const char* value) -> bool { + if (!key || !value) return false; + int used = dp - (char *)&out_frame[1]; + int required = strlen(key) + 1 + strlen(value) + (used > 0 ? 1 : 0); + if (used + required >= max_custom_vars_len) return false; + if (used > 0) { *dp++ = ','; } - strcpy(dp, sensors.getSettingName(i)); + strcpy(dp, key); dp = strchr(dp, 0); *dp++ = ':'; - strcpy(dp, sensors.getSettingValue(i)); + strcpy(dp, value); dp = strchr(dp, 0); + return true; + }; + auto appendKVEscaped = [&](const char* key, const char* value) -> bool { + char escaped[96]; + escapeCustomVarValue(value, escaped, sizeof(escaped)); + return appendKV(key, escaped); + }; + +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + char tcp_port_value[8]; + char ip_value[20] = "0.0.0.0"; + uint16_t tcp_port = 5000; + if (auto* transport = CompanionTransportInterface::instance()) { + tcp_port = transport->getTcpPort(); + } + snprintf(tcp_port_value, sizeof(tcp_port_value), "%u", tcp_port); + if (WiFi.status() == WL_CONNECTED) { + snprintf(ip_value, sizeof(ip_value), "%s", WiFi.localIP().toString().c_str()); + } else if (_prefs.wifi_mode == 0) { + snprintf(ip_value, sizeof(ip_value), "%s", WiFi.softAPIP().toString().c_str()); + } + + appendKVEscaped("transport", _prefs.transport_mode == 1 ? "tcp" : "ble"); + appendKVEscaped("tcp.port", tcp_port_value); + appendKVEscaped("wifi.mode", _prefs.wifi_mode == 1 ? "client" : "ap"); + appendKVEscaped("wifi.ap.ssid", _prefs.wifi_ap_ssid); + appendKVEscaped("wifi.pwd", _prefs.wifi_pwd); + appendKVEscaped("wifi.ap.pwd", _prefs.wifi_ap_pwd); + appendKVEscaped("wifi.ssid", _prefs.wifi_ssid); +#endif + + for (int i = 0; i < sensors.getNumSettings() && dp - (char *)&out_frame[1] < max_custom_vars_len; i++) { + if (!appendKVEscaped(sensors.getSettingName(i), sensors.getSettingValue(i))) break; } _serial->writeFrame(out_frame, dp - (char *)out_frame); } else if (cmd_frame[0] == CMD_SET_CUSTOM_VAR && len >= 4) { @@ -1667,7 +1776,69 @@ void MyMesh::handleCmdFrame(size_t len) { char *np = strchr(sp, ':'); // look for separator char if (np) { *np++ = 0; // modify 'cmd_frame', replace ':' with null - bool success = sensors.setSettingValue(sp, np); + bool success = false; + +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (strcmp(sp, "transport") == 0) { + if (strcmp(np, "ble") == 0) { + _prefs.transport_mode = 0; + _pending_transport_apply = true; + _pending_transport_apply_at = millis() + 500; + success = true; + } else if (strcmp(np, "tcp") == 0 || strcmp(np, "wifi") == 0) { + _prefs.transport_mode = 1; + _pending_transport_apply = true; + _pending_transport_apply_at = millis() + 500; + success = true; + } + if (success) { + savePrefs(); + } + } else if (strcmp(sp, "wifi.ssid") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_ssid)) { + StrHelper::strncpy(_prefs.wifi_ssid, np, sizeof(_prefs.wifi_ssid)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.pwd") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_pwd)) { + StrHelper::strncpy(_prefs.wifi_pwd, np, sizeof(_prefs.wifi_pwd)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.mode") == 0) { + if (strcmp(np, "ap") == 0) { + _prefs.wifi_mode = 0; + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } else if (strcmp(np, "client") == 0 || strcmp(np, "sta") == 0) { + _prefs.wifi_mode = 1; + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.ap.ssid") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_ap_ssid)) { + StrHelper::strncpy(_prefs.wifi_ap_ssid, np, sizeof(_prefs.wifi_ap_ssid)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "wifi.ap.pwd") == 0) { + if (strlen(np) < sizeof(_prefs.wifi_ap_pwd)) { + StrHelper::strncpy(_prefs.wifi_ap_pwd, np, sizeof(_prefs.wifi_ap_pwd)); + success = applyWifiPrefs(_prefs); + if (success) savePrefs(); + } + } else if (strcmp(sp, "tcp.port") == 0) { + long port = atol(np); + if (port >= 1 && port <= 65535) { + success = applyTcpPortPref((uint16_t)port); + } + } +#endif + + if (!success) { + success = sensors.setSettingValue(sp, np); + } if (success) { #if ENV_INCLUDE_GPS == 1 // Update node preferences for GPS settings @@ -1844,9 +2015,182 @@ void MyMesh::checkCLIRescueCmd() { _prefs.ble_pin = atoi(&config[4]); savePrefs(); Serial.printf(" > pin is now %06d\n", _prefs.ble_pin); +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + } else if (memcmp(config, "transport ", 10) == 0) { + const char* mode = &config[10]; + if (strcmp(mode, "ble") == 0) { + _prefs.transport_mode = 0; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > wireless transport is now BLE"); + Serial.println(" > reboot required; board will auto-reset in 10 seconds"); + } else { + Serial.println(" Error: failed to switch transport"); + } + } else if (strcmp(mode, "tcp") == 0 || strcmp(mode, "wifi") == 0) { + _prefs.transport_mode = 1; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > wireless transport is now TCP/WiFi"); + Serial.println(" > reboot required; board will auto-reset in 10 seconds"); + } else { + Serial.println(" Error: failed to switch transport"); + } + } else { + Serial.println(" Error: usage set transport ble|tcp"); + } + } else if (memcmp(config, "wifi.ssid ", 10) == 0) { + const char* ssid = &config[10]; + if (strlen(ssid) < sizeof(_prefs.wifi_ssid)) { + StrHelper::strncpy(_prefs.wifi_ssid, ssid, sizeof(_prefs.wifi_ssid)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi ssid updated"); + } else { + Serial.println(" Error: failed to set wifi config"); + } + } else { + Serial.println(" Error: ssid too long"); + } + } else if (memcmp(config, "wifi.pwd ", 9) == 0) { + const char* pwd = &config[9]; + if (strlen(pwd) < sizeof(_prefs.wifi_pwd)) { + StrHelper::strncpy(_prefs.wifi_pwd, pwd, sizeof(_prefs.wifi_pwd)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi password updated"); + } else { + Serial.println(" Error: failed to set wifi config"); + } + } else { + Serial.println(" Error: password too long"); + } + } else if (memcmp(config, "wifi.mode ", 10) == 0) { + const char* mode = &config[10]; + if (strcmp(mode, "ap") == 0) { + _prefs.wifi_mode = 0; + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi mode is now AP"); + } else { + Serial.println(" Error: failed to set wifi mode"); + } + } else if (strcmp(mode, "client") == 0 || strcmp(mode, "sta") == 0) { + _prefs.wifi_mode = 1; + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi mode is now CLIENT"); + } else { + Serial.println(" Error: failed to set wifi mode"); + } + } else { + Serial.println(" Error: usage set wifi.mode ap|client"); + } + } else if (memcmp(config, "wifi.ap.ssid ", 13) == 0) { + const char* ssid = &config[13]; + if (strlen(ssid) < sizeof(_prefs.wifi_ap_ssid)) { + StrHelper::strncpy(_prefs.wifi_ap_ssid, ssid, sizeof(_prefs.wifi_ap_ssid)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi ap ssid updated"); + } else { + Serial.println(" Error: failed to set wifi config"); + } + } else { + Serial.println(" Error: ssid too long"); + } + } else if (memcmp(config, "wifi.ap.pwd ", 12) == 0) { + const char* pwd = &config[12]; + if (strlen(pwd) < sizeof(_prefs.wifi_ap_pwd)) { + StrHelper::strncpy(_prefs.wifi_ap_pwd, pwd, sizeof(_prefs.wifi_ap_pwd)); + if (applyWifiPrefs(_prefs)) { + savePrefs(); + Serial.println(" > wifi ap password updated"); + } else { + Serial.println(" Error: failed to set wifi config (password must be empty or >= 8 chars)"); + } + } else { + Serial.println(" Error: password too long"); + } +#endif } else { Serial.printf(" Error: unknown config: %s\n", config); } +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + } else if (memcmp(cli_command, "get ", 4) == 0) { + const char* key = &cli_command[4]; + String wifi_mac_str = WiFi.macAddress(); + const char* wifi_status = "off"; + if (WiFi.status() == WL_CONNECTED) { + wifi_status = "connected"; + } else if (_prefs.wifi_mode == 0) { + wifi_status = "ap"; + } else if (_prefs.wifi_mode == 1 && _prefs.transport_mode == 1 && _prefs.wifi_ssid[0] != 0) { + wifi_status = "connecting"; + } + + if (strcmp(key, "ip") == 0 || strcmp(key, "wifi.ip") == 0) { + if (WiFi.status() == WL_CONNECTED) { + Serial.printf(" > %s\n", WiFi.localIP().toString().c_str()); + } else if (_prefs.wifi_mode == 0) { + Serial.printf(" > %s\n", WiFi.softAPIP().toString().c_str()); + } else { + Serial.println(" > 0.0.0.0"); + } + } else if (strcmp(key, "tcp.port") == 0) { + if (auto* transport = CompanionTransportInterface::instance()) { + Serial.printf(" > %u\n", transport->getTcpPort()); + } else { + Serial.println(" > 5000"); + } + } else if (strcmp(key, "wifi.gateway") == 0) { + Serial.printf(" > %s\n", WiFi.gatewayIP().toString().c_str()); + } else if (strcmp(key, "wifi.mask") == 0) { + Serial.printf(" > %s\n", WiFi.subnetMask().toString().c_str()); + } else if (strcmp(key, "wifi.status") == 0) { + Serial.printf(" > %s\n", wifi_status); + } else if (strcmp(key, "wifi.rssi") == 0) { + if (WiFi.status() == WL_CONNECTED) { + Serial.printf(" > %ld\n", (long)WiFi.RSSI()); + } else { + Serial.println(" > n/a"); + } + } else if (strcmp(key, "wifi.mac") == 0) { + Serial.printf(" > %s\n", wifi_mac_str.c_str()); + } else if (strcmp(key, "transport") == 0) { + Serial.printf(" > %s\n", _prefs.transport_mode == 1 ? "tcp" : "ble"); + } else if (strcmp(key, "wifi.mode") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_mode == 1 ? "client" : "ap"); + } else if (strcmp(key, "wifi.ssid") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_ssid); + } else if (strcmp(key, "wifi.pwd") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_pwd); + } else if (strcmp(key, "wifi.ap.ssid") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_ap_ssid); + } else if (strcmp(key, "wifi.ap.pwd") == 0) { + Serial.printf(" > %s\n", _prefs.wifi_ap_pwd); + } else { + Serial.printf(" Error: unknown key: %s\n", key); + } +#endif +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + } else if (strcmp(cli_command, "wifi connect") == 0) { + _prefs.transport_mode = 1; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > switched to TCP/WiFi transport"); + } else { + Serial.println(" Error: failed to enable TCP/WiFi"); + } + } else if (strcmp(cli_command, "ble connect") == 0) { + _prefs.transport_mode = 0; + if (applyTransportModePref(_prefs)) { + savePrefs(); + Serial.println(" > switched to BLE transport"); + } else { + Serial.println(" Error: failed to enable BLE"); + } +#endif } else if (strcmp(cli_command, "rebuild") == 0) { bool success = _store->formatFileSystem(); if (success) { @@ -2035,6 +2379,15 @@ void MyMesh::loop() { dirty_contacts_expiry = 0; } +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (_pending_transport_apply && _serial && !_serial->isWriteBusy() && + millisHasNowPassed(_pending_transport_apply_at)) { + _pending_transport_apply = false; + _pending_transport_apply_at = 0; + applyTransportModePref(_prefs); // best-effort apply after command response is sent + } +#endif + #ifdef DISPLAY_CLASS if (_ui) _ui->setHasConnection(_serial->isConnected()); #endif diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 4d77b5ab7..1e9d0e400 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -201,6 +201,8 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { uint32_t _active_ble_pin; bool _iter_started; bool _cli_rescue; + bool _pending_transport_apply; + unsigned long _pending_transport_apply_at; char cli_command[80]; uint8_t app_target_ver; uint8_t *sign_data; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 090209c1d..7ef3ef666 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -31,4 +31,10 @@ struct NodePrefs { // persisted to file uint8_t client_repeat; uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) -}; \ No newline at end of file + uint8_t transport_mode; // 0 = BLE, 1 = TCP/WiFi + char wifi_ssid[33]; + char wifi_pwd[65]; + uint8_t wifi_mode; // 0 = AP, 1 = Client/STA + char wifi_ap_ssid[33]; + char wifi_ap_pwd[65]; +}; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index eff9efca4..02e5c5c4f 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -35,7 +35,16 @@ static uint32_t _atoi(const char* sp) { #endif #ifdef ESP32 - #ifdef WIFI_SSID + #if defined(COMPANION_ALL_TRANSPORTS) + #include + CompanionTransportInterface serial_interface; + #ifdef SERIAL_RX + HardwareSerial companion_serial(1); + #endif + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #elif defined(WIFI_SSID) #include SerialWifiInterface serial_interface; #ifndef TCP_PORT @@ -193,7 +202,29 @@ void setup() { #endif ); -#ifdef WIFI_SSID +#if defined(COMPANION_ALL_TRANSPORTS) + board.setInhibitSleep(true); // transport manager will keep one wireless stack active + #if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + serial_interface.begin(companion_serial, BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin(), TCP_PORT); + #else + serial_interface.begin(Serial, BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin(), TCP_PORT); + #endif + serial_interface.setWifiCredentials(the_mesh.getNodePrefs()->wifi_ssid, the_mesh.getNodePrefs()->wifi_pwd); + serial_interface.setWifiApCredentials(the_mesh.getNodePrefs()->wifi_ap_ssid, the_mesh.getNodePrefs()->wifi_ap_pwd); + serial_interface.setWifiMode( + the_mesh.getNodePrefs()->wifi_mode == 1 + ? CompanionTransportInterface::WIFI_MODE_STA_CLIENT + : CompanionTransportInterface::WIFI_MODE_AP_ONLY + ); + serial_interface.setWirelessMode( + the_mesh.getNodePrefs()->transport_mode == 1 + ? CompanionTransportInterface::WIRELESS_MODE_TCP + : CompanionTransportInterface::WIRELESS_MODE_BLE, + true + ); +#elif defined(WIFI_SSID) board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.begin(WIFI_SSID, WIFI_PWD); serial_interface.begin(TCP_PORT); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 265532be0..a43d9515d 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2,6 +2,12 @@ #include #include "../MyMesh.h" #include "target.h" +#if defined(ESP32) + #include +#endif +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + #include +#endif #ifdef WIFI_SSID #include #endif @@ -77,18 +83,19 @@ class SplashScreen : public UIScreen { class HomeScreen : public UIScreen { enum HomePage { - FIRST, - RECENT, - RADIO, - BLUETOOTH, - ADVERT, + STATUS, + RECENT_ADVERTS, + RADIO_INFO, + BLE_TRANSPORT, + WIFI_TRANSPORT, + ADVERT_ACTION, #if ENV_INCLUDE_GPS == 1 - GPS, + GPS_STATUS, #endif #if UI_SENSORS_PAGE == 1 - SENSORS, + SENSOR_VALUES, #endif - SHUTDOWN, + POWER, Count // keep as last }; @@ -100,6 +107,35 @@ class HomeScreen : public UIScreen { bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; + bool isPageVisible(HomePage page) const { + if (page == HomePage::BLE_TRANSPORT) return _node_prefs->transport_mode == 0; + if (page == HomePage::WIFI_TRANSPORT) return _node_prefs->transport_mode == 1; + return true; + } + + void ensureVisiblePage() { + if (isPageVisible((HomePage)_page)) return; + for (int i = 0; i < HomePage::Count; i++) { + uint8_t candidate = (_page + i) % HomePage::Count; + if (isPageVisible((HomePage)candidate)) { + _page = candidate; + return; + } + } + } + + uint8_t nextVisiblePage(int dir) const { + for (int i = 1; i <= HomePage::Count; i++) { + int candidate = (int)_page + (dir * i); + while (candidate < 0) candidate += HomePage::Count; + candidate %= HomePage::Count; + if (isPageVisible((HomePage)candidate)) { + return (uint8_t)candidate; + } + } + return _page; + } + void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -180,6 +216,7 @@ class HomeScreen : public UIScreen { } int render(DisplayDriver& display) override { + ensureVisiblePage(); char tmp[80]; // node name display.setTextSize(1); @@ -203,7 +240,7 @@ class HomeScreen : public UIScreen { } } - if (_page == HomePage::FIRST) { + if (_page == HomePage::STATUS) { display.setColor(DisplayDriver::YELLOW); display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); @@ -220,13 +257,13 @@ class HomeScreen : public UIScreen { display.setTextSize(1); display.drawTextCentered(display.width() / 2, 43, "< Connected >"); - } else if (the_mesh.getBLEPin() != 0) { // BT pin + } else if (_node_prefs->transport_mode == 0 && the_mesh.getBLEPin() != 0) { // BT pin display.setColor(DisplayDriver::RED); display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, 43, tmp); } - } else if (_page == HomePage::RECENT) { + } else if (_page == HomePage::RECENT_ADVERTS) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); display.setColor(DisplayDriver::GREEN); int y = 20; @@ -251,7 +288,7 @@ class HomeScreen : public UIScreen { display.setCursor(display.width() - timestamp_width - 1, y); display.print(tmp); } - } else if (_page == HomePage::RADIO) { + } else if (_page == HomePage::RADIO_INFO) { display.setColor(DisplayDriver::YELLOW); display.setTextSize(1); // freq / sf @@ -270,19 +307,84 @@ class HomeScreen : public UIScreen { display.setCursor(0, 53); sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); display.print(tmp); - } else if (_page == HomePage::BLUETOOTH) { + } else if (_page == HomePage::BLE_TRANSPORT) { display.setColor(DisplayDriver::GREEN); - display.drawXbm((display.width() - 32) / 2, 18, + display.drawTextCentered(display.width() / 2, 16, "BLE transport"); + display.drawXbm((display.width() - 32) / 2, 24, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); - display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL); - } else if (_page == HomePage::ADVERT) { + display.drawTextCentered(display.width() / 2, 56, "Long press: WiFi"); + } else if (_page == HomePage::WIFI_TRANSPORT) { + const int row1_y = 28; + const int row2_y = 38; + const int row3_y = 48; + const int footer_y = 57; + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 20, "WiFi transport"); + uint16_t tcp_port = 5000; +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (auto* transport = CompanionTransportInterface::instance()) { + tcp_port = transport->getTcpPort(); + } +#endif + bool flip = ((millis() / 1300) % 2) == 0; + + if (_node_prefs->wifi_mode == 0) { + char ap_name[40]; + if (_node_prefs->wifi_ap_ssid[0]) { + snprintf(ap_name, sizeof(ap_name), "AP:%s", _node_prefs->wifi_ap_ssid); + } else { + snprintf(ap_name, sizeof(ap_name), "AP:MeshCore-%s", _node_prefs->node_name); + } + const char* ap_pwd = _node_prefs->wifi_ap_pwd[0] ? _node_prefs->wifi_ap_pwd : ""; + char ap_pwd_row[40]; + snprintf(ap_pwd_row, sizeof(ap_pwd_row), "PW:%s", ap_pwd); + + if (flip) { + display.drawTextEllipsized(0, row1_y, display.width(), ap_name); + display.drawTextEllipsized(0, row2_y, display.width(), ap_pwd_row); + } else { + display.drawTextEllipsized(0, row1_y, display.width(), ap_pwd_row); + display.drawTextEllipsized(0, row2_y, display.width(), ap_name); + } +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + IPAddress ap_ip = WiFi.softAPIP(); + snprintf(tmp, sizeof(tmp), "%d.%d.%d.%d:%u", ap_ip[0], ap_ip[1], ap_ip[2], ap_ip[3], tcp_port); +#else + snprintf(tmp, sizeof(tmp), "0.0.0.0:%u", tcp_port); +#endif + display.drawTextCentered(display.width() / 2, row3_y, tmp); + } else { + display.drawTextCentered(display.width() / 2, row1_y, "Mode: Client"); + snprintf(tmp, sizeof(tmp), "SSID:%s", _node_prefs->wifi_ssid[0] ? _node_prefs->wifi_ssid : ""); + display.drawTextEllipsized(0, row2_y, display.width(), tmp); +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (WiFi.status() == WL_CONNECTED) { + IPAddress ip = WiFi.localIP(); + snprintf(tmp, sizeof(tmp), "%d.%d.%d.%d:%u", ip[0], ip[1], ip[2], ip[3], tcp_port); + } else { + snprintf(tmp, sizeof(tmp), "0.0.0.0:%u", tcp_port); + } +#else + snprintf(tmp, sizeof(tmp), "0.0.0.0:%u", tcp_port); +#endif + display.drawTextCentered(display.width() / 2, row3_y, tmp); + } + + display.setColor(DisplayDriver::YELLOW); + // Keep labels short enough to fully fit 128px width with centered rendering. + const char* labels[2] = {"Hold: BLE", "Triple: AP/Client"}; + const int label_turn_ms = 3500; + int idx = (millis() / label_turn_ms) % 2; + display.drawTextCentered(display.width() / 2, footer_y, labels[idx]); + } else if (_page == HomePage::ADVERT_ACTION) { display.setColor(DisplayDriver::GREEN); display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL); #if ENV_INCLUDE_GPS == 1 - } else if (_page == HomePage::GPS) { + } else if (_page == HomePage::GPS_STATUS) { LocationProvider* nmea = sensors.getLocationProvider(); char buf[50]; int y = 18; @@ -321,7 +423,7 @@ class HomeScreen : public UIScreen { } #endif #if UI_SENSORS_PAGE == 1 - } else if (_page == HomePage::SENSORS) { + } else if (_page == HomePage::SENSOR_VALUES) { int y = 18; refresh_sensors(); char buf[30]; @@ -392,7 +494,7 @@ class HomeScreen : public UIScreen { if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; else sensors_scroll_offset = 0; #endif - } else if (_page == HomePage::SHUTDOWN) { + } else if (_page == HomePage::POWER) { display.setColor(DisplayDriver::GREEN); display.setTextSize(1); if (_shutdown_init) { @@ -406,18 +508,36 @@ class HomeScreen : public UIScreen { } bool handleInput(char c) override { + ensureVisiblePage(); if (c == KEY_LEFT || c == KEY_PREV) { - _page = (_page + HomePage::Count - 1) % HomePage::Count; + _page = nextVisiblePage(-1); return true; } if (c == KEY_NEXT || c == KEY_RIGHT) { - _page = (_page + 1) % HomePage::Count; - if (_page == HomePage::RECENT) { + _page = nextVisiblePage(1); + if (_page == HomePage::RECENT_ADVERTS) { _task->showAlert("Recent adverts", 800); } return true; } - if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { + if (c == KEY_ENTER && _page == HomePage::BLE_TRANSPORT) { +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (_task->consumeLongPress()) { + bool success = false; + if (auto* transport = CompanionTransportInterface::instance()) { + success = transport->setWirelessMode(CompanionTransportInterface::WIRELESS_MODE_TCP); + if (success) _node_prefs->transport_mode = 1; + } + if (success) { + the_mesh.savePrefs(); + _task->notify(UIEventType::ack); + _task->showAlert("WiFi enabled", 1200); + } else { + _task->showAlert("Transport switch failed", 1200); + } + return true; + } +#endif if (_task->isSerialEnabled()) { // toggle Bluetooth on/off _task->disableSerial(); } else { @@ -425,7 +545,49 @@ class HomeScreen : public UIScreen { } return true; } - if (c == KEY_ENTER && _page == HomePage::ADVERT) { + if (c == KEY_ENTER && _page == HomePage::WIFI_TRANSPORT) { +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + if (_task->consumeLongPress()) { + bool success = false; + if (auto* transport = CompanionTransportInterface::instance()) { + success = transport->setWirelessMode(CompanionTransportInterface::WIRELESS_MODE_BLE); + if (success) _node_prefs->transport_mode = 0; + } + if (success) { + the_mesh.savePrefs(); + _task->notify(UIEventType::ack); + _task->showAlert("BLE enabled", 1200); + } else { + _task->showAlert("Transport switch failed", 1200); + } + } +#endif + return true; + } + if (c == KEY_SELECT && _page == HomePage::WIFI_TRANSPORT) { +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + { + bool success = false; + uint8_t next_mode = _node_prefs->wifi_mode == 0 ? 1 : 0; + if (auto* transport = CompanionTransportInterface::instance()) { + auto mode = next_mode == 0 + ? CompanionTransportInterface::WIFI_MODE_AP_ONLY + : CompanionTransportInterface::WIFI_MODE_STA_CLIENT; + success = transport->setWifiMode(mode); + if (success) _node_prefs->wifi_mode = next_mode; + } + if (success) { + the_mesh.savePrefs(); + _task->notify(UIEventType::ack); + _task->showAlert(next_mode == 0 ? "WiFi mode: AP" : "WiFi mode: Client", 1400); + } else { + _task->showAlert("WiFi mode switch failed", 1400); + } + } +#endif + return true; + } + if (c == KEY_ENTER && _page == HomePage::ADVERT_ACTION) { _task->notify(UIEventType::ack); if (the_mesh.advert()) { _task->showAlert("Advert sent!", 1000); @@ -435,19 +597,19 @@ class HomeScreen : public UIScreen { return true; } #if ENV_INCLUDE_GPS == 1 - if (c == KEY_ENTER && _page == HomePage::GPS) { + if (c == KEY_ENTER && _page == HomePage::GPS_STATUS) { _task->toggleGPS(); return true; } #endif #if UI_SENSORS_PAGE == 1 - if (c == KEY_ENTER && _page == HomePage::SENSORS) { + if (c == KEY_ENTER && _page == HomePage::SENSOR_VALUES) { _task->toggleGPS(); next_sensors_refresh=0; return true; } #endif - if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { + if (c == KEY_ENTER && _page == HomePage::POWER) { _shutdown_init = true; // need to wait for button to be released return true; } @@ -559,6 +721,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #endif _node_prefs = node_prefs; + _last_transport_mode = _node_prefs ? _node_prefs->transport_mode : 255; + _last_wifi_mode = _node_prefs ? _node_prefs->wifi_mode : 255; #if ENV_INCLUDE_GPS == 1 // Apply GPS preferences from stored prefs @@ -716,6 +880,35 @@ bool UITask::isButtonPressed() const { void UITask::loop() { char c = 0; + + if (_node_prefs != NULL) { + if (_node_prefs->transport_mode != _last_transport_mode) { + _last_transport_mode = _node_prefs->transport_mode; + _transport_reboot_pending = true; + _transport_reboot_deadline = millis() + 10000; + _next_refresh = 0; // immediate redraw after remote command changes + } + if (_node_prefs->wifi_mode != _last_wifi_mode) { + _last_wifi_mode = _node_prefs->wifi_mode; + _wifi_mode_notice_active = true; + _wifi_mode_notice_value = _node_prefs->wifi_mode; + _wifi_mode_notice_until = millis() + 3000; + _last_transport_mode = _node_prefs->transport_mode; + _next_refresh = 0; // immediate redraw after remote command changes + } + } + + if (_transport_reboot_pending && millis() >= _transport_reboot_deadline) { + _transport_reboot_pending = false; + shutdown(true); + return; + } + + if (_wifi_mode_notice_active && millis() >= _wifi_mode_notice_until) { + _wifi_mode_notice_active = false; + _next_refresh = 0; + } + #if UI_HAS_JOYSTICK int ev = user_btn.check(); if (ev == BUTTON_EVENT_CLICK) { @@ -779,7 +972,11 @@ void UITask::loop() { #endif if (c != 0 && curr) { - curr->handleInput(c); + bool handled = curr->handleInput(c); + if (!handled && c == KEY_SELECT) { + toggleBuzzer(); + } + _pending_long_press = false; // one-shot flag _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer _next_refresh = 100; // trigger refresh } @@ -795,7 +992,41 @@ void UITask::loop() { if (_display != NULL && _display->isOn()) { if (millis() >= _next_refresh && curr) { _display->startFrame(); - int delay_millis = curr->render(*_display); + int delay_millis = 5000; + if (_transport_reboot_pending) { + int remaining_secs = (int)((_transport_reboot_deadline - millis() + 999) / 1000); + if (remaining_secs < 0) remaining_secs = 0; + + _display->setTextSize(1); + _display->setColor(DisplayDriver::YELLOW); + _display->drawTextCentered(_display->width() / 2, 8, "Transport Changed"); + _display->setColor(DisplayDriver::LIGHT); + _display->drawTextCentered(_display->width() / 2, 22, "Reboot required"); + _display->drawTextCentered(_display->width() / 2, 34, "Auto reset in:"); + + char tmp[16]; + snprintf(tmp, sizeof(tmp), "%ds", remaining_secs); + _display->setTextSize(2); + _display->setColor(DisplayDriver::RED); + _display->drawTextCentered(_display->width() / 2, 46, tmp); + _display->setTextSize(1); + delay_millis = 200; + } else if (_wifi_mode_notice_active) { + _display->setTextSize(1); + _display->setColor(DisplayDriver::YELLOW); + _display->drawTextCentered(_display->width() / 2, 10, "WiFi Mode Updated"); + _display->setColor(DisplayDriver::LIGHT); + if (_wifi_mode_notice_value == 0) { + _display->drawTextCentered(_display->width() / 2, 30, "Switching to AP mode"); + _display->drawTextCentered(_display->width() / 2, 44, "SSID: device hotspot"); + } else { + _display->drawTextCentered(_display->width() / 2, 30, "Switching to Client"); + _display->drawTextCentered(_display->width() / 2, 44, "Joining configured SSID"); + } + delay_millis = 200; + } else { + delay_millis = curr->render(*_display); + } if (millis() < _alert_expiry) { // render alert popup _display->setTextSize(1); int y = _display->height() / 3; @@ -864,6 +1095,8 @@ char UITask::handleLongPress(char c) { if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue the_mesh.enterCLIRescue(); c = 0; // consume event + } else { + _pending_long_press = true; } return c; } @@ -877,8 +1110,6 @@ char UITask::handleDoubleClick(char c) { char UITask::handleTripleClick(char c) { MESH_DEBUG_PRINTLN("UITask: triple click triggered"); checkDisplayOn(c); - toggleBuzzer(); - c = 0; return c; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index a77ad6e7e..6464fb031 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -32,6 +32,14 @@ class UITask : public AbstractUITask { GenericVibration vibration; #endif unsigned long _next_refresh, _auto_off; + bool _pending_long_press; + uint8_t _last_transport_mode; + uint8_t _last_wifi_mode; + bool _transport_reboot_pending; + unsigned long _transport_reboot_deadline; + bool _wifi_mode_notice_active; + unsigned long _wifi_mode_notice_until; + uint8_t _wifi_mode_notice_value; NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; @@ -68,6 +76,14 @@ class UITask : public AbstractUITask { UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; ui_started_at = 0; + _pending_long_press = false; + _last_transport_mode = 255; + _last_wifi_mode = 255; + _transport_reboot_pending = false; + _transport_reboot_deadline = 0; + _wifi_mode_notice_active = false; + _wifi_mode_notice_until = 0; + _wifi_mode_notice_value = 255; curr = NULL; } void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); @@ -77,6 +93,7 @@ class UITask : public AbstractUITask { int getMsgCount() const { return _msgcount; } bool hasDisplay() const { return _display != NULL; } bool isButtonPressed() const; + bool consumeLongPress() { bool v = _pending_long_press; _pending_long_press = false; return v; } bool isBuzzerQuiet() { #ifdef PIN_BUZZER diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index cbe237902..402552728 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -119,4 +119,4 @@ static const uint8_t advert_icon[] = { static const uint8_t muted_icon[] = { 0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 -}; \ No newline at end of file +}; diff --git a/src/helpers/esp32/CompanionTransportInterface.cpp b/src/helpers/esp32/CompanionTransportInterface.cpp new file mode 100644 index 000000000..e0b7e4152 --- /dev/null +++ b/src/helpers/esp32/CompanionTransportInterface.cpp @@ -0,0 +1,275 @@ +#include "CompanionTransportInterface.h" +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) +#include +#include + +CompanionTransportInterface* CompanionTransportInterface::_instance = nullptr; + +namespace { +constexpr unsigned long WIFI_STA_RETRY_BASE_MS = 10000; +constexpr unsigned long WIFI_STA_RETRY_MAX_MS = 300000; +constexpr uint8_t WIFI_STA_MAX_RETRIES = 5; +} + +CompanionTransportInterface::CompanionTransportInterface() + : _isEnabled(false), _wifi_server_started(false), _last_source(0), _mode(WIRELESS_MODE_BLE), _wifi_started(false), + _last_usb_activity_at(0), _wifi_retry_count(0), _wifi_retry_suspended(false), _next_wifi_retry_at(0), + _wifi_mode(WIFI_MODE_AP_ONLY), _tcp_port(5000) { + _node_name[0] = 0; + _wifi_ssid[0] = 0; + _wifi_pwd[0] = 0; + _wifi_ap_ssid[0] = 0; + _wifi_ap_pwd[0] = 0; + _instance = this; +} + +bool CompanionTransportInterface::usbSessionActive() const { + return _last_usb_activity_at != 0 && (unsigned long)(millis() - _last_usb_activity_at) < USB_ACTIVITY_TIMEOUT_MS; +} + +void CompanionTransportInterface::resetWifiRetryState() { + _wifi_retry_count = 0; + _wifi_retry_suspended = false; + _next_wifi_retry_at = 0; +} + +void CompanionTransportInterface::begin(Stream& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port) { + _tcp_port = tcp_port; + snprintf(_node_name, sizeof(_node_name), "%s", node_name); + + _usb.begin(usb_serial); + _ble.begin(ble_prefix, node_name, ble_pin); +} + +void CompanionTransportInterface::begin(HardwareSerial& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port) { + begin((Stream&)usb_serial, ble_prefix, node_name, ble_pin, tcp_port); +} + +void CompanionTransportInterface::connectWifi() { + if (_wifi_ssid[0] == 0) { + _wifi_retry_suspended = true; + _next_wifi_retry_at = 0; + return; + } + + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(false); + WiFi.begin(_wifi_ssid, _wifi_pwd); + + _wifi_retry_count++; + unsigned long retry_delay = WIFI_STA_RETRY_BASE_MS; + if (_wifi_retry_count > 1) { + retry_delay <<= (_wifi_retry_count - 1); + if (retry_delay > WIFI_STA_RETRY_MAX_MS) retry_delay = WIFI_STA_RETRY_MAX_MS; + } + _next_wifi_retry_at = millis() + retry_delay; +} + +void CompanionTransportInterface::startWifiAP() { + WiFi.mode(WIFI_AP); + char ap_name[33] = {0}; + if (_wifi_ap_ssid[0] != 0) { + snprintf(ap_name, sizeof(ap_name), "%s", _wifi_ap_ssid); + } else { + snprintf(ap_name, sizeof(ap_name), "MeshCore-%s", _node_name); + } + + if (_wifi_ap_pwd[0] != 0 && strlen(_wifi_ap_pwd) >= 8) { + WiFi.softAP(ap_name, _wifi_ap_pwd); + } else { + WiFi.softAP(ap_name); + } +} + +void CompanionTransportInterface::startWifi() { + stopWifi(); + resetWifiRetryState(); + + if (_wifi_mode == WIFI_MODE_STA_CLIENT) { + if (_wifi_ssid[0] == 0) return; + _wifi_started = true; + connectWifi(); + } else { + _wifi_started = true; + startWifiAP(); + } +} + +void CompanionTransportInterface::stopWifi() { + if (!_wifi_started) return; + WiFi.disconnect(true, true); + WiFi.mode(WIFI_OFF); + _wifi_started = false; +} + +void CompanionTransportInterface::startWireless() { + if (_mode == WIRELESS_MODE_TCP) { + startWifi(); + if (!_wifi_server_started) { + _wifi.begin(_tcp_port); + _wifi_server_started = true; + } + _wifi.enable(); + _ble.disable(); + } else { + _wifi.disable(); + stopWifi(); + resetWifiRetryState(); + _ble.enable(); + } +} + +void CompanionTransportInterface::stopWireless() { + _ble.disable(); + _wifi.disable(); + stopWifi(); + resetWifiRetryState(); +} + +bool CompanionTransportInterface::setWirelessMode(WirelessMode mode, bool persist_only) { + if (mode != WIRELESS_MODE_BLE && mode != WIRELESS_MODE_TCP) { + return false; + } + _mode = mode; + if (_isEnabled && !persist_only) { + // Keep active USB control session stable: if command/control is over USB, + // defer live wireless stack switch until after USB disconnect/reconnect. + if (!(_last_source == 0 && _usb.isConnected())) { + startWireless(); + } + } + return true; +} + +bool CompanionTransportInterface::setWifiCredentials(const char* ssid, const char* pwd) { + if (!ssid || !pwd) return false; + if (strlen(ssid) > 32 || strlen(pwd) > 64) return false; + snprintf(_wifi_ssid, sizeof(_wifi_ssid), "%s", ssid); + snprintf(_wifi_pwd, sizeof(_wifi_pwd), "%s", pwd); + resetWifiRetryState(); + + if (_isEnabled && _mode == WIRELESS_MODE_TCP && _wifi_mode == WIFI_MODE_STA_CLIENT) { + startWifi(); + } + return true; +} + +bool CompanionTransportInterface::setWifiMode(WifiMode mode) { + if (mode != WIFI_MODE_AP_ONLY && mode != WIFI_MODE_STA_CLIENT) { + return false; + } + _wifi_mode = mode; + resetWifiRetryState(); + if (_isEnabled && _mode == WIRELESS_MODE_TCP) { + startWifi(); + } + return true; +} + +bool CompanionTransportInterface::setWifiApCredentials(const char* ssid, const char* pwd) { + if (!ssid || !pwd) return false; + if (strlen(ssid) > 32 || strlen(pwd) > 64) return false; + if (pwd[0] != 0 && strlen(pwd) < 8) return false; // WPA2 requirement + snprintf(_wifi_ap_ssid, sizeof(_wifi_ap_ssid), "%s", ssid); + snprintf(_wifi_ap_pwd, sizeof(_wifi_ap_pwd), "%s", pwd); + + if (_isEnabled && _mode == WIRELESS_MODE_TCP && _wifi_mode == WIFI_MODE_AP_ONLY) { + startWifi(); + } + return true; +} + +bool CompanionTransportInterface::setTcpPort(uint16_t port) { + if (port == 0) return false; + _tcp_port = port; + + // If TCP server is already running, restart it on the new port. + if (_wifi_server_started) { + _wifi.disable(); + _wifi.begin(_tcp_port); + if (_mode == WIRELESS_MODE_TCP) { + _wifi.enable(); + } + } + return true; +} + +void CompanionTransportInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + _last_source = 0xFF; + _last_usb_activity_at = 0; + _usb.enable(); + startWireless(); +} + +void CompanionTransportInterface::disable() { + _isEnabled = false; + stopWireless(); + _usb.disable(); +} + +bool CompanionTransportInterface::isConnected() const { + if (_last_source == 2) return _wifi.isConnected(); + if (_last_source == 1) return _ble.isConnected(); + if (_last_source == 0) return usbSessionActive(); + return _ble.isConnected() || _wifi.isConnected() || usbSessionActive(); +} + +bool CompanionTransportInterface::isWriteBusy() const { + if (_last_source == 2) return _wifi.isWriteBusy(); + if (_last_source == 1) return _ble.isWriteBusy(); + return _usb.isWriteBusy(); +} + +size_t CompanionTransportInterface::writeFrame(const uint8_t src[], size_t len) { + if (_last_source == 0) _last_usb_activity_at = millis(); + if (_last_source == 2 && _wifi.isConnected()) return _wifi.writeFrame(src, len); + if (_last_source == 1 && _ble.isConnected()) return _ble.writeFrame(src, len); + return _usb.writeFrame(src, len); +} + +size_t CompanionTransportInterface::checkRecvFrame(uint8_t dest[]) { + // Prioritise USB input so local serial control remains reliable even + // when wireless transport is active or reconnecting. + size_t usb_len = _usb.checkRecvFrame(dest); + if (usb_len > 0) { + _last_source = 0; + _last_usb_activity_at = millis(); + return usb_len; + } + + if (_mode == WIRELESS_MODE_TCP) { + if (_wifi_mode == WIFI_MODE_STA_CLIENT) { + if (WiFi.status() == WL_CONNECTED) { + resetWifiRetryState(); + } else if (_wifi_started && !_wifi_retry_suspended && _wifi_ssid[0] != 0) { + unsigned long now = millis(); + if ((long)(now - _next_wifi_retry_at) >= 0) { + if (_wifi_retry_count >= WIFI_STA_MAX_RETRIES) { + stopWifi(); + _wifi_retry_suspended = true; + } else { + WiFi.disconnect(true, false); + connectWifi(); + } + } + } + } + + size_t wifi_len = _wifi.checkRecvFrame(dest); + if (wifi_len > 0) { + _last_source = 2; + return wifi_len; + } + } else { + size_t ble_len = _ble.checkRecvFrame(dest); + if (ble_len > 0) { + _last_source = 1; + return ble_len; + } + } + return 0; +} + +#endif diff --git a/src/helpers/esp32/CompanionTransportInterface.h b/src/helpers/esp32/CompanionTransportInterface.h new file mode 100644 index 000000000..030968f36 --- /dev/null +++ b/src/helpers/esp32/CompanionTransportInterface.h @@ -0,0 +1,89 @@ +#pragma once + +#if defined(ESP32) && defined(COMPANION_ALL_TRANSPORTS) + +#include +#include "../BaseSerialInterface.h" +#include "../ArduinoSerialInterface.h" +#include "SerialBLEInterface.h" +#include "SerialWifiInterface.h" + +class CompanionTransportInterface : public BaseSerialInterface { +public: + enum WirelessMode : uint8_t { + WIRELESS_MODE_BLE = 0, + WIRELESS_MODE_TCP = 1 + }; + enum WifiMode : uint8_t { + WIFI_MODE_AP_ONLY = 0, + WIFI_MODE_STA_CLIENT = 1 + }; + +private: + static CompanionTransportInterface* _instance; + static constexpr unsigned long USB_ACTIVITY_TIMEOUT_MS = 15000; + + ArduinoSerialInterface _usb; + SerialBLEInterface _ble; + SerialWifiInterface _wifi; + + bool _isEnabled; + bool _wifi_server_started; + uint8_t _last_source; // 0=usb,1=ble,2=wifi + WirelessMode _mode; + bool _wifi_started; + unsigned long _last_usb_activity_at; + uint8_t _wifi_retry_count; + bool _wifi_retry_suspended; + unsigned long _next_wifi_retry_at; + + char _node_name[33]; + char _wifi_ssid[33]; + char _wifi_pwd[65]; + WifiMode _wifi_mode; + char _wifi_ap_ssid[33]; + char _wifi_ap_pwd[65]; + uint16_t _tcp_port; + +public: + CompanionTransportInterface(); + + static CompanionTransportInterface* instance() { return _instance; } + + void begin(Stream& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port); + void begin(HardwareSerial& usb_serial, const char* ble_prefix, char* node_name, uint32_t ble_pin, uint16_t tcp_port); + + bool setWirelessMode(WirelessMode mode, bool persist_only = false); + WirelessMode getWirelessMode() const { return _mode; } + + bool setWifiCredentials(const char* ssid, const char* pwd); + bool setWifiMode(WifiMode mode); + WifiMode getWifiMode() const { return _wifi_mode; } + bool setWifiApCredentials(const char* ssid, const char* pwd); + bool setTcpPort(uint16_t port); + uint16_t getTcpPort() const { return _tcp_port; } + const char* getWifiSSID() const { return _wifi_ssid; } + const char* getWifiApSSID() const { return _wifi_ap_ssid; } + const char* getWifiApPassword() const { return _wifi_ap_pwd; } + bool hasWifiCredentials() const { return _wifi_ssid[0] != 0; } + + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + bool isConnected() const override; + bool isWriteBusy() const override; + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; + +private: + bool usbSessionActive() const; + void resetWifiRetryState(); + void startWireless(); + void stopWireless(); + void startWifi(); + void stopWifi(); + void connectWifi(); + void startWifiAP(); +}; + +#endif diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e3..f07ea1c2a 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -10,47 +10,72 @@ #define ADVERT_RESTART_DELAY 1000 // millis -void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { - _pin_code = pin_code; - - if (strcmp(name, "@@MAC") == 0) { - uint8_t addr[8]; - memset(addr, 0, sizeof(addr)); - esp_efuse_mac_get_default(addr); - sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param) - addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); - } - char dev_name[32+16]; - sprintf(dev_name, "%s%s", prefix, name); +void SerialBLEInterface::initStack() { + if (_stack_initialized || _device_name[0] == 0) return; - // Create the BLE Device - BLEDevice::init(dev_name); + BLEDevice::init(_device_name); BLEDevice::setSecurityCallbacks(this); BLEDevice::setMTU(MAX_FRAME_SIZE); - BLESecurity sec; - sec.setStaticPIN(pin_code); + BLESecurity sec; + sec.setStaticPIN(_pin_code); sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND); - //BLEDevice::setPower(ESP_PWR_LVL_N8); - - // Create the BLE Server pServer = BLEDevice::createServer(); pServer->setCallbacks(this); - // Create the BLE Service pService = pServer->createService(SERVICE_UUID); - // Create a BLE Characteristic - pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); + pTxCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); pTxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM); pTxCharacteristic->addDescriptor(new BLE2902()); - BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); + BLECharacteristic * pRxCharacteristic = + pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE); pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM); pRxCharacteristic->setCallbacks(this); pServer->getAdvertising()->addServiceUUID(SERVICE_UUID); + _stack_initialized = true; +} + +void SerialBLEInterface::shutdownStack() { + if (!_stack_initialized) return; + + if (pServer != NULL && pServer->getAdvertising() != NULL) { + pServer->getAdvertising()->stop(); + } + if (pServer != NULL && pServer->getConnectedCount() > 0) { + pServer->disconnect(last_conn_id); + } + if (pService != NULL) { + pService->stop(); + } + + BLEDevice::deinit(false); + + pTxCharacteristic = NULL; + pService = NULL; + pServer = NULL; + oldDeviceConnected = deviceConnected = false; + adv_restart_time = 0; + last_conn_id = 0; + _stack_initialized = false; +} + +void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) { + _pin_code = pin_code; + + if (strcmp(name, "@@MAC") == 0) { + uint8_t addr[8]; + memset(addr, 0, sizeof(addr)); + esp_efuse_mac_get_default(addr); + sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param) + addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]); + } + snprintf(_device_name, sizeof(_device_name), "%s%s", prefix, name); + initStack(); } // -------- BLESecurityCallbacks methods @@ -132,6 +157,9 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat void SerialBLEInterface::enable() { if (_isEnabled) return; + initStack(); + if (!_stack_initialized || pService == NULL || pServer == NULL) return; + _isEnabled = true; clearBuffers(); @@ -151,12 +179,8 @@ void SerialBLEInterface::disable() { _isEnabled = false; BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); - - pServer->getAdvertising()->stop(); - pServer->disconnect(last_conn_id); - pService->stop(); - oldDeviceConnected = deviceConnected = false; - adv_restart_time = 0; + clearBuffers(); + shutdownStack(); } size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd1..a65fbec18 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -13,10 +13,12 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool deviceConnected; bool oldDeviceConnected; bool _isEnabled; + bool _stack_initialized; uint16_t last_conn_id; uint32_t _pin_code; unsigned long _last_write; unsigned long adv_restart_time; + char _device_name[49]; struct Frame { uint8_t len; @@ -30,6 +32,8 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE Frame send_queue[FRAME_QUEUE_SIZE]; void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + void initStack(); + void shutdownStack(); protected: // BLESecurityCallbacks methods @@ -56,9 +60,11 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE oldDeviceConnected = false; adv_restart_time = 0; _isEnabled = false; + _stack_initialized = false; _last_write = 0; last_conn_id = 0; send_queue_len = recv_queue_len = 0; + _device_name[0] = 0; } /** diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index f8cc93608..415eb9f50 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -195,3 +195,26 @@ build_src_filter = ${Heltec_lora32_v2.build_src_filter} lib_deps = ${Heltec_lora32_v2.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:Heltec_v2_companion_radio_tcp_usb_ble] +extends = Heltec_lora32_v2 +build_flags = + ${Heltec_lora32_v2.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=70 + -D MAX_GROUP_CHANNELS=8 + -D BLE_PIN_CODE=123456 + -D COMPANION_ALL_TRANSPORTS + -D OFFLINE_QUEUE_SIZE=200 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v2.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v2.lib_deps} + densaugeo/base64 @ ~1.4.0 diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 4d299104e..5cc8e02a0 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -202,6 +202,29 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:Heltec_v3_companion_radio_tcp_usb_ble] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=SSD1306Display + -D BLE_PIN_CODE=123456 + -D COMPANION_ALL_TRANSPORTS + -D OFFLINE_QUEUE_SIZE=256 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_v3_sensor] extends = Heltec_lora32_v3 build_flags = @@ -375,4 +398,4 @@ build_flags = build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/kiss_modem/> lib_deps = - ${Heltec_lora32_v3.lib_deps} \ No newline at end of file + ${Heltec_lora32_v3.lib_deps}