EEPROM data format

JetHome controllers store board identification data in the EEPROM chip on the I2C bus: board name and version, serial number, MAC address, processor identifiers, and on some devices, a manufacturer digital signature. The chip address is specified in the description of each controller.

Data is written in the JEEFS format: a board header with a checksum, followed by a simple file system. The format is open; its reference implementation is the jeefs library for C, C++, Python, and Rust.

See also

Manufacturer signature verification is described in section Digital signature verification.

Memory layout

Offset

Size

Content

0x0000

256 bytes

Board header (512 bytes in obsolete version 1)

0x0100 (0x0200 in version 1)

variable

File system: chain of files, each file is a 28-byte header plus data

after the last file

until the end of memory

Free space, bytes 0x00 or 0xFF

Common rules for all structures:

  • Multi-byte fields are stored in little-endian order.

  • Structures are packed without alignment.

  • Checksum — CRC32 per IEEE 802.3 (polynomial 0xEDB88320), matches the crc32() function of the zlib library. Stored in little-endian order. In the board header and in the device.id record it covers all bytes of the structure up to the checksum field. The file has two checksums: headerCrc32 covers the file header bytes up to itself, while crc32 covers the file data, not the header.

  • Reserved bytes within written structures are equal to 0x00. The unwritten area of an erased chip reads as 0xFF. Both values indicate the absence of data, and the integrity of written data is determined by the signature, version, and checksum.

  • String fields come in two types:

    • null-terminated strings (boardname, boardversion) — up to 31 characters, a null byte must follow the string;

    • fixed-length strings (board_serial, usid, cpuid) — printable ASCII characters (0x20–0x7E), padded with null bytes. If the value fills the field entirely, there is no null byte.

Board header

The header describes one board. The processor module and the motherboard each carry their own header in their EEPROM.

Header version determination

The first 12 bytes are identical for all versions:

Offset

Size

Field

Description

0–7

8

magic

Signature JETHOME\0: bytes 4A 45 54 48 4F 4D 45 00

8

1

version

Header version

9–11

3

—

Meaning depends on version

Detection order:

  1. Read 12 bytes and compare the signature byte by byte. If all bytes equal 0x00 or 0xFF, the header is not written.

  2. Select the structure based on the version byte: version 1 — 512 bytes, versions 2, 3, and 4 — 256 bytes.

  3. Unknown version — parse error.

Header version 4

The current header version is 4.

Byte map of the 256-byte board header version 4

Layout of header version 4. Rows are 16 bytes each, the row address is shown on the left. Green marks board information, yellow — values that build the string signed with the device signature (see Digital signature verification).

Offset

Size

Field

Description

0–7

8

magic

Signature JETHOME\0

8

1

version

Header version: 4

9

1

signature_version

10

1

fs_version

File system version number, current version is 1

11

1

—

Reserved

12–43

32

boardname

Board name

44–75

32

boardversion

Board version

76–107

32

board_serial

Board serial number

108–139

32

usid

Internal device identifier (USID)

140–171

32

cpuid

Processor identifier

172–177

6

mac

MAC address, 6 bytes in binary

178–179

2

—

Reserved

180–243

64

signature

244–251

8

timestamp

Header write time: signed 64-bit number, seconds of Unix time

252–255

4

crc32

CRC32 of bytes 0–251

Field meanings:

  • board_serial — serial number of this board. The device serial number as a whole is stored in the device.id record.

  • usid and cpuid are filled if the board has such identifiers, otherwise the fields are zero. For JXD series controllers, usid is a string of 30 characters, cpuid is the factory MAC address of the ESP32 microcontroller in the form XX:XX:XX:XX:XX:XX.

  • timestamp — header write moment. A value of 0 means no time is set.

A header whose bytes from offset 12 to the crc32 field are all zero is empty: it reserves space for the header but contains no board data.

Reading the header

  1. Determine the version by the first 12 bytes.

  2. Compare the CRC32 of bytes 0–251 with the value in bytes 252–255.

  3. Extract strings: up to the first zero byte, and if there is none — the entire field.

  4. Read fs_version — the file system version number, which is described below. An unknown version is a parse error.

  5. Ignore non-zero bytes in reserved fields.

File system

The file system version is written in the fs_version byte of the board header; the current version is 1. Files form a one-way chain that starts right after the board header: at offset 0x0100, or 0x0200 for header version 1. Each file is a file header followed by data.

File header

Offset

Size

Field

Description

0–15

16

name

File name: up to 15 printable ASCII characters, terminated by a zero byte

16–17

2

dataSize

Data size: from 1 to 32767 bytes

18–21

4

crc32

CRC32 of the file data

22–23

2

nextFileAddress

