Merge pull request #251 from zjs81/dev-discoverScreen

Contact discovery
This commit is contained in:
zjs81
2026-03-06 11:59:24 -07:00
committed by GitHub
43 changed files with 3084 additions and 105 deletions
+305 -44
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart' as crypto;
import 'package:meshcore_open/models/discovery_contact.dart';
import 'package:pointycastle/export.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
@@ -24,6 +25,7 @@ import '../storage/channel_message_store.dart';
import '../storage/channel_order_store.dart';
import '../storage/channel_settings_store.dart';
import '../storage/channel_store.dart';
import '../storage/contact_discovery_store.dart';
import '../storage/contact_settings_store.dart';
import '../storage/contact_store.dart';
import '../storage/message_store.dart';
@@ -111,6 +113,7 @@ class MeshCoreConnector extends ChangeNotifier {
final List<ScanResult> _scanResults = [];
final List<Contact> _contacts = [];
final List<DiscoveryContact> _discoveredContacts = [];
final List<Channel> _channels = [];
final Map<String, List<Message>> _conversations = {};
final Map<int, List<ChannelMessage>> _channelMessages = {};
@@ -155,6 +158,18 @@ class MeshCoreConnector extends ChangeNotifier {
bool _batteryRequested = false;
bool _awaitingSelfInfo = false;
bool _preserveContactsOnRefresh = false;
bool _autoAddUsers = false;
bool _autoAddRepeaters = false;
bool _autoAddRoomServers = false;
bool _autoAddSensors = false;
bool _overwriteOldest = false;
bool _manualAddContacts = false;
int _telemetryModeBase = 0;
int _telemetryModeLoc = 0;
int _telemetryModeEnv = 0;
int _advertLocPolicy = 0;
int _multiAcks = 0;
static const int _defaultMaxContacts = 32;
static const int _defaultMaxChannels = 8;
int _maxContacts = _defaultMaxContacts;
@@ -195,6 +210,7 @@ class MeshCoreConnector extends ChangeNotifier {
final ChannelSettingsStore _channelSettingsStore = ChannelSettingsStore();
final ContactSettingsStore _contactSettingsStore = ContactSettingsStore();
final ContactStore _contactStore = ContactStore();
final ContactDiscoveryStore _discoveryContactStore = ContactDiscoveryStore();
final ChannelStore _channelStore = ChannelStore();
final UnreadStore _unreadStore = UnreadStore();
List<Channel> _cachedChannels = [];
@@ -242,6 +258,10 @@ class MeshCoreConnector extends ChangeNotifier {
);
}
List<DiscoveryContact> get discoveredContacts {
return List.unmodifiable(_discoveredContacts);
}
List<Channel> get channels => List.unmodifiable(_channels);
bool get isConnected => _state == MeshCoreConnectionState.connected;
bool get isLoadingContacts => _isLoadingContacts;
@@ -258,12 +278,18 @@ class MeshCoreConnector extends ChangeNotifier {
int? get currentBwHz => _currentBwHz;
int? get currentSf => _currentSf;
int? get currentCr => _currentCr;
bool? get autoAddUsers => _autoAddUsers;
bool? get autoAddRepeaters => _autoAddRepeaters;
bool? get autoAddRoomServers => _autoAddRoomServers;
bool? get autoAddSensors => _autoAddSensors;
bool? get autoAddOverwriteOldest => _overwriteOldest;
bool? get clientRepeat => _clientRepeat;
int? get firmwareVerCode => _firmwareVerCode;
Map<String, String>? get currentCustomVars => _currentCustomVars;
int? get batteryMillivolts => _batteryMillivolts;
int get maxContacts => _maxContacts;
int get maxChannels => _maxChannels;
Set<String> get knownContactKeys => Set.unmodifiable(_knownContactKeys);
bool get isSyncingQueuedMessages => _isSyncingQueuedMessages;
bool get isSyncingChannels => _isSyncingChannels;
int get channelSyncProgress =>
@@ -602,6 +628,13 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
Future<void> loadDiscoveredContactCache() async {
final cached = await _discoveryContactStore.loadContacts();
_discoveredContacts
..clear()
..addAll(cached);
}
Future<void> loadChannelSettings({int? maxChannels}) async {
_channelSmazEnabled.clear();
final channelCount = maxChannels ?? _maxChannels;
@@ -848,6 +881,9 @@ class MeshCoreConnector extends ChangeNotifier {
// Fetch channels so we can track unread counts for incoming messages
unawaited(getChannels());
// Load discovered contacts from storage
unawaited(loadDiscoveredContactCache());
} catch (e) {
debugPrint("Connection error: $e");
await disconnect(manual: false);
@@ -968,6 +1004,7 @@ class MeshCoreConnector extends ChangeNotifier {
_deviceDisplayName = null;
_deviceId = null;
_contacts.clear();
_discoveredContacts.clear();
_conversations.clear();
_loadedConversationKeys.clear();
_selfPublicKey = null;
@@ -1058,8 +1095,9 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildDeviceQueryFrame());
await sendFrame(buildAppStartFrame());
await requestBatteryStatus(force: true);
await sendFrame(buildGetRadioSettingsFrame());
await sendFrame(buildGetCustomVarsFrame());
await sendFrame(buildGetAutoAddFlagsFrame());
_scheduleSelfInfoRetry();
}
@@ -1070,7 +1108,7 @@ class MeshCoreConnector extends ChangeNotifier {
await sendFrame(buildAppStartFrame());
await sendFrame(buildGetCustomVarsFrame());
await requestBatteryStatus();
await sendFrame(buildGetAutoAddFlagsFrame());
_scheduleSelfInfoRetry();
}
@@ -1485,6 +1523,8 @@ class MeshCoreConnector extends ChangeNotifier {
Future<void> removeContact(Contact contact) async {
if (!isConnected) return;
_handleDiscovery(contact, Uint8List(0), noNotify: true);
await sendFrame(buildRemoveContactFrame(contact.publicKey));
_contacts.removeWhere((c) => c.publicKeyHex == contact.publicKeyHex);
_knownContactKeys.remove(contact.publicKeyHex);
@@ -1499,6 +1539,42 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
}
Future<void> removeDiscoveredContact(DiscoveryContact contact) async {
if (!isConnected) return;
_discoveredContacts.removeWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
unawaited(_persistDiscoveredContacts());
notifyListeners();
}
Future<void> importDiscoveredContact(DiscoveryContact contact) async {
if (!isConnected) return;
await sendFrame(
buildUpdateContactPathFrame(
contact.publicKey,
contact.path,
contact.pathLength,
type: contact.type,
flags: 0,
name: contact.name,
),
);
_handleContactAdvert(
Contact(
publicKey: contact.publicKey,
name: contact.name,
type: contact.type,
pathLength: contact.pathLength,
path: contact.path,
lastSeen: DateTime.now(),
),
);
notifyListeners();
}
Future<void> clearContactPath(Contact contact) async {
if (!isConnected) return;
@@ -1899,8 +1975,9 @@ class MeshCoreConnector extends ChangeNotifier {
case respCodeChannelInfo:
_handleChannelInfo(frame);
break;
case respCodeRadioSettings:
_handleRadioSettings(frame);
case respCodeAutoAddConfig:
_handleAutoAddConfig(frame);
_checkManualAddContacts();
break;
case respCodeBattAndStorage:
_handleBatteryAndStorage(frame);
@@ -1973,25 +2050,35 @@ class MeshCoreConnector extends ChangeNotifier {
// [56] = sf
// [57] = cr
// [58+] = node_name
if (frame.length < 4 + pubKeySize) return;
final reader = BufferReader(frame);
try {
reader.skipBytes(2);
_currentTxPower = reader.readByte();
_maxTxPower = reader.readByte();
_selfPublicKey = reader.readBytes(pubKeySize);
_selfLatitude = reader.readInt32LE() / 1000000.0;
_selfLongitude = reader.readInt32LE() / 1000000.0;
_multiAcks = reader.readByte();
_advertLocPolicy = reader.readByte();
_currentTxPower = frame[2];
_maxTxPower = frame[3];
_selfPublicKey = Uint8List.fromList(frame.sublist(4, 4 + pubKeySize));
_selfLatitude = readInt32LE(frame, 36) / 1000000.0;
_selfLongitude = readInt32LE(frame, 40) / 1000000.0;
final telemetryFlag = reader.readByte();
_telemetryModeBase = telemetryFlag & 0x03;
_telemetryModeEnv = telemetryFlag >> 2 & 0x03;
_telemetryModeLoc = telemetryFlag >> 4 & 0x03;
// Radio settings (if frame is long enough)
if (frame.length >= 58) {
_currentFreqHz = readUint32LE(frame, 48);
_currentBwHz = readUint32LE(frame, 52);
_currentSf = frame[56];
_currentCr = frame[57];
}
_manualAddContacts = reader.readByte() & 0x01 == 0x00;
// Node name starts at offset 58 if frame is long enough
if (frame.length > 58) {
_selfName = readCString(frame, 58, frame.length - 58);
_currentFreqHz = reader.readUInt32LE();
_currentBwHz = reader.readUInt32LE();
_currentSf = reader.readByte();
_currentCr = reader.readByte();
_selfName = reader.readString();
} catch (e) {
_appDebugLogService?.error(
'Error parsing SELF_INFO frame: $e',
tag: 'Connector',
);
}
_awaitingSelfInfo = false;
_selfInfoRetryTimer?.cancel();
@@ -2052,25 +2139,6 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_requestNextQueuedMessage());
}
void _handleRadioSettings(Uint8List frame) {
// Frame format from C++:
// [0] = RESP_CODE_RADIO_SETTINGS
// [1-4] = freq (uint32 LE, in Hz)
// [5-8] = bw (uint32 LE, in Hz)
// [9] = sf
// [10] = cr
if (frame.length >= 11) {
_currentFreqHz = readUint32LE(frame, 1);
_currentBwHz = readUint32LE(frame, 5);
_currentSf = frame[9];
_currentCr = frame[10];
debugPrint(
'Radio settings: freq=$_currentFreqHz bw=$_currentBwHz sf=$_currentSf cr=$_currentCr',
);
notifyListeners();
}
}
void _handleBatteryAndStorage(Uint8List frame) {
// Frame format from C++:
// [0] = RESP_CODE_BATT_AND_STORAGE
@@ -2088,6 +2156,32 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
void _checkManualAddContacts() async {
// If manual add contacts is enabled, set auto add config and other params.
// and disable it after
if (_manualAddContacts) {
await sendFrame(
buildSetAutoAddConfigFrame(
autoAddChat: true,
autoAddRepeater: true,
autoAddRoomServer: true,
autoAddSensor: true,
overwriteOldest: _overwriteOldest,
),
);
await sendFrame(
buildSetOtherParamsFrame(
(_telemetryModeEnv << 4) |
(_telemetryModeLoc << 2) |
(_telemetryModeBase),
_advertLocPolicy,
_multiAcks,
),
);
_manualAddContacts = false;
}
}
/// Calculate timeout for a message based on radio settings and path length
/// Returns timeout in milliseconds, considering number of hops
int calculateTimeout({required int pathLength, int messageBytes = 100}) {
@@ -2271,6 +2365,10 @@ class MeshCoreConnector extends ChangeNotifier {
await _contactStore.saveContacts(_contacts);
}
Future<void> _persistDiscoveredContacts() async {
await _discoveryContactStore.saveContacts(_discoveredContacts);
}
int _latestContactLastmod() {
if (_contacts.isEmpty) return 0;
var latest = 0;
@@ -3705,22 +3803,99 @@ class MeshCoreConnector extends ChangeNotifier {
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
return;
}
final rawPacket = frame.sublist(3);
switch (payloadType) {
case payloadTypeADVERT:
_handlePayloadAdvertReceived(payload, pathBytes, routeType, snr);
_handlePayloadAdvertReceived(
rawPacket,
payload,
pathBytes,
routeType,
snr,
);
break;
default:
}
}
void importContact(Uint8List frame) {
final packet = BufferReader(frame);
int payloadType = 0;
Uint8List pathBytes = Uint8List(0);
try {
packet.skipBytes(1); // Skip frame type byte
packet.skipBytes(1); // Skip SNR byte
packet.skipBytes(1); // Skip RSSI byte
final header = packet.readByte();
payloadType = (header >> 2) & 0x0F;
//final payloadVer = (header >> 6) & 0x03;
final pathLen = packet.readByte();
pathBytes = packet.readBytes(pathLen);
} catch (e) {
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
return;
}
double latitude = 0.0;
double longitude = 0.0;
String name = '';
Uint8List publicKey = Uint8List(0);
int type = 0;
int timestamp = 0;
bool hasLocation = false;
bool hasName = false;
if (payloadType != payloadTypeADVERT) {
appLogger.warn('Unexpected payload type: $payloadType', tag: 'Connector');
return;
}
try {
publicKey = packet.readBytes(32);
timestamp = packet.readInt32LE();
//TODO add signature verification
packet.skipBytes(64); // Skip signature for now
final flags = packet.readByte();
type = flags & 0x0F;
hasLocation = (flags & 0x10) != 0;
// For future use:
//final hasFeature1 = (flags & 0x20) != 0;
//final hasFeature2 = (flags & 0x40) != 0;
hasName = (flags & 0x80) != 0;
if (hasLocation && packet.remaining >= 8) {
latitude = packet.readInt32LE() / 1e6;
longitude = packet.readInt32LE() / 1e6;
}
if (hasName && packet.remaining > 0) {
name = packet.readString();
}
} catch (e) {
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
return;
}
importDiscoveredContact(
DiscoveryContact(
rawPacket: frame,
publicKey: publicKey,
name: name,
type: type,
pathLength: pathBytes.length,
path: Uint8List.fromList(
pathBytes.reversed.toList(),
), // Store path in reverse for easier use in outgoing messages
latitude: latitude,
longitude: longitude,
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
),
);
}
void _handlePayloadAdvertReceived(
Uint8List frame,
Uint8List rawPacket,
Uint8List payload,
Uint8List path,
int routeType,
double snr,
) {
final advert = BufferReader(frame);
final advert = BufferReader(payload);
double latitude = 0.0;
double longitude = 0.0;
String name = '';
@@ -3758,6 +3933,7 @@ class MeshCoreConnector extends ChangeNotifier {
return;
}
//We ignore our own adverts
if (listEquals(publicKey, _selfPublicKey)) {
return;
}
@@ -3778,7 +3954,14 @@ class MeshCoreConnector extends ChangeNotifier {
longitude: longitude,
lastSeen: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
);
_handleContactAdvert(newContact);
if ((_autoAddUsers && type == advTypeChat) ||
(_autoAddRepeaters && type == advTypeRepeater) ||
(_autoAddRoomServers && type == advTypeRoom) ||
(_autoAddSensors && type == advTypeSensor)) {
_handleContactAdvert(newContact);
} else {
_handleDiscovery(newContact, rawPacket);
}
_updateDirectRepeater(newContact, snr, path);
return;
}
@@ -3866,6 +4049,84 @@ class MeshCoreConnector extends ChangeNotifier {
}
notifyListeners();
}
void _handleAutoAddConfig(Uint8List frame) {
final reader = BufferReader(frame);
try {
reader.skipBytes(1); // Skip the response code byte
final flags = reader.readByte();
_autoAddUsers = flags & autoAddChatFlag != 0;
_autoAddRepeaters = flags & autoAddRepeaterFlag != 0;
_autoAddRoomServers = flags & autoAddRoomServerFlag != 0;
_autoAddSensors = flags & autoAddSensorFlag != 0;
_overwriteOldest = flags & autoAddOverwriteOldestFlag != 0;
} catch (e) {
appLogger.error('Failed to parse auto-add config: $e', tag: 'Connector');
}
}
void _handleDiscovery(
Contact contact,
Uint8List rawPacket, {
bool noNotify = false,
}) {
appLogger.info('Discovered new contact: ${contact.name}', tag: 'Connector');
final existingIndex = _discoveredContacts.indexWhere(
(c) => c.publicKeyHex == contact.publicKeyHex,
);
// Update existing contact
if (existingIndex >= 0) {
_discoveredContacts[existingIndex] = _discoveredContacts[existingIndex]
.copyWith(
rawPacket: rawPacket,
name: contact.name,
type: contact.type,
pathLength: contact.pathLength,
path: contact.path,
latitude: contact.latitude,
longitude: contact.longitude,
lastSeen: contact.lastSeen,
);
notifyListeners();
unawaited(_persistDiscoveredContacts());
return;
}
final disContact = DiscoveryContact(
rawPacket: rawPacket,
publicKey: contact.publicKey,
name: contact.name,
type: contact.type,
pathLength: contact.pathLength,
path: contact.path,
latitude: contact.latitude,
longitude: contact.longitude,
lastSeen: contact.lastSeen,
);
_discoveredContacts.add(disContact);
unawaited(_persistDiscoveredContacts());
// Show notification for new contact (advertisement)
if (_appSettingsService != null && !noNotify) {
final settings = _appSettingsService!.settings;
if (settings.notificationsEnabled && settings.notifyOnNewAdvert) {
_notificationService.showAdvertNotification(
contactName: contact.name,
contactType: contact.typeLabel,
contactId: contact.publicKeyHex,
);
}
}
}
void removeAllDiscoveredContacts() {
_discoveredContacts.clear();
unawaited(_persistDiscoveredContacts());
notifyListeners();
}
}
const int _phRouteMask = 0x03;
+66 -31
View File
@@ -120,25 +120,27 @@ class BufferWriter {
}
void writeHex(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException(
'Invalid hex characters at position $i: $hexByte',
);
}
result.add(byte);
}
writeBytes(Uint8List.fromList(result));
writeBytes(hex2Uint8List(hex));
}
}
Uint8List hex2Uint8List(String hex) {
// Validate hex string length is even and not empty
if (hex.isEmpty || hex.length % 2 != 0) {
throw FormatException('Invalid hex string length: ${hex.length}');
}
List<int> result = [];
for (int i = 0; i < hex.length ~/ 2; i++) {
final hexByte = hex.substring(i * 2, i * 2 + 2);
final byte = int.tryParse(hexByte, radix: 16);
if (byte == null) {
throw FormatException('Invalid hex characters at position $i: $hexByte');
}
result.add(byte);
}
return Uint8List.fromList(result);
}
// Command codes (to device)
const int cmdAppStart = 1;
const int cmdSendTxtMsg = 2;
@@ -168,11 +170,13 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdGetRadioSettings = 57;
const int cmdSendAnonReq = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
// Text message types
const int txtTypePlain = 0;
@@ -206,8 +210,8 @@ const int respCodeDeviceInfo = 13;
const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
const int respCodeAutoAddConfig = 25;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -253,6 +257,18 @@ const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
const int payloadTypeRawCustom =
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
//auto-add flags
const int autoAddOverwriteOldestFlag =
1 << 0; // 0x01 - overwrite oldest non-favourite when full
const int autoAddChatFlag =
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
const int autoAddRepeaterFlag =
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
const int autoAddRoomServerFlag =
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
const int autoAddSensorFlag =
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
// Sizes
const int pubKeySize = 32;
const int maxPathSize = 64;
@@ -303,7 +319,7 @@ const int contactNameOffset = 100;
const int contactTimestampOffset = 132;
const int contactLatOffset = 136;
const int contactLonOffset = 140;
const int contactLastmodOffset = 144;
const int contactLastModOffset = 144;
const int contactFrameSize = 148;
// Message frame offsets
@@ -681,16 +697,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
return writer.toBytes();
}
// Build CMD_GET_RADIO_SETTINGS frame
Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]);
}
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
Uint8List buildGetAutoAddFlagsFrame() {
return Uint8List.fromList([cmdGetAutoAddConfig]);
}
// Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds
@@ -815,10 +830,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) {
// Build a import contact frame
// [cmd][contact_frame x98+]
Uint8List buildImportContactFrame(String contactFrame) {
Uint8List buildImportContactFrame(Uint8List contactFrame) {
final writer = BufferWriter();
writer.writeByte(cmdImportContact);
writer.writeHex(contactFrame);
writer.writeBytes(contactFrame);
return writer.toBytes();
}
@@ -832,20 +847,40 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
}
// Build CMD_SET_OTHER_PARAMS frame
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advertLocationPolicy][multiAcks]
// Format: [cmd][allowTelemetryFlags][advertLocationPolicy][multiAcks]
Uint8List buildSetOtherParamsFrame(
bool allowAutoAddContacts,
int allowTelemetryFlags,
int advertLocationPolicy,
int multiAcks,
) {
final writer = BufferWriter();
writer.writeByte(cmdSetOtherParams);
writer.writeByte(
allowAutoAddContacts ? 0x00 : 0x01,
); // Allow Auto Add Contacts
//Going forward the app will just set Auto Add Contacts to disabled, and use the filter flags
//Allow Auto Add Contacts use inverted logic (0x01 = disabled, 0x00 = enabled).
writer.writeByte(0x01);
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
writer.writeByte(advertLocationPolicy); // Advertisement Location Policy
writer.writeByte(multiAcks); // Multi Acknowledgements
return writer.toBytes();
}
// Build CMD_SET_AUTO_ADD_CONFIG frame
// Format: [cmd][flags]
Uint8List buildSetAutoAddConfigFrame({
required bool autoAddChat,
required bool autoAddRepeater,
required bool autoAddRoomServer,
required bool autoAddSensor,
required bool overwriteOldest,
}) {
final writer = BufferWriter();
writer.writeByte(cmdSetAutoAddConfig);
int flags = 0;
if (autoAddChat) flags |= autoAddChatFlag;
if (autoAddRepeater) flags |= autoAddRepeaterFlag;
if (autoAddRoomServer) flags |= autoAddRoomServerFlag;
if (autoAddSensor) flags |= autoAddSensorFlag;
if (overwriteOldest) flags |= autoAddOverwriteOldestFlag;
writer.writeByte(flags);
return writer.toBytes();
}