@startuml Bluetooth Thread Model box "nimble_host task\n(NimBLE internal, 4 KB stack)" #LightBlue participant "gap_event_handler\n(esp32_ble.cpp)" as GAP participant "ble_gatt\ncallbacks" as GATT participant "bt_event_bridge\n(Bluetooth.cpp)" as Bridge participant "hidHostGapCb\n& discovery\n(BluetoothHidHost.cpp)" as HID end box box "main_dispatcher task\n(Tactility main task)" #LightGreen participant "settings I/O\n& peer updates" as Dispatch end box box "App tasks" #LightYellow participant "BtManage\nBtPeerSettings" as Apps end box box "esp_timer task" #LightGray participant "advRestart\nmidiKeepalive\nhidEncRetry" as Timers end box box "LVGL task\n(GuiService)" #MistyRose participant "hidHostKeyboard\nReadCb / mouseReadCb" as LVGL end box GAP -> Bridge : ble_publish_event() →\nBtEventCallback (bt_event_bridge) Bridge -> Dispatch : getMainDispatcher().dispatch()\n(settings::load/save, autoConnect) Dispatch -> Apps : publishEventCpp → PubSub callbacks GATT -> GAP : GATT access within\nnimble_host task HID -> Dispatch : getMainDispatcher().dispatch()\n(indev cleanup, ProfileStateChanged) Timers -> GAP : advRestartCallback\n(calls ble_start_advertising) LVGL -> HID : lv_indev read callbacks\n(LVGL tick) note over GAP, GATT NO file I/O on NimBLE host task — stringstream blows the 4 KB stack end note note over GAP, Bridge Driver fires BtEvent via callback array. Bridge (Tactility layer) translates to C++ PubSub and dispatches I/O. end note @enduml