mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-19 05:04:20 +00:00
222 lines
7.1 KiB
C++
222 lines
7.1 KiB
C++
#pragma once
|
|
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
namespace mesh {
|
|
namespace storage {
|
|
|
|
// Contact pages are deliberately smaller than a 4 KiB LittleFS block. A
|
|
// single contact update therefore never rewrites the complete contact list.
|
|
static const uint8_t CONTACTS_PER_PAGE = 25;
|
|
static const uint8_t CONTACT_PAGE_COUNT = 14;
|
|
static const uint16_t CONTACT_RECORD_SIZE = 152;
|
|
static const uint16_t CONTACT_PAGE_HEADER_SIZE = 20;
|
|
static const uint16_t CONTACT_PAGE_PAYLOAD_SIZE =
|
|
CONTACTS_PER_PAGE * CONTACT_RECORD_SIZE;
|
|
static const uint16_t CONTACT_PAGE_FILE_SIZE =
|
|
CONTACT_PAGE_HEADER_SIZE + CONTACT_PAGE_PAYLOAD_SIZE;
|
|
static const uint16_t CONTACT_SLOT_NONE = 0xFFFF;
|
|
|
|
static const uint8_t CONTACT_PAGE_VERSION = 1;
|
|
static const uint8_t CONTACT_PAGE_MAGIC[4] = {'M', 'C', 'P', '4'};
|
|
|
|
struct ContactPageHeader {
|
|
uint8_t page_index;
|
|
uint32_t occupied;
|
|
uint32_t generation;
|
|
uint32_t payload_crc;
|
|
};
|
|
|
|
enum class ContactStoreSource : uint8_t {
|
|
EMPTY,
|
|
LEGACY,
|
|
PAGED,
|
|
};
|
|
|
|
// A retained legacy file means migration is still in progress. Its complete
|
|
// record count is the authoritative prefix boundary; page records at or above
|
|
// that boundary have already committed.
|
|
inline ContactStoreSource chooseContactStoreSource(bool legacy_exists,
|
|
bool page_exists) {
|
|
if (legacy_exists) return ContactStoreSource::LEGACY;
|
|
if (page_exists) return ContactStoreSource::PAGED;
|
|
return ContactStoreSource::EMPTY;
|
|
}
|
|
|
|
// When a legacy file returns after a downgrade, old page files may describe a
|
|
// different contact list. Only a marker created after stale pages were removed
|
|
// makes those pages valid participants in an in-progress migration.
|
|
inline bool trustMigratedContactPages(bool legacy_exists,
|
|
bool migration_marker_exists) {
|
|
return !legacy_exists || migration_marker_exists;
|
|
}
|
|
|
|
inline uint16_t legacyContactCountForSize(size_t file_size) {
|
|
const size_t count = file_size / CONTACT_RECORD_SIZE;
|
|
const size_t capacity = CONTACT_PAGE_COUNT * CONTACTS_PER_PAGE;
|
|
return (uint16_t)(count < capacity ? count : capacity);
|
|
}
|
|
|
|
inline uint8_t legacyMigrationPage(uint16_t legacy_contact_count) {
|
|
return legacy_contact_count == 0 ? CONTACT_PAGE_COUNT
|
|
: (uint8_t)((legacy_contact_count - 1) / CONTACTS_PER_PAGE);
|
|
}
|
|
|
|
inline uint16_t legacyCountAfterMigratingPage(uint8_t page) {
|
|
return page < CONTACT_PAGE_COUNT
|
|
? (uint16_t)page * CONTACTS_PER_PAGE : 0;
|
|
}
|
|
|
|
// During tail-first migration, the still-present prefix in /contacts3 wins.
|
|
// Page slots at or beyond its complete-record count are already committed and
|
|
// can be loaded. This also makes a reset between page commit and truncation
|
|
// harmless: the duplicate page records remain hidden until truncation commits.
|
|
inline bool loadSlotFromMigratedPage(uint16_t slot,
|
|
uint16_t legacy_contact_count) {
|
|
return slot >= legacy_contact_count;
|
|
}
|
|
|
|
inline uint16_t readLE16(const uint8_t* src) {
|
|
return (uint16_t)src[0] | ((uint16_t)src[1] << 8);
|
|
}
|
|
|
|
inline uint32_t readLE32(const uint8_t* src) {
|
|
return (uint32_t)src[0] | ((uint32_t)src[1] << 8)
|
|
| ((uint32_t)src[2] << 16) | ((uint32_t)src[3] << 24);
|
|
}
|
|
|
|
inline void writeLE16(uint8_t* dest, uint16_t value) {
|
|
dest[0] = (uint8_t)value;
|
|
dest[1] = (uint8_t)(value >> 8);
|
|
}
|
|
|
|
inline void writeLE32(uint8_t* dest, uint32_t value) {
|
|
dest[0] = (uint8_t)value;
|
|
dest[1] = (uint8_t)(value >> 8);
|
|
dest[2] = (uint8_t)(value >> 16);
|
|
dest[3] = (uint8_t)(value >> 24);
|
|
}
|
|
|
|
// Same CRC convention as AtomicFileWriter: reflected CRC-32, initial value
|
|
// 0xFFFFFFFF, with no final xor. Keeping this incremental makes it usable on
|
|
// small targets without buffering an entire file.
|
|
inline uint32_t updateCRC32(uint32_t crc, const uint8_t* data, size_t len) {
|
|
while (len-- > 0) {
|
|
crc ^= *data++;
|
|
for (uint8_t bit = 0; bit < 8; bit++) {
|
|
crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320UL : 0);
|
|
}
|
|
}
|
|
return crc;
|
|
}
|
|
|
|
inline void encodeContactPageHeader(uint8_t dest[CONTACT_PAGE_HEADER_SIZE],
|
|
const ContactPageHeader& header) {
|
|
memcpy(dest, CONTACT_PAGE_MAGIC, sizeof(CONTACT_PAGE_MAGIC));
|
|
dest[4] = CONTACT_PAGE_VERSION;
|
|
dest[5] = header.page_index;
|
|
writeLE16(&dest[6], CONTACT_RECORD_SIZE);
|
|
writeLE32(&dest[8], header.occupied);
|
|
writeLE32(&dest[12], header.generation);
|
|
writeLE32(&dest[16], header.payload_crc);
|
|
}
|
|
|
|
inline bool decodeContactPageHeader(const uint8_t src[CONTACT_PAGE_HEADER_SIZE],
|
|
uint8_t expected_page,
|
|
ContactPageHeader& header) {
|
|
const uint32_t valid_slots = (1UL << CONTACTS_PER_PAGE) - 1UL;
|
|
if (memcmp(src, CONTACT_PAGE_MAGIC, sizeof(CONTACT_PAGE_MAGIC)) != 0
|
|
|| src[4] != CONTACT_PAGE_VERSION || src[5] != expected_page
|
|
|| readLE16(&src[6]) != CONTACT_RECORD_SIZE) {
|
|
return false;
|
|
}
|
|
|
|
header.page_index = src[5];
|
|
header.occupied = readLE32(&src[8]);
|
|
header.generation = readLE32(&src[12]);
|
|
header.payload_crc = readLE32(&src[16]);
|
|
return (header.occupied & ~valid_slots) == 0;
|
|
}
|
|
|
|
class DirtyPageSet {
|
|
uint32_t _bits;
|
|
|
|
public:
|
|
DirtyPageSet() : _bits(0) {}
|
|
|
|
void clearAll() { _bits = 0; }
|
|
bool empty() const { return _bits == 0; }
|
|
uint32_t bits() const { return _bits; }
|
|
|
|
bool mark(uint8_t page) {
|
|
if (page >= CONTACT_PAGE_COUNT) return false;
|
|
_bits |= (1UL << page);
|
|
return true;
|
|
}
|
|
|
|
int first() const {
|
|
for (uint8_t page = 0; page < CONTACT_PAGE_COUNT; page++) {
|
|
if ((_bits & (1UL << page)) != 0) return page;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
void clear(uint8_t page) {
|
|
if (page < CONTACT_PAGE_COUNT) _bits &= ~(1UL << page);
|
|
}
|
|
};
|
|
|
|
class ContactSlotMap {
|
|
uint32_t _used[CONTACT_PAGE_COUNT];
|
|
|
|
public:
|
|
ContactSlotMap() { clear(); }
|
|
|
|
void clear() { memset(_used, 0, sizeof(_used)); }
|
|
|
|
uint32_t pageMask(uint8_t page) const {
|
|
return page < CONTACT_PAGE_COUNT ? _used[page] : 0;
|
|
}
|
|
|
|
bool isUsed(uint16_t slot) const {
|
|
if (slot >= CONTACT_PAGE_COUNT * CONTACTS_PER_PAGE) return false;
|
|
return (_used[slot / CONTACTS_PER_PAGE]
|
|
& (1UL << (slot % CONTACTS_PER_PAGE))) != 0;
|
|
}
|
|
|
|
bool reserve(uint16_t slot) {
|
|
if (slot >= CONTACT_PAGE_COUNT * CONTACTS_PER_PAGE) return false;
|
|
const uint8_t page = slot / CONTACTS_PER_PAGE;
|
|
const uint32_t bit = 1UL << (slot % CONTACTS_PER_PAGE);
|
|
if ((_used[page] & bit) != 0) return false;
|
|
_used[page] |= bit;
|
|
return true;
|
|
}
|
|
|
|
uint16_t allocate() {
|
|
const uint32_t valid_slots = (1UL << CONTACTS_PER_PAGE) - 1UL;
|
|
for (uint8_t page = 0; page < CONTACT_PAGE_COUNT; page++) {
|
|
if ((_used[page] & valid_slots) == valid_slots) continue;
|
|
for (uint8_t index = 0; index < CONTACTS_PER_PAGE; index++) {
|
|
const uint32_t bit = 1UL << index;
|
|
if ((_used[page] & bit) == 0) {
|
|
_used[page] |= bit;
|
|
return (uint16_t)page * CONTACTS_PER_PAGE + index;
|
|
}
|
|
}
|
|
}
|
|
return CONTACT_SLOT_NONE;
|
|
}
|
|
|
|
bool release(uint16_t slot) {
|
|
if (!isUsed(slot)) return false;
|
|
_used[slot / CONTACTS_PER_PAGE] &= ~(1UL << (slot % CONTACTS_PER_PAGE));
|
|
return true;
|
|
}
|
|
};
|
|
|
|
} // namespace storage
|
|
} // namespace mesh
|