Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A single node runs a genuinely simultaneous **dual-role BLE stack** on NimBLE. I

- **Signed identity announces.** Identity rides in a signed `ANNOUNCE` (type `0x01`) carrying TLVs for nickname, Noise static key, and Ed25519 signing key, plus two Bitle-private TLVs: firmware version (`0xB0`) and role/authority flags (`0xB1`). A legacy `0x13` identity-announce is still emitted best-effort for older clients. Inbound announces are hard-rejected unless the sender ID equals SHA-256(announced Noise key)[0:8].

- **Dual-role BLE mesh relay.** Packets are encoded/decoded with the BitChat binary format and relayed to every other subscribed link. Relay is TTL-based (packets with `ttl <= 1` are dropped, otherwise the TTL byte is decremented before rebroadcast) and de-duplicated with an FNV-1a fingerprint over the packet bytes (skipping the TTL byte) kept in a 64-entry ring. Own echoes, `REQUEST_SYNC`, packets addressed to this node, and undirected Noise handshakes are never relayed. Phone-fragmented packets are reassembled in a small bounded pool (2 slots, up to 4 parts × 501 bytes, 15 s timeout); anything larger is forwarded relay-only. Max handled BLE packet size is 520 bytes. A 30 s subscribe watchdog drops links that connect but never enable notifications, and a short deny/cool-down list prevents immediately re-dialing a just-dropped peer.
- **Dual-role BLE mesh relay.** Packets are encoded/decoded with the BitChat binary format and relayed to every other subscribed link. Relay is TTL-based (packets with `ttl <= 1` are dropped, otherwise the TTL byte is decremented before rebroadcast) and de-duplicated with an FNV-1a fingerprint over the packet bytes (skipping the TTL byte) kept in a 64-entry ring. Own echoes, `REQUEST_SYNC`, packets addressed to this node, and undirected Noise handshakes are never relayed. Phone-fragmented packets are reassembled in a small bounded pool (2 slots, up to 4 parts × 501 bytes, 15 s timeout); anything larger is forwarded relay-only. Max handled BLE packet size is 520 bytes. A 30 s subscribe watchdog drops links that connect but never enable notifications, and a short deny/cool-down list prevents immediately re-dialing a just-dropped peer. Once a direct link is confirmed (via a signature-verified announce) to be another Bitle node rather than a phone, the node opportunistically requests LE Coded PHY on that connection for better margin at the edge of range; this is a non-binding request that either side may decline, never applies to phone connections, and does not change how far apart two nodes can first discover each other (discovery still happens on legacy 1M PHY, which phone scanning requires).

- **LoRa long-range trunk (ESP32-S3 nodes).** When an SX1262 is detected at boot, the node brings up a 915 MHz LoRa backbone between nodes — a second radio the phones never touch. The trunk registers as one more link in the transport-agnostic link registry, so the mesh relays BLE↔LoRa with no special cases: a message crosses the trunk and comes back down to BLE at the far end. See the [LoRa backhaul](#lora-backhaul) section for the details (framing, ARQ, spreading factor, range).

Expand Down
54 changes: 54 additions & 0 deletions main/bitchat_ble.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ static uint16_t s_tx_val_handle;
static bool s_host_synced;
static uint8_t s_own_addr_type;

/* Coded-PHY request outcome for one connection. Attempted at most once per
* link: an immediate local decline or an async accept/reject both settle it,
* since neither our controller's support nor the peer's changes mid-session. */
typedef enum {
BITLE_CODED_PHY_NOT_REQUESTED = 0,
BITLE_CODED_PHY_PENDING,
BITLE_CODED_PHY_SETTLED,
} bitle_coded_phy_state_t;

typedef struct {
bool in_use;
bool subscribed;
Expand All @@ -41,6 +50,7 @@ typedef struct {
uint16_t svc_end_handle;
ble_addr_t peer_addr;
uint64_t connected_at_ms;
bitle_coded_phy_state_t coded_phy_state;
} ble_conn_state_t;

static ble_conn_state_t s_connections[BITLE_BLE_MAX_CONNECTIONS];
Expand Down Expand Up @@ -548,6 +558,24 @@ static int gap_event_cb(struct ble_gap_event *event, void *arg)
event->mtu.conn_handle, event->mtu.value);
return 0;

case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE: {
ble_conn_state_t *state = find_conn(event->phy_updated.conn_handle);
if (state) {
/* Settled either way: a decline here is a normal, expected
* outcome (peer or local controller lacks/declines Coded PHY),
* not a fault — never retried for this link. */
state->coded_phy_state = BITLE_CODED_PHY_SETTLED;
}
if (event->phy_updated.status == 0) {
ESP_LOGI(TAG, "conn=%u PHY updated tx=%u rx=%u", event->phy_updated.conn_handle,
event->phy_updated.tx_phy, event->phy_updated.rx_phy);
} else {
ESP_LOGD(TAG, "conn=%u PHY update declined status=%d; link continues on current PHY",
event->phy_updated.conn_handle, event->phy_updated.status);
}
return 0;
}

default:
return 0;
}
Expand Down Expand Up @@ -717,3 +745,29 @@ esp_err_t bitchat_ble_send(uint16_t conn_handle, const uint8_t *data, size_t len
}
return ESP_OK;
}

