import 'package:flutter/foundation.dart'; import '../connector/meshcore_protocol.dart'; class BleDebugLogEntry { final DateTime timestamp; final bool outgoing; final String description; final Uint8List payload; BleDebugLogEntry({ required this.timestamp, required this.outgoing, required this.description, required this.payload, }); String get hexPreview { const maxBytes = 64; final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload; final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); return payload.length > maxBytes ? '$hex …' : hex; } } class BleRawLogRxEntry { final DateTime timestamp; final Uint8List payload; BleRawLogRxEntry({required this.timestamp, required this.payload}); String get hexPreview { const maxBytes = 64; final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload; final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' '); return payload.length > maxBytes ? '$hex …' : hex; } } class BleDebugLogService extends ChangeNotifier { static const int maxEntries = 500; final List _entries = []; final List _rawLogRxEntries = []; List get entries => List.unmodifiable(_entries); List get rawLogRxEntries => List.unmodifiable(_rawLogRxEntries); void logFrame(Uint8List frame, {required bool outgoing, String? note}) { if (frame.isEmpty) return; final code = frame[0]; final description = _describeFrame(code, frame, outgoing, note); _entries.add( BleDebugLogEntry( timestamp: DateTime.now(), outgoing: outgoing, description: description, payload: Uint8List.fromList(frame), ), ); if (_entries.length > maxEntries) { _entries.removeRange(0, _entries.length - maxEntries); } if (!outgoing && code == pushCodeLogRxData && frame.length > 3) { _rawLogRxEntries.add( BleRawLogRxEntry( timestamp: DateTime.now(), payload: Uint8List.fromList(frame.sublist(3)), ), ); if (_rawLogRxEntries.length > maxEntries) { _rawLogRxEntries.removeRange(0, _rawLogRxEntries.length - maxEntries); } } notifyListeners(); } void clear() { _entries.clear(); _rawLogRxEntries.clear(); notifyListeners(); } String _describeFrame( int code, Uint8List frame, bool outgoing, String? note, ) { final label = _codeLabel(code, outgoing: outgoing); final prefix = outgoing ? 'TX' : 'RX'; final extra = _frameDetail(code, frame); final noteText = note != null ? ' • $note' : ''; return '$prefix $label$extra$noteText'; } String _codeLabel(int code, {required bool outgoing}) { if (outgoing) { return _commandLabel(code) ?? 'CODE_$code'; } final pushLabel = _pushLabel(code); if (pushLabel != null) return pushLabel; final responseLabel = _responseLabel(code); if (responseLabel != null) return responseLabel; return 'CODE_$code'; } String? _commandLabel(int code) { switch (code) { case cmdAppStart: return 'CMD_APP_START'; case cmdSendTxtMsg: return 'CMD_SEND_TXT_MSG'; case cmdSendChannelTxtMsg: return 'CMD_SEND_CHANNEL_TXT_MSG'; case cmdGetContacts: return 'CMD_GET_CONTACTS'; case cmdGetDeviceTime: return 'CMD_GET_DEVICE_TIME'; case cmdSetDeviceTime: return 'CMD_SET_DEVICE_TIME'; case cmdSendSelfAdvert: return 'CMD_SEND_SELF_ADVERT'; case cmdSetAdvertName: return 'CMD_SET_ADVERT_NAME'; case cmdAddUpdateContact: return 'CMD_ADD_UPDATE_CONTACT'; case cmdSyncNextMessage: return 'CMD_SYNC_NEXT_MESSAGE'; case cmdSetRadioParams: return 'CMD_SET_RADIO_PARAMS'; case cmdSetRadioTxPower: return 'CMD_SET_RADIO_TX_POWER'; case cmdResetPath: return 'CMD_RESET_PATH'; case cmdRemoveContact: return 'CMD_REMOVE_CONTACT'; case cmdReboot: return 'CMD_REBOOT'; case cmdGetBattAndStorage: return 'CMD_GET_BATT_AND_STORAGE'; case cmdSendLogin: return 'CMD_SEND_LOGIN'; case cmdGetChannel: return 'CMD_GET_CHANNEL'; case cmdSetChannel: return 'CMD_SET_CHANNEL'; case cmdGetRadioSettings: return 'CMD_GET_RADIO_SETTINGS'; case cmdSetCustomVar: return 'CMD_SET_CUSTOM_VAR'; case cmdSendTracePath: return 'CMD_SEND_TRACE_PATH'; default: return null; } } String? _responseLabel(int code) { switch (code) { case respCodeOk: return 'RESP_CODE_OK'; case respCodeErr: return 'RESP_CODE_ERR'; case respCodeContactsStart: return 'RESP_CODE_CONTACTS_START'; case respCodeContact: return 'RESP_CODE_CONTACT'; case respCodeEndOfContacts: return 'RESP_CODE_END_OF_CONTACTS'; case respCodeSelfInfo: return 'RESP_CODE_SELF_INFO'; case respCodeSent: return 'RESP_CODE_SENT'; case respCodeContactMsgRecv: return 'RESP_CODE_CONTACT_MSG_RECV'; case respCodeChannelMsgRecv: return 'RESP_CODE_CHANNEL_MSG_RECV'; case respCodeCurrTime: return 'RESP_CODE_CURR_TIME'; case respCodeNoMoreMessages: return 'RESP_CODE_NO_MORE_MESSAGES'; case respCodeBattAndStorage: return 'RESP_CODE_BATT_AND_STORAGE'; case respCodeContactMsgRecvV3: return 'RESP_CODE_CONTACT_MSG_RECV_V3'; case respCodeChannelMsgRecvV3: return 'RESP_CODE_CHANNEL_MSG_RECV_V3'; case respCodeChannelInfo: return 'RESP_CODE_CHANNEL_INFO'; case respCodeRadioSettings: return 'RESP_CODE_RADIO_SETTINGS'; case pushCodeTraceData: return 'PUSH_CODE_TRACE_DATA'; default: return null; } } String? _pushLabel(int code) { switch (code) { case pushCodeAdvert: return 'PUSH_CODE_ADVERT'; case pushCodePathUpdated: return 'PUSH_CODE_PATH_UPDATED'; case pushCodeSendConfirmed: return 'PUSH_CODE_SEND_CONFIRMED'; case pushCodeMsgWaiting: return 'PUSH_CODE_MSG_WAITING'; case pushCodeLoginSuccess: return 'PUSH_CODE_LOGIN_SUCCESS'; case pushCodeLoginFail: return 'PUSH_CODE_LOGIN_FAIL'; case pushCodeLogRxData: return 'PUSH_CODE_LOG_RX_DATA'; case pushCodeNewAdvert: return 'PUSH_CODE_NEW_ADVERT'; default: return null; } } String _frameDetail(int code, Uint8List frame) { switch (code) { case respCodeSent: if (frame.length >= 10) { final timeoutMs = readUint32LE(frame, 6); return ' • timeout=${timeoutMs}ms'; } return ''; case pushCodeSendConfirmed: if (frame.length >= 9) { final tripMs = readUint32LE(frame, 5); return ' • trip=${tripMs}ms'; } return ''; case pushCodeLoginSuccess: return ' • login ok'; case pushCodeLoginFail: return ' • login fail'; case respCodeBattAndStorage: if (frame.length >= 3) { final mv = readUint16LE(frame, 1); return ' • ${mv}mV'; } return ''; default: return ''; } } }