diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 1f30842..d30df1a 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -394,6 +394,14 @@ project(zephcore) string(TIMESTAMP ZEPHCORE_BUILD_DATE "%Y %b %d" UTC) add_definitions(-DFIRMWARE_BUILD_DATE="${ZEPHCORE_BUILD_DATE}") +# Single source of truth for the firmware version string (vMAJOR.MINOR.PATCH-zephyr). +# Injected globally like FIRMWARE_BUILD_DATE above, so every app TU sees the same +# value; the per-app `#ifndef FIRMWARE_VERSION` fallbacks only apply to builds that +# bypass this injection. NOTE: the BLE DIS value in boards/common/zephcore_common.conf +# (CONFIG_BT_DIS_FW_REV_STR) is Kconfig, not C, so it must be bumped here AND there. +set(ZEPHCORE_FIRMWARE_VERSION "v1.16.0-zephyr") +add_definitions(-DFIRMWARE_VERSION="${ZEPHCORE_FIRMWARE_VERSION}") + add_subdirectory(lib/monocypher) target_link_libraries(app PRIVATE monocypher) diff --git a/zephcore/Repeater_CLI_commands.md b/zephcore/Repeater_CLI_commands.md index e5bc7e7..0dbe7c2 100644 --- a/zephcore/Repeater_CLI_commands.md +++ b/zephcore/Repeater_CLI_commands.md @@ -187,6 +187,7 @@ All `set uplink.*` changes are saved immediately and only applied after reboot. | `get backoff.multiplier` | Per-dupe reactive backoff multiplier | | `get flood.max` | Max flood retransmit hops | | `get flood.max.unscoped` | Max retransmit hops for un-scoped floods | +| `get flood.max.advert` | Max retransmit hops for ADVERT floods | | `get flood.advert.interval` | Flood advertisement interval in hours | | `get advert.interval` | Local advertisement interval in minutes | | `get apc.margin` | Adaptive Power Control target RSSI margin in dB | @@ -229,6 +230,7 @@ Changes are persisted immediately unless noted. Some require a reboot. | `set backoff.multiplier ` | 0.0–2.0 | Per-dupe reactive backoff multiplier (0 = disable reactive backoff) | | `set flood.max ` | 0–64 | Maximum flood retransmit hops | | `set flood.max.unscoped ` | 0–64 | Hop limit for un-scoped floods only (default 64 = same as flood.max); scoped/transport floods still use flood.max | +| `set flood.max.advert ` | 0–64 | Hop limit for ADVERT floods only (default 8); curbs advert churn independent of flood.max | | `set flood.advert.interval ` | 3–168 | How often the repeater floods its own advertisement | | `set advert.interval ` | min–240 | How often the repeater sends local advertisements | | `set apc.margin ` | 6–30 | Target RSSI margin for Adaptive Power Control | diff --git a/zephcore/adapters/datastore/ZephyrDataStore.cpp b/zephcore/adapters/datastore/ZephyrDataStore.cpp index 55d3b35..7a10b21 100644 --- a/zephcore/adapters/datastore/ZephyrDataStore.cpp +++ b/zephcore/adapters/datastore/ZephyrDataStore.cpp @@ -7,6 +7,7 @@ */ #include "ZephyrDataStore.h" +#include // ADV_TYPE_NONE (transient/anon contacts) #include #include #include @@ -702,18 +703,25 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host) struct fs_file_t file; uint8_t rec[CONTACT_DATA_SZ]; - uint32_t idx = 0; + uint32_t idx = 0; // contacts iterated (incl. skipped anon) + uint32_t written = 0; // records actually written to the file ContactInfo c; bool write_ok = true; auto write_contacts = [&](struct fs_file_t *dst) -> bool { while (host->getContactForSave(idx, c)) { + // Don't persist transient/anon contacts (non-contact requests) + if (c.type == ADV_TYPE_NONE) { + idx++; + continue; + } contact_to_record(c, rec); if (fs_write(dst, rec, CONTACT_DATA_SZ) != (ssize_t)CONTACT_DATA_SZ) { LOG_ERR("saveContacts: write failed at record %u", idx); return false; } idx++; + written++; } return true; }; @@ -749,7 +757,7 @@ void ZephyrDataStore::saveContacts(DataStoreHost *host) return; } LOG_INF("saveContacts: mode=%s saved %u contacts to %s (%u bytes)", - save_mode, idx, path, idx * CONTACT_DATA_SZ); + save_mode, written, path, written * CONTACT_DATA_SZ); } /* ── Channels ──────────────────────────────────────────────────────── */ diff --git a/zephcore/app/CompanionMesh.cpp b/zephcore/app/CompanionMesh.cpp index 088944a..e172ca9 100644 --- a/zephcore/app/CompanionMesh.cpp +++ b/zephcore/app/CompanionMesh.cpp @@ -552,8 +552,9 @@ bool CompanionMesh::continueContactIteration() if (_contact_iter_idx < getNumContacts()) { ContactInfo c; if (getContactByIdx(_contact_iter_idx, c)) { + // Skip transient/anon contacts (ADV_TYPE_NONE) — never synced to the app. // Apply 'since' filter - only send contacts modified after the timestamp - if (c.lastmod > _contact_iter_since) { + if (c.type != ADV_TYPE_NONE && c.lastmod > _contact_iter_since) { if (c.lastmod > _contact_iter_lastmod) { _contact_iter_lastmod = c.lastmod; } @@ -2226,6 +2227,9 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE __DATE__ #endif + #ifndef FIRMWARE_VERSION + #define FIRMWARE_VERSION "v0.0.0-dev" // real value injected by CMakeLists.txt + #endif /* Wire format reserves 40 bytes for the board name. Require room * for a null terminator within those 40 bytes so a phone parsing * it as a C-string never reads past the field. */ @@ -2233,10 +2237,10 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) "CONFIG_ZEPHCORE_BOARD_NAME must fit in 40 bytes including null terminator"); static const uint8_t fw_build[12] = FIRMWARE_BUILD_DATE; static const uint8_t model[40] = CONFIG_ZEPHCORE_BOARD_NAME; - static const uint8_t version[20] = "v1.15.9-zephyr"; + static const uint8_t version[20] = FIRMWARE_VERSION; // injected by CMakeLists.txt uint8_t rsp[82]; rsp[0] = PACKET_DEVICE_INFO; - rsp[1] = 12; // FIRMWARE_VER_CODE - v12 = CMD_SET_FLOOD_SCOPE_KEY [0x36, 0x01] unscoped variant + rsp[1] = 13; // FIRMWARE_VER_CODE - v13 = CMD_SEND_ANON_REQ to non-contact pubkey (transient anon contacts) rsp[2] = (MAX_CONTACTS / 2 > 255) ? 255 : (MAX_CONTACTS / 2); // protocol byte, app multiplies by 2 rsp[3] = MAX_GROUP_CHANNELS; put_le32(&rsp[4], prefs.ble_pin ? prefs.ble_pin : 123456); // BLE PIN @@ -2952,6 +2956,19 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) case CMD_SEND_ANON_REQ: if (len >= 1 + PUB_KEY_SIZE + 1) { ContactInfo *contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + if (contact == nullptr) { + /* FIRMWARE_VER_CODE 13+: allow requests to a non-contact pubkey by + * creating a transient "anon" contact (ADV_TYPE_NONE). These are never + * persisted or synced to the app; re-lookup to get the stable slot. */ + ContactInfo anon; + memset(&anon, 0, sizeof(anon)); + memcpy(anon.id.pub_key, &data[1], PUB_KEY_SIZE); + anon.out_path_len = 0; // zero-hop direct by default + anon.type = ADV_TYPE_NONE; // transient/unknown + if (addContact(anon)) { + contact = lookupContactByPubKey(&data[1], PUB_KEY_SIZE); + } + } if (contact) { uint32_t tag, est_timeout; int result = sendAnonReq(*contact, &data[1 + PUB_KEY_SIZE], len - 1 - PUB_KEY_SIZE, tag, est_timeout); @@ -2963,7 +2980,7 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len) sendPacketError(ERR_BAD_STATE); } } else { - sendPacketError(ERR_NOT_FOUND); + sendPacketError(ERR_TABLE_FULL); } } else { sendPacketError(ERR_ILLEGAL_ARG); diff --git a/zephcore/app/ObserverMesh.h b/zephcore/app/ObserverMesh.h index 689a800..2350b39 100644 --- a/zephcore/app/ObserverMesh.h +++ b/zephcore/app/ObserverMesh.h @@ -19,7 +19,9 @@ #include "observer_creds.h" #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.15.9-zephyr" + // Real version injected by CMakeLists.txt (-DFIRMWARE_VERSION); this fallback + // only applies to builds that bypass that injection and should never surface. + #define FIRMWARE_VERSION "v0.0.0-dev" #endif #ifndef FIRMWARE_BUILD_DATE diff --git a/zephcore/app/RepeaterDataStore.cpp b/zephcore/app/RepeaterDataStore.cpp index 33dc218..52e8c06 100644 --- a/zephcore/app/RepeaterDataStore.cpp +++ b/zephcore/app/RepeaterDataStore.cpp @@ -195,6 +195,10 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { fs_read(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle)); fs_read(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled)); fs_read(&file, &prefs.apc_margin, sizeof(prefs.apc_margin)); + /* Flood hop-ceiling extensions (absent in <296-byte files; the no-op EOF + * read leaves the constructor defaults flood_max_unscoped=64, flood_max_advert=8). */ + fs_read(&file, &prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped)); + fs_read(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert)); fs_close(&file); @@ -234,7 +238,7 @@ bool RepeaterDataStore::loadPrefs(NodePrefs& prefs) { prefs.path_hash_mode = 1; prefs.loop_detect = LOOP_DETECT_MODERATE; savePrefs(prefs); - LOG_INF("loadPrefs: upgraded prefs format (%d -> 294 bytes)", (int)entry.size); + LOG_INF("loadPrefs: upgraded prefs format (%d -> 296 bytes)", (int)entry.size); } /* One-time migration: disable RX duty cycle if it was previously enabled. */ @@ -313,6 +317,9 @@ bool RepeaterDataStore::savePrefs(const NodePrefs& prefs) { fs_write(&file, &prefs.rx_duty_cycle, sizeof(prefs.rx_duty_cycle)); fs_write(&file, &prefs.apc_enabled, sizeof(prefs.apc_enabled)); fs_write(&file, &prefs.apc_margin, sizeof(prefs.apc_margin)); + /* Flood hop-ceiling extensions (extend the format past 294 bytes) */ + fs_write(&file, &prefs.flood_max_unscoped, sizeof(prefs.flood_max_unscoped)); + fs_write(&file, &prefs.flood_max_advert, sizeof(prefs.flood_max_advert)); ret = fs_sync(&file); fs_close(&file); diff --git a/zephcore/app/RepeaterMesh.cpp b/zephcore/app/RepeaterMesh.cpp index 61ff880..5b3ff98 100644 --- a/zephcore/app/RepeaterMesh.cpp +++ b/zephcore/app/RepeaterMesh.cpp @@ -532,6 +532,8 @@ bool RepeaterMesh::allowPacketForward(const mesh::Packet* packet) { if (packet->getPathHashCount() >= _prefs.flood_max) return false; // un-scoped floods can be clamped to a lower hop limit than scoped (transport) floods if (packet->getRouteType() == ROUTE_TYPE_FLOOD && packet->getPathHashCount() >= _prefs.flood_max_unscoped) return false; + // ADVERT floods get their own (typically tighter) hop ceiling to curb advert churn + if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= _prefs.flood_max_advert) return false; } if (packet->isRouteFlood() && recv_pkt_region == nullptr) return false; if (packet->isRouteFlood() && _prefs.loop_detect != LOOP_DETECT_OFF) { diff --git a/zephcore/app/RepeaterMesh.h b/zephcore/app/RepeaterMesh.h index cc75090..27b369c 100644 --- a/zephcore/app/RepeaterMesh.h +++ b/zephcore/app/RepeaterMesh.h @@ -28,7 +28,9 @@ #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.15.9-zephyr" + // Real version injected by CMakeLists.txt (-DFIRMWARE_VERSION); this fallback + // only applies to builds that bypass that injection and should never surface. + #define FIRMWARE_VERSION "v0.0.0-dev" #endif #ifndef FIRMWARE_BUILD_DATE diff --git a/zephcore/app/RoomServerMesh.h b/zephcore/app/RoomServerMesh.h index 2baf269..d4dc03e 100644 --- a/zephcore/app/RoomServerMesh.h +++ b/zephcore/app/RoomServerMesh.h @@ -27,7 +27,9 @@ #include "RepeaterDataStore.h" #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.15.9-zephyr" + // Real version injected by CMakeLists.txt (-DFIRMWARE_VERSION); this fallback + // only applies to builds that bypass that injection and should never surface. + #define FIRMWARE_VERSION "v0.0.0-dev" #endif #ifndef FIRMWARE_BUILD_DATE diff --git a/zephcore/boards/common/zephcore_common.conf b/zephcore/boards/common/zephcore_common.conf index 26747d8..7802504 100644 --- a/zephcore/boards/common/zephcore_common.conf +++ b/zephcore/boards/common/zephcore_common.conf @@ -147,10 +147,11 @@ CONFIG_BT_DIS_MANUF_NAME_STR="ZephCore" CONFIG_BT_DIS_MODEL_NUMBER=y CONFIG_BT_DIS_MODEL_NUMBER_STR="ZephCore" CONFIG_BT_DIS_FW_REV=y -# Keep in sync with the version string in CompanionMesh.cpp CMD_DEVICE_QUERY -# response (the v1.15.9-zephyr literal). Phones that read DIS and phones +# Keep in sync with ZEPHCORE_FIRMWARE_VERSION in CMakeLists.txt (the single source +# of truth for the C side, injected as -DFIRMWARE_VERSION). This Kconfig value can't +# read a C macro, so it must be bumped here too. Phones that read DIS and phones # that query CMD_DEVICE_QUERY should see the same version. -CONFIG_BT_DIS_FW_REV_STR="v1.15.9-zephyr" +CONFIG_BT_DIS_FW_REV_STR="v1.16.0-zephyr" CONFIG_BT_DIS_SW_REV=y CONFIG_BT_DIS_SW_REV_STR="Zephyr" CONFIG_BT_DIS_PNP=n diff --git a/zephcore/helpers/BaseChatMesh.cpp b/zephcore/helpers/BaseChatMesh.cpp index f5e4a1e..d348408 100644 --- a/zephcore/helpers/BaseChatMesh.cpp +++ b/zephcore/helpers/BaseChatMesh.cpp @@ -82,18 +82,27 @@ void BaseChatMesh::bootstrapRTCfromContacts() } } -ContactInfo *BaseChatMesh::allocateContactSlot() +ContactInfo *BaseChatMesh::allocateContactSlot(bool transient_only) { if (num_contacts < MAX_CONTACTS) { return &contacts[num_contacts++]; - } else if (shouldOverwriteWhenFull()) { + } else if (transient_only || shouldOverwriteWhenFull()) { int oldest_idx = -1; uint32_t oldest_lastmod = 0xFFFFFFFF; for (int i = 0; i < num_contacts; i++) { - bool is_favourite = (contacts[i].flags & 0x01) != 0; - if (!is_favourite && contacts[i].lastmod < oldest_lastmod) { - oldest_lastmod = contacts[i].lastmod; - oldest_idx = i; + if (transient_only) { + // transient/anon requests only ever recycle an existing anon slot + if (contacts[i].type == ADV_TYPE_NONE && contacts[i].lastmod < oldest_lastmod) { + oldest_lastmod = contacts[i].lastmod; + oldest_idx = i; + } + } else { + // never evict favourites or transient/anon contacts + bool is_favourite = (contacts[i].flags & 0x01) != 0; + if (!is_favourite && contacts[i].lastmod < oldest_lastmod && contacts[i].type != ADV_TYPE_NONE) { + oldest_lastmod = contacts[i].lastmod; + oldest_idx = i; + } } } if (oldest_idx >= 0) { @@ -740,7 +749,7 @@ ContactInfo *BaseChatMesh::lookupContactByPubKey(const uint8_t *pub_key, int pre bool BaseChatMesh::addContact(const ContactInfo &contact) { - ContactInfo *dest = allocateContactSlot(); + ContactInfo *dest = allocateContactSlot(contact.type == ADV_TYPE_NONE); if (dest) { *dest = contact; dest->shared_secret_valid = false; diff --git a/zephcore/helpers/BaseChatMesh.h b/zephcore/helpers/BaseChatMesh.h index 5c21c6b..fa629e8 100644 --- a/zephcore/helpers/BaseChatMesh.h +++ b/zephcore/helpers/BaseChatMesh.h @@ -19,6 +19,11 @@ #define MAX_CONTACTS 32 #endif +/* Headroom for transient "anon" contacts (type ADV_TYPE_NONE) created to service + * non-contact requests (CMD_SEND_ANON_REQ to a pubkey not in the contact list). + * These are never persisted or synced to the app. Mirrors upstream. */ +#define MAX_ANON_CONTACTS 8 + #ifdef CONFIG_ZEPHCORE_MAX_CHANNELS #define MAX_GROUP_CHANNELS CONFIG_ZEPHCORE_MAX_CHANNELS #else @@ -78,9 +83,9 @@ public: class BaseChatMesh : public mesh::Mesh { friend class ContactsIterator; - ContactInfo contacts[MAX_CONTACTS]; + ContactInfo contacts[MAX_CONTACTS + MAX_ANON_CONTACTS]; int num_contacts; - int sort_array[MAX_CONTACTS]; + int sort_array[MAX_CONTACTS + MAX_ANON_CONTACTS]; int matching_peer_indexes[MAX_SEARCH_RESULTS]; unsigned long txt_send_timeout; @@ -117,7 +122,7 @@ protected: void bootstrapRTCfromContacts(); void resetContacts() { num_contacts = 0; } void populateContactFromAdvert(ContactInfo &ci, const mesh::Identity &id, const AdvertDataParser &parser, uint32_t timestamp); - ContactInfo *allocateContactSlot(); + ContactInfo *allocateContactSlot(bool transient_only = false); // UI concepts for subclasses to implement virtual bool isAutoAddEnabled() const { return true; } diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 5a171c0..6ecd9d5 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -116,6 +116,7 @@ void CommonCLI::loadPrefs(const char* path) { ok = ok && prefs_read(&file, &_prefs->apc_enabled, sizeof(_prefs->apc_enabled)); // 292 ok = ok && prefs_read(&file, &_prefs->apc_margin, sizeof(_prefs->apc_margin)); // 293 ok = ok && prefs_read(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 294 + ok = ok && prefs_read(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 295 if (!ok) { LOG_WRN("Prefs file %s truncated, some fields use defaults", path); @@ -160,6 +161,7 @@ void CommonCLI::loadPrefs(const char* path) { _prefs->apc_enabled = constrain(_prefs->apc_enabled, (uint8_t)0, (uint8_t)1); _prefs->apc_margin = constrain(_prefs->apc_margin, (uint8_t)6, (uint8_t)30); _prefs->flood_max_unscoped = constrain(_prefs->flood_max_unscoped, (uint8_t)0, (uint8_t)64); + _prefs->flood_max_advert = constrain(_prefs->flood_max_advert, (uint8_t)0, (uint8_t)64); LOG_INF("Loaded prefs from %s", path); } @@ -226,6 +228,7 @@ void CommonCLI::savePrefs(const char* path) { fs_write(&file, &_prefs->apc_enabled, sizeof(_prefs->apc_enabled)); fs_write(&file, &_prefs->apc_margin, sizeof(_prefs->apc_margin)); fs_write(&file, &_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); + fs_write(&file, &_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); fs_close(&file); LOG_INF("Saved prefs to %s", path); @@ -451,6 +454,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch (double)est, (double)ff); } else if (memcmp(config, "apc.margin", 10) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %d dB", (int)_callbacks->getAPCTargetMargin()); + } else if (memcmp(config, "flood.max.advert", 16) == 0) { + snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max_advert); } else if (memcmp(config, "flood.max.unscoped", 18) == 0) { snprintf(reply, CLI_REPLY_SIZE, "> %u", (uint32_t)_prefs->flood_max_unscoped); } else if (memcmp(config, "flood.max", 9) == 0) { @@ -679,6 +684,15 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _prefs->tx_delay_factor = atof(&config[8]); savePrefs(); strcpy(reply, "OK (ignored: txdelay is now adaptive)"); + } else if (memcmp(config, "flood.max.advert ", 17) == 0) { + int m = atoi(&config[17]); + if (m >= 0 && m <= 64) { + _prefs->flood_max_advert = (uint8_t)m; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: range 0-64"); + } } else if (memcmp(config, "flood.max.unscoped ", 19) == 0) { int m = atoi(&config[19]); if (m >= 0 && m <= 64) { diff --git a/zephcore/helpers/NodePrefs.h b/zephcore/helpers/NodePrefs.h index 07030ee..5da7c3a 100644 --- a/zephcore/helpers/NodePrefs.h +++ b/zephcore/helpers/NodePrefs.h @@ -48,6 +48,7 @@ struct NodePrefs { float bw; uint8_t flood_max; uint8_t flood_max_unscoped; // hop limit for un-scoped (ROUTE_TYPE_FLOOD) floods + uint8_t flood_max_advert; // hop limit for ADVERT floods (curbs advert churn) uint8_t interference_threshold; uint8_t agc_reset_interval; // stored as secs / 4 // Power saving @@ -120,6 +121,7 @@ static inline void initNodePrefs(NodePrefs* prefs) { prefs->multi_acks = 0; prefs->flood_max = 64; // max hops for flood packets (0 = blocking all!) prefs->flood_max_unscoped = 64; // un-scoped flood hop limit (defaults to flood_max) + prefs->flood_max_advert = 8; // ADVERT flood hop limit (upstream default) prefs->interference_threshold = 0; prefs->agc_reset_interval = 0; prefs->powersaving_enabled = 0; diff --git a/zephcore/src/main_companion.cpp b/zephcore/src/main_companion.cpp index 899cba9..5259b41 100644 --- a/zephcore/src/main_companion.cpp +++ b/zephcore/src/main_companion.cpp @@ -581,7 +581,7 @@ public: void savePrefs() override { data_store.savePrefs(companion_mesh.prefs); } - const char* getFirmwareVer() override { return "v1.15.9-zephyr"; } + const char* getFirmwareVer() override { return FIRMWARE_VERSION; } const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } const char* getRole() override { return "companion"; } bool formatFileSystem() override { return data_store.formatFileSystem(); }