void bitchat_ble_request_coded_phy(uint16_t conn_handle)
{
ble_conn_state_t *state = find_conn(conn_handle);
if (!state || state->coded_phy_state != BITLE_CODED_PHY_NOT_REQUESTED) {
return; /* no such link, or already requested/settled for it */
}
state->coded_phy_state = BITLE_CODED_PHY_PENDING;
/* BLE_GAP_LE_PHY_CODED_ANY leaves the coding scheme (S=2 vs S=8) to the
* Link Layer rather than asserting one we cannot enforce through this
* API — the host can only express a PHY preference, not dictate the
* coding scheme used for a given packet. An immediate non-zero rc here
* means our own controller declined the request outright (e.g. Coded
* PHY unsupported on this build); settle immediately rather than wait
* for an event that will never arrive. A successful rc only means the
* PHY Update procedure was kicked off — BLE_GAP_EVENT_PHY_UPDATE_COMPLETE
* (above) reports whether the peer actually accepted it. */
int rc = ble_gap_set_prefered_le_phy(conn_handle,
BLE_GAP_LE_PHY_CODED_MASK,
BLE_GAP_LE_PHY_CODED_MASK,
BLE_GAP_LE_PHY_CODED_ANY);
if (rc != 0) {
ESP_LOGD(TAG, "conn=%u coded PHY request declined locally rc=%d", conn_handle, rc);
state->coded_phy_state = BITLE_CODED_PHY_SETTLED;
}
}
12 changes: 12 additions & 0 deletions main/bitchat_ble.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ esp_err_t bitchat_ble_send(uint16_t conn_handle, const uint8_t *data, size_t len
bool bitchat_ble_conn_is_central(uint16_t conn_handle);
void bitchat_ble_disconnect(uint16_t conn_handle);

/* Opportunistically requests the Link-Layer PHY Update procedure toward LE
* Coded PHY on an already-established Bitle-to-Bitle connection. This is a
* reliability/margin improvement for a link that already exists — it does
* NOT extend how far apart two nodes can first discover and connect to each
* other (that still happens on legacy 1M PHY, which phone discovery also
* requires; see start_advertising()). The request is a preference, not a
* guarantee: either side's controller may decline it, in which case the
* connection simply continues on whatever PHY it already had. Safe to call
* repeatedly; internally de-duplicated per connection so it is attempted at
* most once per link. Never call this for a phone connection. */
void bitchat_ble_request_coded_phy(uint16_t conn_handle);

#ifdef __cplusplus
}
#endif
Expand Down
10 changes: 10 additions & 0 deletions main/noise_handshake.c
Original file line number Diff line number Diff line change
Expand Up @@ -1470,6 +1470,16 @@ static void process_announce_event(const noise_event_t *evt)
if (is_direct) {
bitchat_time_consider_peer_announce(evt->timestamp_ms,
ident.is_infra, ident.is_authoritative);
/* Opportunistic link-quality upgrade for confirmed Bitle-to-
* Bitle links only: request LE Coded PHY once an authenticated
* announce (not a bare BLE heuristic) shows the direct peer on
* this link is another relay, not a phone. Phone links never
* carry is_infra, so this never touches them. The LoRa trunk
* handle has no BLE PHY concept (its "PHY" is the SX1262's own
* spreading factor), so it is explicitly excluded. */
if (ident.is_infra && !bitle_link_is_broadcast(conn_handle)) {
bitchat_ble_request_coded_phy(conn_handle);
}
}
}
if (is_direct) {
Expand Down
6 changes: 6 additions & 0 deletions sdkconfig.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=30
CONFIG_BT_NIMBLE_TRANSPORT_EVT_DISCARD_COUNT=8
CONFIG_BT_NIMBLE_USE_ESP_TIMER=y
CONFIG_BT_NIMBLE_CRYPTO_STACK_MBEDTLS=y
# Both supported targets (esp32c3, esp32s3) support LE Coded PHY at the
# controller level; enables ble_gap_set_prefered_le_phy and the
# BLE_GAP_EVENT_PHY_UPDATE_COMPLETE path used for Bitle-to-Bitle link
# quality upgrades (see bitchat_ble_request_coded_phy). Never applied to
# phone connections or advertising.
CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=y