diff --git a/pkg/service/nrf_commands.go b/pkg/service/nrf_commands.go index 2fb3e54..31c3efb 100644 --- a/pkg/service/nrf_commands.go +++ b/pkg/service/nrf_commands.go @@ -7,10 +7,30 @@ import ( "github.com/librescoot/bluetooth-service/pkg/ble" ) -// InitializeNRF52 initializes communication with the nRF52 +// nrfHandshakeTimeout caps how long InitializeNRF52 waits for the nRF to +// reply to the version request. The reply normally arrives in a few ms over +// USOCK; 2 seconds is generous enough to ride out worst-case scheduling +// latency and tight enough to surface a non-responsive nRF immediately +// instead of leaving the service silently degraded. +const nrfHandshakeTimeout = 2 * time.Second + +// InitializeNRF52 initializes communication with the nRF52 and blocks until +// the nRF acknowledges by emitting a BLE version frame, or until +// nrfHandshakeTimeout elapses. A timeout is returned as an error so callers +// (Bootstrap.startNormally, ReconnectUSock) can surface FaultNRFInit or +// failed:reconnect as appropriate. Late-arriving versions clear the fault +// via handleBLEVersionMessage, so a slow handshake self-heals. func (s *Service) InitializeNRF52() error { s.log.Infof("Starting nRF52 initialization...") + // Drain any stale version signal from a previous handshake. The buffered + // channel may still hold an entry if the version handler fired between + // the previous wait and now (e.g. on rapid reconnects). + select { + case <-s.versionRxCh: + default: + } + // 1. Disable data streaming if err := writeUARTMessage(s.usock, ble.TypeDataStream, ble.TypeDataStreamEnable, 0); err != nil { s.log.Warnf(" failed to disable data streaming: %v", err) @@ -58,8 +78,19 @@ func (s *Service) InitializeNRF52() error { s.log.Debugf("Sent command to restart advertising without whitelist") } - s.log.Infof("nRF52 initialization complete") - return nil + // Block until the version reply arrives or we time out. The version + // handler does a non-blocking send on versionRxCh on receipt; a reply + // that arrived during the steps above is sitting in the buffered + // channel and is consumed immediately here. + select { + case <-s.versionRxCh: + s.log.Infof("nRF52 initialization complete") + return nil + case <-time.After(nrfHandshakeTimeout): + return fmt.Errorf("handshake timeout: no version response within %v", nrfHandshakeTimeout) + case <-s.stopCh: + return fmt.Errorf("handshake aborted: service stopping") + } } // ShutdownNRF52 tells the nRF to stop its autonomous data stream before diff --git a/pkg/service/service.go b/pkg/service/service.go index 1c1cd11..077ccd4 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -70,6 +70,13 @@ type Service struct { // recovers. schemaMu sync.RWMutex schemaCache map[string]settingSchema + + // Signaled by handleBLEVersionMessage whenever a BLE version frame + // arrives from the nRF. InitializeNRF52 drains this channel before + // sending the version request, then waits on it to confirm the + // handshake completed before returning. Buffered (size 1) so the + // version handler's send is non-blocking. + versionRxCh chan struct{} } // Fault codes for bluetooth service @@ -82,10 +89,11 @@ const ( // New creates a new Service instance func New(ipcClient *ipc.Client, log *logger.Logger) *Service { return &Service{ - ipc: ipcClient, - log: log, - stopCh: make(chan struct{}), - faults: ipcClient.NewFaultSet("ble:fault", "ble", "fault"), + ipc: ipcClient, + log: log, + stopCh: make(chan struct{}), + faults: ipcClient.NewFaultSet("ble:fault", "ble", "fault"), + versionRxCh: make(chan struct{}, 1), } } diff --git a/pkg/service/usock_handlers.go b/pkg/service/usock_handlers.go index efef45c..60a8fd5 100644 --- a/pkg/service/usock_handlers.go +++ b/pkg/service/usock_handlers.go @@ -380,6 +380,15 @@ func (s *Service) handleBLEVersionMessage(msgType ble.MessageType, absSubTypeKey if versionStr, ok := convertToString(value); ok { s.log.Infof("Detected firmware version: %s", versionStr) + // Signal InitializeNRF52's handshake wait; clear FaultNRFInit + // so a late-arriving version after a handshake-timeout self-heals + // the fault state. + select { + case s.versionRxCh <- struct{}{}: + default: + } + s.ClearFault(FaultNRFInit) + // Write version to Redis immediately (before compatibility check) if err := s.ipc.Hash(KeyFirmwareVersion).Set("nrf-fw-version", versionStr); err != nil { s.log.Errorf("Failed to update BLE version in Redis: %v", err)