mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-07-17 00:52:01 +00:00
Fix Meshtastic BLE disconnect recovery
This commit is contained in:
@@ -490,6 +490,7 @@ Required tags:
|
||||
| `want_config` | Android wrote `ToRadio.want_config_id`; this starts/restarts PhoneAPI config snapshot. |
|
||||
| `heartbeat` | Android wrote `ToRadio.heartbeat`; firmware must enqueue a non-empty `FromRadio.queueStatus` for liveness when phase allows it. |
|
||||
| `phase_change` | PhoneAPI phase changed while handling a `ToRadio` write. |
|
||||
| `phone_disconnect` | Android wrote `ToRadio.disconnect`; firmware must request a real GAP/GATT disconnect, not only reset PhoneAPI state. |
|
||||
| `periodic` | Low-rate snapshot while connected, used to diagnose stale app UI state when no edge event occurs. |
|
||||
| `link_down` | GAP disconnected; PhoneAPI session and unread published slots must be closed/reset. |
|
||||
|
||||
@@ -499,6 +500,13 @@ Important distinction:
|
||||
- If app UI says offline while `FromRadio` messages still drain, the suspect boundary is app-level connection projection or config freshness, not immediate packet transport.
|
||||
- Do not fix this symptom by forcing a fake online state or by emitting out-of-phase config frames. The proper fix is to align transport session lifecycle, `want_config` handling, heartbeat response, and `FromRadio` drain ordering with official firmware.
|
||||
|
||||
Transport lifecycle rules:
|
||||
|
||||
- `ToRadio.disconnect` is a phone transport lifecycle command. Handling it is not complete until the platform BLE runtime requests a real link disconnect.
|
||||
- `PhoneCore` may reset PhoneAPI state, but it must delegate physical disconnect to the transport/runtime owner.
|
||||
- Platform runtimes must route the resulting GAP/GATT disconnect through the same `link_down` cleanup path used by remote disconnects: close PhoneAPI session, release published `FromRadio` slots, clear pending reads/notifies, clear pairing UI state, then restart advertising as appropriate.
|
||||
- nRF runtimes must also defend against half-open sessions: if GAP still reports connected but no phone-side liveness traffic is observed for the stale-session window, the runtime should proactively disconnect the old link so Android can perform a fresh connection handshake.
|
||||
|
||||
### MQTT Proxy Reliability Window
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -70,6 +70,7 @@ class MeshtasticPhoneTransport
|
||||
virtual ~MeshtasticPhoneTransport() = default;
|
||||
virtual bool isBleConnected() const = 0;
|
||||
virtual void notifyFromNum(uint32_t from_num) = 0;
|
||||
virtual void requestPhoneDisconnect() {}
|
||||
};
|
||||
|
||||
class MeshtasticPhoneBluetoothConfigHooks
|
||||
|
||||
@@ -1113,6 +1113,7 @@ bool MeshtasticPhoneCore::handleToRadio(const uint8_t* data, size_t len)
|
||||
return true;
|
||||
case meshtastic_ToRadio_disconnect_tag:
|
||||
reset();
|
||||
transport_.requestPhoneDisconnect();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -36,10 +36,12 @@ class FakeMeshtasticTransport final : public phone::meshtastic::MeshtasticPhoneT
|
||||
last_from_num = from_num;
|
||||
notify_count++;
|
||||
}
|
||||
void requestPhoneDisconnect() override { disconnect_count++; }
|
||||
|
||||
bool connected = true;
|
||||
uint32_t last_from_num = 0;
|
||||
int notify_count = 0;
|
||||
int disconnect_count = 0;
|
||||
};
|
||||
|
||||
class FakeBluetoothConfigHooks final : public phone::meshtastic::MeshtasticPhoneBluetoothConfigHooks
|
||||
@@ -360,6 +362,20 @@ bool encodeHeartbeatToRadio(uint8_t* out, size_t out_len, size_t& written, uint3
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeDisconnectToRadio(uint8_t* out, size_t out_len, size_t& written)
|
||||
{
|
||||
meshtastic_ToRadio to_radio = meshtastic_ToRadio_init_zero;
|
||||
to_radio.which_payload_variant = meshtastic_ToRadio_disconnect_tag;
|
||||
|
||||
pb_ostream_t to_radio_stream = pb_ostream_from_buffer(out, out_len);
|
||||
if (!pb_encode(&to_radio_stream, meshtastic_ToRadio_fields, &to_radio))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
written = to_radio_stream.bytes_written;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool encodeMqttProxyToRadio(const meshtastic_MqttClientProxyMessage& msg,
|
||||
uint8_t* out,
|
||||
size_t out_len,
|
||||
@@ -538,6 +554,21 @@ int main()
|
||||
assert(heartbeat_from.queueStatus.res == 0);
|
||||
assert(!heartbeat_session.popToPhone(&heartbeat_frame));
|
||||
|
||||
phone::tests::FakePhoneRuntimeContext disconnect_runtime;
|
||||
FakeMeshtasticTransport disconnect_transport;
|
||||
phone::meshtastic::MeshtasticPhoneSession disconnect_session(
|
||||
disconnect_runtime, disconnect_transport, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
|
||||
enterMeshtasticSendPackets(disconnect_session);
|
||||
assert(disconnect_session.isSendingPackets());
|
||||
uint8_t disconnect_to_radio[meshtastic_ToRadio_size] = {};
|
||||
size_t disconnect_to_radio_len = 0;
|
||||
assert(encodeDisconnectToRadio(disconnect_to_radio,
|
||||
sizeof(disconnect_to_radio),
|
||||
disconnect_to_radio_len));
|
||||
assert(disconnect_session.handleToRadio(disconnect_to_radio, disconnect_to_radio_len));
|
||||
assert(disconnect_transport.disconnect_count == 1);
|
||||
assert(!disconnect_session.isSendingPackets());
|
||||
|
||||
phone::tests::FakePhoneRuntimeContext backpressure_runtime;
|
||||
FakeMeshtasticTransport backpressure_transport;
|
||||
phone::meshtastic::MeshtasticPhoneSession backpressure_session(
|
||||
|
||||
@@ -53,6 +53,7 @@ class MeshtasticBleService : public BleService,
|
||||
uint32_t pendingPhoneBlePasskey() const override;
|
||||
void requestPhoneHighThroughputConnection() override;
|
||||
void requestPhoneLowerPowerConnection() override;
|
||||
void requestPhoneDisconnect() override;
|
||||
void onPhoneBluetoothConfigChanged() override;
|
||||
void onPhoneModuleConfigChanged() override;
|
||||
|
||||
|
||||
@@ -47,6 +47,21 @@ void MeshtasticBleService::requestPhoneLowerPowerConnection()
|
||||
requestLowerPowerConnection();
|
||||
}
|
||||
|
||||
void MeshtasticBleService::requestPhoneDisconnect()
|
||||
{
|
||||
if (!server_ || !connected_ || !conn_handle_valid_)
|
||||
{
|
||||
Serial.printf("[BLE][mt] phone disconnect ignored connected=%u handle_valid=%u\n",
|
||||
connected_ ? 1U : 0U,
|
||||
conn_handle_valid_ ? 1U : 0U);
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[BLE][mt] phone disconnect request handle=%u\n",
|
||||
static_cast<unsigned>(conn_handle_));
|
||||
server_->disconnect(conn_handle_);
|
||||
}
|
||||
|
||||
void MeshtasticBleService::onPhoneBluetoothConfigChanged()
|
||||
{
|
||||
saveBleConfig();
|
||||
|
||||
@@ -61,6 +61,7 @@ class MeshtasticBleService final : public BleService,
|
||||
uint32_t pendingPhoneBlePasskey() const override;
|
||||
void requestPhoneHighThroughputConnection() override;
|
||||
void requestPhoneLowerPowerConnection() override;
|
||||
void requestPhoneDisconnect() override;
|
||||
void onPhoneBluetoothConfigChanged() override;
|
||||
void onPhoneModuleConfigChanged() override;
|
||||
|
||||
@@ -90,6 +91,8 @@ class MeshtasticBleService final : public BleService,
|
||||
void markConfigSavePending(bool bluetooth_changed, bool module_changed);
|
||||
void flushPendingConfigSaves(bool force = false);
|
||||
void applyBleSecurity();
|
||||
bool processPendingPhoneDisconnect();
|
||||
void checkPhoneSessionLiveness(uint32_t now_ms);
|
||||
void logFromRadioState(const char* tag) const;
|
||||
void logSessionState(const char* tag, uint32_t detail = 0);
|
||||
void requestPairingIfNeeded(uint16_t conn_handle);
|
||||
@@ -136,6 +139,7 @@ class MeshtasticBleService final : public BleService,
|
||||
|
||||
volatile bool pairing_request_pending_ = false;
|
||||
volatile uint16_t pending_pairing_conn_handle_ = BLE_CONN_HANDLE_INVALID;
|
||||
volatile bool pending_phone_disconnect_request_ = false;
|
||||
|
||||
volatile bool pending_connect_log_ = false;
|
||||
volatile bool pending_disconnect_log_ = false;
|
||||
@@ -181,6 +185,7 @@ class MeshtasticBleService final : public BleService,
|
||||
uint32_t last_from_num_notify_ms_ = 0;
|
||||
uint32_t next_connected_session_log_ms_ = 0;
|
||||
uint32_t next_ble_idle_log_ms_ = 0;
|
||||
uint32_t next_liveness_log_ms_ = 0;
|
||||
ble_gap_addr_t remembered_phone_peer_ = {};
|
||||
bool remembered_phone_peer_valid_ = false;
|
||||
bool remembered_phone_peer_bonded_ = false;
|
||||
|
||||
@@ -31,6 +31,8 @@ constexpr uint32_t kDirectedAdvertisingMs = 3000UL;
|
||||
constexpr uint16_t kBleAdvertisingFastInterval = 32;
|
||||
constexpr uint16_t kBleAdvertisingSlowInterval = 668;
|
||||
constexpr uint16_t kBleAdvertisingFastTimeoutSec = 30;
|
||||
constexpr uint32_t kPhoneSessionStaleMs = 90000UL;
|
||||
constexpr uint32_t kPhoneSessionStaleLogIntervalMs = 30000UL;
|
||||
constexpr uint8_t kFromRadioEmptyInactive = 1;
|
||||
constexpr uint8_t kFromRadioEmptyNoFrame = 2;
|
||||
constexpr uint8_t kFromRadioEmptyInvalidFrame = 3;
|
||||
@@ -771,6 +773,7 @@ void MeshtasticBleService::start()
|
||||
pending_disconnect_reason_ = 0;
|
||||
pairing_request_pending_ = false;
|
||||
pending_pairing_conn_handle_ = BLE_CONN_HANDLE_INVALID;
|
||||
pending_phone_disconnect_request_ = false;
|
||||
directed_advertising_active_ = false;
|
||||
directed_advertising_attempted_ = false;
|
||||
directed_advertising_until_ms_ = 0;
|
||||
@@ -786,6 +789,7 @@ void MeshtasticBleService::start()
|
||||
last_from_num_notify_ms_ = 0;
|
||||
next_connected_session_log_ms_ = 0;
|
||||
next_ble_idle_log_ms_ = last_ble_activity_ms_ + 3000UL;
|
||||
next_liveness_log_ms_ = 0;
|
||||
|
||||
prepareBluefruit(device_name_);
|
||||
loadRememberedPhonePeer();
|
||||
@@ -903,6 +907,7 @@ void MeshtasticBleService::stop()
|
||||
pending_disconnect_reason_ = 0;
|
||||
pairing_request_pending_ = false;
|
||||
pending_pairing_conn_handle_ = BLE_CONN_HANDLE_INVALID;
|
||||
pending_phone_disconnect_request_ = false;
|
||||
directed_advertising_active_ = false;
|
||||
directed_advertising_attempted_ = false;
|
||||
directed_advertising_until_ms_ = 0;
|
||||
@@ -916,6 +921,7 @@ void MeshtasticBleService::stop()
|
||||
last_from_num_notify_ms_ = 0;
|
||||
next_connected_session_log_ms_ = 0;
|
||||
next_ble_idle_log_ms_ = 0;
|
||||
next_liveness_log_ms_ = 0;
|
||||
if (s_active_service == this)
|
||||
{
|
||||
s_active_service = nullptr;
|
||||
@@ -938,6 +944,10 @@ void MeshtasticBleService::update()
|
||||
|
||||
processPendingPairingRequest();
|
||||
processPendingToRadio();
|
||||
if (processPendingPhoneDisconnect())
|
||||
{
|
||||
return;
|
||||
}
|
||||
flushPendingFromRadioReadAuthorize();
|
||||
flushPendingFromNumNotify();
|
||||
logDeferredBleEvents();
|
||||
@@ -993,6 +1003,7 @@ void MeshtasticBleService::update()
|
||||
logSessionState("periodic");
|
||||
next_connected_session_log_ms_ = now_ms + kBleConnectedSessionLogIntervalMs;
|
||||
}
|
||||
checkPhoneSessionLiveness(now_ms);
|
||||
}
|
||||
|
||||
void MeshtasticBleService::handleIncomingTextFromApp(const chat::MeshIncomingText& msg)
|
||||
@@ -1605,7 +1616,9 @@ void MeshtasticBleService::handleConnectEvent(uint16_t conn_handle)
|
||||
last_from_radio_read_ms_ = 0;
|
||||
last_from_num_notify_ms_ = 0;
|
||||
next_connected_session_log_ms_ = last_ble_activity_ms_ + kBleConnectedSessionLogIntervalMs;
|
||||
next_liveness_log_ms_ = 0;
|
||||
from_num_notify_enabled_ = false;
|
||||
pending_phone_disconnect_request_ = false;
|
||||
if (phone_session_)
|
||||
{
|
||||
phone_session_->close();
|
||||
@@ -1666,6 +1679,8 @@ void MeshtasticBleService::handleDisconnectEvent(uint16_t conn_handle, uint8_t r
|
||||
pending_disconnect_conn_handle_ = conn_handle;
|
||||
pending_disconnect_reason_ = reason;
|
||||
pending_disconnect_log_ = true;
|
||||
pending_phone_disconnect_request_ = false;
|
||||
next_liveness_log_ms_ = 0;
|
||||
directed_advertising_active_ = false;
|
||||
directed_advertising_attempted_ = false;
|
||||
directed_advertising_until_ms_ = 0;
|
||||
@@ -1779,6 +1794,100 @@ void MeshtasticBleService::requestPhoneLowerPowerConnection()
|
||||
{
|
||||
}
|
||||
|
||||
void MeshtasticBleService::requestPhoneDisconnect()
|
||||
{
|
||||
pending_phone_disconnect_request_ = true;
|
||||
}
|
||||
|
||||
bool MeshtasticBleService::processPendingPhoneDisconnect()
|
||||
{
|
||||
if (!pending_phone_disconnect_request_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pending_phone_disconnect_request_ = false;
|
||||
const uint16_t handle = conn_handle_;
|
||||
const bool handle_valid = handle != BLE_CONN_HANDLE_INVALID;
|
||||
const bool gap_connected = handle_valid && Bluefruit.connected(handle);
|
||||
bleLogBoth("[BLE][nrf52][mt][flow] phone disconnect request connected=%u gap=%u handle=%u",
|
||||
connected_ ? 1U : 0U,
|
||||
gap_connected ? 1U : 0U,
|
||||
handle_valid ? static_cast<unsigned>(handle) : 0xFFFFU);
|
||||
|
||||
clearPendingFromRadioReadAuthorize();
|
||||
pending_to_radio_head_ = 0;
|
||||
pending_to_radio_tail_ = 0;
|
||||
pending_to_radio_count_ = 0;
|
||||
clearToPhoneQueue();
|
||||
from_num_notify_enabled_ = false;
|
||||
pending_passkey_.store(0);
|
||||
|
||||
if (!connected_ || !gap_connected)
|
||||
{
|
||||
if (!Bluefruit.Advertising.isRunning())
|
||||
{
|
||||
startPhoneAdvertising(!directed_advertising_attempted_);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool ok = Bluefruit.disconnect(handle);
|
||||
bleLogBoth("[BLE][nrf52][mt][flow] phone disconnect gap_request handle=%u ok=%u",
|
||||
static_cast<unsigned>(handle),
|
||||
ok ? 1U : 0U);
|
||||
if (!ok)
|
||||
{
|
||||
pending_phone_disconnect_request_ = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MeshtasticBleService::checkPhoneSessionLiveness(uint32_t now_ms)
|
||||
{
|
||||
if (!active_ || !connected_ || conn_handle_ == BLE_CONN_HANDLE_INVALID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t last_phone_activity_ms = last_ble_activity_ms_;
|
||||
if (last_heartbeat_ms_ > last_phone_activity_ms)
|
||||
{
|
||||
last_phone_activity_ms = last_heartbeat_ms_;
|
||||
}
|
||||
if (last_from_radio_read_ms_ > last_phone_activity_ms)
|
||||
{
|
||||
last_phone_activity_ms = last_from_radio_read_ms_;
|
||||
}
|
||||
if (last_to_radio_ms_ > last_phone_activity_ms)
|
||||
{
|
||||
last_phone_activity_ms = last_to_radio_ms_;
|
||||
}
|
||||
|
||||
if (last_phone_activity_ms == 0 ||
|
||||
static_cast<uint32_t>(now_ms - last_phone_activity_ms) < kPhoneSessionStaleMs)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (next_liveness_log_ms_ == 0 || static_cast<int32_t>(now_ms - next_liveness_log_ms_) >= 0)
|
||||
{
|
||||
bleLogBoth("[BLE][nrf52][mt][flow] phone session stale age_ms=%lu limit_ms=%lu connected=%u gap=%u "
|
||||
"notify=%u cfg=%u send=%u",
|
||||
static_cast<unsigned long>(now_ms - last_phone_activity_ms),
|
||||
static_cast<unsigned long>(kPhoneSessionStaleMs),
|
||||
connected_ ? 1U : 0U,
|
||||
Bluefruit.connected(conn_handle_) ? 1U : 0U,
|
||||
from_num_notify_enabled_ ? 1U : 0U,
|
||||
(phone_session_ && phone_session_->isConfigFlowActive()) ? 1U : 0U,
|
||||
(phone_session_ && phone_session_->isSendingPackets()) ? 1U : 0U);
|
||||
next_liveness_log_ms_ = now_ms + kPhoneSessionStaleLogIntervalMs;
|
||||
}
|
||||
|
||||
pending_phone_disconnect_request_ = true;
|
||||
(void)processPendingPhoneDisconnect();
|
||||
}
|
||||
|
||||
void MeshtasticBleService::onPhoneBluetoothConfigChanged()
|
||||
{
|
||||
markConfigSavePending(true, false);
|
||||
|
||||
Reference in New Issue
Block a user