From d32674617d92706fb6f658a230e66d3e12314865 Mon Sep 17 00:00:00 2001 From: kerms Date: Wed, 25 Feb 2026 16:31:05 +0100 Subject: [PATCH] feat: add OTA data parser and fix verbatimModuleSyntax compliance - Add lib/ota-data/ module: parses esp_ota_select_entry_t structures, determines active OTA partition from sequence numbers and CRC validation - Fix all lib files to use `import type` for type-only imports to comply with TypeScript verbatimModuleSyntax (partition-table, nvs, app-image) - Export ota-data from lib/index.ts --- lib/app-image/parser.ts | 2 +- lib/index.ts | 1 + lib/nvs/nvs-binary-parser.ts | 3 +- lib/nvs/nvs-binary-serializer.ts | 3 +- lib/nvs/nvs-csv-parser.ts | 3 +- lib/nvs/nvs-csv-serializer.ts | 3 +- lib/nvs/nvs-partition.ts | 6 +-- lib/ota-data/constants.ts | 8 +++ lib/ota-data/index.ts | 9 ++++ lib/ota-data/parser.ts | 78 +++++++++++++++++++++++++++ lib/ota-data/types.ts | 36 +++++++++++++ lib/partition-table/csv-parser.ts | 3 +- lib/partition-table/csv-serializer.ts | 3 +- lib/partition-table/parser.ts | 3 +- lib/partition-table/serializer.ts | 2 +- lib/partition-table/validator.ts | 2 +- 16 files changed, 151 insertions(+), 14 deletions(-) create mode 100644 lib/ota-data/constants.ts create mode 100644 lib/ota-data/index.ts create mode 100644 lib/ota-data/parser.ts create mode 100644 lib/ota-data/types.ts diff --git a/lib/app-image/parser.ts b/lib/app-image/parser.ts index 88a1a1f..00a0b42 100644 --- a/lib/app-image/parser.ts +++ b/lib/app-image/parser.ts @@ -1,5 +1,5 @@ import { readU8, readU16, readU32, readNullTermString } from '../shared/binary-reader'; -import { +import type { AppImageInfo, ImageHeader, ExtendedHeader, SegmentHeader, AppDescription, } from './types'; import { diff --git a/lib/index.ts b/lib/index.ts index 77d4b7a..1a8c26f 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,3 +2,4 @@ export * from './shared'; export * as nvs from './nvs'; export * as partitionTable from './partition-table'; export * as appImage from './app-image'; +export * as otaData from './ota-data'; diff --git a/lib/nvs/nvs-binary-parser.ts b/lib/nvs/nvs-binary-parser.ts index 4c90ba0..2ed9030 100644 --- a/lib/nvs/nvs-binary-parser.ts +++ b/lib/nvs/nvs-binary-parser.ts @@ -1,4 +1,5 @@ -import { NvsPartition, NvsEntry, NvsType, NvsVersion, PageState, EntryState } from './types'; +import type { NvsPartition, NvsEntry } from './types'; +import { NvsType, NvsVersion, PageState, EntryState } from './types'; import { PAGE_SIZE, PAGE_HEADER_SIZE, BITMAP_OFFSET, BITMAP_SIZE, FIRST_ENTRY_OFFSET, ENTRY_SIZE, ENTRIES_PER_PAGE, KEY_FIELD_SIZE, diff --git a/lib/nvs/nvs-binary-serializer.ts b/lib/nvs/nvs-binary-serializer.ts index 0a4a9f7..02377f6 100644 --- a/lib/nvs/nvs-binary-serializer.ts +++ b/lib/nvs/nvs-binary-serializer.ts @@ -1,4 +1,5 @@ -import { NvsPartition, NvsType, NvsVersion, PageState } from './types'; +import type { NvsPartition } from './types'; +import { NvsType, NvsVersion, PageState } from './types'; import { PAGE_SIZE, PAGE_HEADER_SIZE, BITMAP_OFFSET, BITMAP_SIZE, FIRST_ENTRY_OFFSET, ENTRY_SIZE, ENTRIES_PER_PAGE, diff --git a/lib/nvs/nvs-csv-parser.ts b/lib/nvs/nvs-csv-parser.ts index a3d9046..29ce262 100644 --- a/lib/nvs/nvs-csv-parser.ts +++ b/lib/nvs/nvs-csv-parser.ts @@ -1,4 +1,5 @@ -import { NvsPartition, NvsEntry, NvsType, NvsVersion, NvsEncoding, ENCODING_TO_TYPE } from './types'; +import type { NvsPartition, NvsEntry, NvsEncoding } from './types'; +import { NvsType, NvsVersion, ENCODING_TO_TYPE } from './types'; import { generateEntryId } from './nvs-partition'; /** diff --git a/lib/nvs/nvs-csv-serializer.ts b/lib/nvs/nvs-csv-serializer.ts index 92f13a8..cfadbc8 100644 --- a/lib/nvs/nvs-csv-serializer.ts +++ b/lib/nvs/nvs-csv-serializer.ts @@ -1,4 +1,5 @@ -import { NvsPartition, NvsType, TYPE_TO_ENCODING, isPrimitiveType } from './types'; +import type { NvsPartition } from './types'; +import { NvsType, TYPE_TO_ENCODING, isPrimitiveType } from './types'; /** Convert Uint8Array to hex string */ function bytesToHex(data: Uint8Array): string { diff --git a/lib/nvs/nvs-partition.ts b/lib/nvs/nvs-partition.ts index 446e8b1..51a578a 100644 --- a/lib/nvs/nvs-partition.ts +++ b/lib/nvs/nvs-partition.ts @@ -1,7 +1,5 @@ -import { - NvsEntry, NvsPartition, NvsFlashStats, NvsType, NvsVersion, - isPrimitiveType, -} from './types'; +import type { NvsEntry, NvsPartition, NvsFlashStats } from './types'; +import { NvsType, NvsVersion, isPrimitiveType } from './types'; import { ENTRIES_PER_PAGE, ENTRY_SIZE, PAGE_SIZE, MAX_KEY_LENGTH, MAX_NAMESPACES, MAX_STRING_LENGTH, MAX_BLOB_SIZE_V1, MAX_BLOB_SIZE_V2 } from './constants'; /** Generate a random unique ID for client-side entry tracking */ diff --git a/lib/ota-data/constants.ts b/lib/ota-data/constants.ts new file mode 100644 index 0000000..ef660e6 --- /dev/null +++ b/lib/ota-data/constants.ts @@ -0,0 +1,8 @@ +/** Size of one esp_ota_select_entry_t structure in bytes */ +export const OTA_SELECT_ENTRY_SIZE = 32; + +/** Minimum data needed: two entries */ +export const OTA_DATA_MIN_SIZE = 2 * OTA_SELECT_ENTRY_SIZE; + +/** Default number of OTA app partitions (used to derive partition name from seq) */ +export const DEFAULT_NUM_OTA_PARTITIONS = 2; diff --git a/lib/ota-data/index.ts b/lib/ota-data/index.ts new file mode 100644 index 0000000..69c1a14 --- /dev/null +++ b/lib/ota-data/index.ts @@ -0,0 +1,9 @@ +// Types +export type { OtaSelectEntry, OtaData } from './types'; +export { OtaImgState, OTA_STATE_NAMES } from './types'; + +// Constants +export { OTA_SELECT_ENTRY_SIZE, OTA_DATA_MIN_SIZE, DEFAULT_NUM_OTA_PARTITIONS } from './constants'; + +// Parser +export { parseOtaData } from './parser'; diff --git a/lib/ota-data/parser.ts b/lib/ota-data/parser.ts new file mode 100644 index 0000000..1e043cb --- /dev/null +++ b/lib/ota-data/parser.ts @@ -0,0 +1,78 @@ +import { readU32 } from '../shared/binary-reader'; +import { readNullTermString } from '../shared/binary-reader'; +import { crc32 } from '../shared/crc32'; +import { OTA_SELECT_ENTRY_SIZE, OTA_DATA_MIN_SIZE, DEFAULT_NUM_OTA_PARTITIONS } from './constants'; +import { OtaImgState } from './types'; +import type { OtaSelectEntry, OtaData } from './types'; + +/** + * Parse a single esp_ota_select_entry_t (32 bytes): + * uint32_t ota_seq [0..3] + * uint8_t seq_label[20] [4..23] + * uint32_t ota_state [24..27] + * uint32_t crc [28..31] + * + * CRC is computed over the first 28 bytes. + */ +function parseEntry(data: Uint8Array, offset: number): OtaSelectEntry { + const seq = readU32(data, offset); + const label = readNullTermString(data, offset + 4, 20); + const state = readU32(data, offset + 24) as OtaImgState; + const storedCrc = readU32(data, offset + 28); + const computedCrc = crc32(data.subarray(offset, offset + 28)); + + return { + seq, + label, + state, + crc: storedCrc, + crcValid: storedCrc === computedCrc, + }; +} + +/** Check if an entry represents a valid (non-empty) OTA selection */ +function isEntryValid(entry: OtaSelectEntry): boolean { + // seq of 0 or 0xFFFFFFFF means empty/erased + return entry.crcValid && entry.seq !== 0 && entry.seq !== 0xFFFFFFFF; +} + +/** + * Parse OTA data partition binary. + * + * @param data - Raw bytes of the otadata partition (typically 0x2000 bytes, only first 64 used) + * @param numOtaPartitions - Number of OTA app partitions (default 2, used for partition name derivation) + */ +export function parseOtaData(data: Uint8Array, numOtaPartitions = DEFAULT_NUM_OTA_PARTITIONS): OtaData { + if (data.length < OTA_DATA_MIN_SIZE) { + throw new Error(`OTA data too short: ${data.length} bytes, need at least ${OTA_DATA_MIN_SIZE}`); + } + + const entry0 = parseEntry(data, 0); + const entry1 = parseEntry(data, OTA_SELECT_ENTRY_SIZE); + + // Determine active entry: the one with the higher valid seq + let activeIndex: number | null = null; + const valid0 = isEntryValid(entry0); + const valid1 = isEntryValid(entry1); + + if (valid0 && valid1) { + activeIndex = entry0.seq >= entry1.seq ? 0 : 1; + } else if (valid0) { + activeIndex = 0; + } else if (valid1) { + activeIndex = 1; + } + + let activeOtaPartition: string | null = null; + if (activeIndex !== null) { + const activeSeq = activeIndex === 0 ? entry0.seq : entry1.seq; + const partitionIndex = (activeSeq - 1) % numOtaPartitions; + activeOtaPartition = `ota_${partitionIndex}`; + } + + return { + entries: [entry0, entry1], + activeIndex, + activeOtaPartition, + }; +} diff --git a/lib/ota-data/types.ts b/lib/ota-data/types.ts new file mode 100644 index 0000000..c52391e --- /dev/null +++ b/lib/ota-data/types.ts @@ -0,0 +1,36 @@ +/** OTA image state enum matching ESP-IDF esp_ota_img_states_t */ +export enum OtaImgState { + NEW = 0x0, + PENDING_VERIFY = 0x1, + VALID = 0x2, + INVALID = 0x3, + ABORTED = 0x4, + UNDEFINED = 0xFFFFFFFF, +} + +export const OTA_STATE_NAMES: Record = { + [OtaImgState.NEW]: 'new', + [OtaImgState.PENDING_VERIFY]: 'pending_verify', + [OtaImgState.VALID]: 'valid', + [OtaImgState.INVALID]: 'invalid', + [OtaImgState.ABORTED]: 'aborted', + [OtaImgState.UNDEFINED]: 'undefined', +}; + +/** A single OTA select entry (parsed from 32-byte structure) */ +export interface OtaSelectEntry { + seq: number; + label: string; + state: OtaImgState; + crc: number; + crcValid: boolean; +} + +/** Parsed OTA data partition */ +export interface OtaData { + entries: [OtaSelectEntry, OtaSelectEntry]; + /** Index of the active entry (0 or 1), null if neither is valid */ + activeIndex: number | null; + /** Derived OTA partition name, e.g. "ota_0", "ota_1" */ + activeOtaPartition: string | null; +} diff --git a/lib/partition-table/csv-parser.ts b/lib/partition-table/csv-parser.ts index f8dee38..7221b22 100644 --- a/lib/partition-table/csv-parser.ts +++ b/lib/partition-table/csv-parser.ts @@ -1,4 +1,5 @@ -import { PartitionEntry, PartitionFlags, PartitionTable, PartitionType, NAME_TO_TYPE, subtypeFromName } from './types'; +import type { PartitionEntry, PartitionTable } from './types'; +import { PartitionFlags, PartitionType, NAME_TO_TYPE, subtypeFromName } from './types'; const U32_MAX = 0xFFFF_FFFF; diff --git a/lib/partition-table/csv-serializer.ts b/lib/partition-table/csv-serializer.ts index 4677730..129e6e4 100644 --- a/lib/partition-table/csv-serializer.ts +++ b/lib/partition-table/csv-serializer.ts @@ -1,4 +1,5 @@ -import { PartitionTable, TYPE_NAMES, getSubtypeName } from './types'; +import type { PartitionTable } from './types'; +import { TYPE_NAMES, getSubtypeName } from './types'; /** Format a number as hex with 0x prefix */ function toHex(val: number): string { diff --git a/lib/partition-table/parser.ts b/lib/partition-table/parser.ts index db42ef0..4c907d1 100644 --- a/lib/partition-table/parser.ts +++ b/lib/partition-table/parser.ts @@ -1,5 +1,6 @@ import { readU16, readU32, readNullTermString } from '../shared/binary-reader'; -import { PartitionEntry, PartitionTable, PartitionType } from './types'; +import type { PartitionEntry, PartitionTable } from './types'; +import { PartitionType } from './types'; import { ENTRY_SIZE, ENTRY_MAGIC, MD5_MAGIC, NAME_FIELD_SIZE, MAX_ENTRIES } from './constants'; import { md5 } from './md5'; diff --git a/lib/partition-table/serializer.ts b/lib/partition-table/serializer.ts index 4591e7c..b2cfdb6 100644 --- a/lib/partition-table/serializer.ts +++ b/lib/partition-table/serializer.ts @@ -1,5 +1,5 @@ import { writeU16, writeU32, writeNullTermString } from '../shared/binary-writer'; -import { PartitionTable } from './types'; +import type { PartitionTable } from './types'; import { ENTRY_SIZE, ENTRY_MAGIC, MD5_MAGIC, NAME_FIELD_SIZE, TABLE_MAX_SIZE } from './constants'; import { md5 } from './md5'; diff --git a/lib/partition-table/validator.ts b/lib/partition-table/validator.ts index 9710561..613155e 100644 --- a/lib/partition-table/validator.ts +++ b/lib/partition-table/validator.ts @@ -1,4 +1,4 @@ -import { PartitionEntry, PartitionTable } from './types'; +import type { PartitionEntry, PartitionTable } from './types'; export interface PartitionValidationError { type: 'overlap' | 'alignment' | 'duplicate_name';