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
This commit is contained in:
parent
577b845afc
commit
d32674617d
|
|
@ -1,5 +1,5 @@
|
|||
import { readU8, readU16, readU32, readNullTermString } from '../shared/binary-reader';
|
||||
import {
|
||||
import type {
|
||||
AppImageInfo, ImageHeader, ExtendedHeader, SegmentHeader, AppDescription,
|
||||
} from './types';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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<number, string> = {
|
||||
[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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { PartitionEntry, PartitionTable } from './types';
|
||||
import type { PartitionEntry, PartitionTable } from './types';
|
||||
|
||||
export interface PartitionValidationError {
|
||||
type: 'overlap' | 'alignment' | 'duplicate_name';
|
||||
|
|
|
|||
Loading…
Reference in New Issue