Offset of the next file from the start of EEPROM; 0x0000 or 0xFFFF — end of chain

24–27

4

headerCrc32

CRC32 of bytes 0–23 of the file header

Chain rules

  • File data with offset A and size D occupies bytes from A+28 to A+28+D-1, the next file starts at A+28+D. For the last file in the chain, the nextFileAddress field equals 0x0000 or 0xFFFF; for other files, it must contain exactly this calculated value; any other value indicates corruption.

  • The file is stored contiguously. When a file is deleted, subsequent files shift into its place, and the freed end is filled with 0x00.

  • Files are arranged in the order of addition. The exception is the device.id file: it is always first.

  • A slot is free if the first byte of the name equals 0x00 or 0xFF. A written name with an incorrect headerCrc32 indicates corruption, not a free slot.

  • headerCrc32 is verified when traversing the chain for each header, and data crc32 — on every file read.

  • Offsets are 16-bit, so the memory size does not exceed 64 KB.

Device identification data

A device may consist of multiple boards, each with its own EEPROM. The model, serial number, and hardware revision of the device as a whole are stored separately from the board data — in the device.id file, so that device identification data is preserved when a board is replaced during repair.

The device.id file is always first in the chain, so the loader obtains device data by reading 540 bytes: board header (256), file header (28), and record (256). With version 1 header — 796 bytes.

Record format of device.id, 256 bytes:

Offset

Size

Field

Description

0–7

8

magic

Signature JHDEVID\0

8

1

record_version

Record version: 1

9

1

signature_version

Device signature algorithm: 0 — no signature, 1 — ECDSA secp192r1, 2 — ECDSA secp256r1

10–11

2

—

Reserved

12–43

32

device_model

Device model

44–75

32

device_serial

Device serial number

76–91

16

hw_revision

Hardware revision: numbers separated by a dot with an optional variant letter, e.g., 1.2 or 1.2a

92–93

2

flags

Flags, all bits reserved: write 0, ignore on read

94–179

86

—

Reserved

180–243

64

signature

Device signature: a pair of numbers r and s without DER encoding, padded with zeros

244–251

8

timestamp

Record creation time, seconds of Unix time

252–255

4

crc32

CRC32 of bytes 0–251

Record string fields are fixed-length strings. The record tail (signature, timestamp, crc32) is located at the same offsets as in the board header. The record is protected by two checksums: its own and the CRC32 of the file data. Therefore, it can be verified both inside the file system and separately when extracted.

Record reading rules:

  • A buffer consisting entirely of 0x00 or 0xFF means there is no record.

  • record_version other than 1 and unknown signature_version are a parse error.

The record signature is the device signature. What it signs and how to verify it is described in section Digital signature verification.

Previous header versions

The jeefs library parses all header versions.

Version

Size

Differences from version 4

1

512 bytes

There are no signature_version, signature, or timestamp fields. Bytes 180–211 are the modules array of 16 module identifiers (16-bit numbers), 212–507 are reserved, and a CRC32 of bytes 0–507 is written in bytes 508–511. The file system starts at offset 0x0200.

2

256 bytes

There are no signature_version, signature, or timestamp fields: bytes 9, 11, and 180–251 are reserved.

3

256 bytes

The layout matches version 4, and the field at offset 76 is called serial.

In versions 1 and 2, byte 10 is described as part of the reserved area, but it always remains the fs_version byte: a file system write places the version number in it and recalculates the header checksum. When rewriting the header, this byte must be preserved.

The jeefs library

The jeefs library implements the format in several languages. Implementations are checked by a common set of tests and reference images.

Language

Installation

Features

Python

pip install jeefs

Headers, writing device.id, packing and unpacking a complete EEPROM image

Rust

cargo add jeefs-header

Headers, writing device.id, file system (no dynamic memory, no_std), packing and unpacking an image

C, C++17

CMake package jeefs, pkg-config

Headers, writing device.id, file system. The library performs no I/O and operates on a buffer read from EEPROM

Parsing an EEPROM image in Python:

from pathlib import Path

from jeefs import parse_image

image = parse_image(Path("eeprom.bin").read_bytes())
header = image.header
if header is None:  # the Python package does not parse v1 and v2 fields
    print(f"v{image.version}: header fields unavailable")
else:
    print(f"v{image.version}", header.boardname, header.boardversion, header.mac)
for file in image.files:
    print(file.name, len(file.data))

The parse_image function raises a ValueError exception if the board header is not found or is damaged, the file chain is broken, or the file system version is unknown. Files with an incorrect data checksum are listed in image.unreadable. For images with headers of versions 1 and 2, image.header is None: the Python package does not parse the fields of these versions; they are read by the C implementation.