diff --git a/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.c b/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.c
index 4d0780d9..fb31cc21 100644
--- a/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.c
+++ b/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.c
@@ -273,3 +273,14 @@ uint16_t nvs_storage_get_version_in_nvs()
return storage_version_in_nvs;
}
+
+int32_t nvs_storage_get_free_space()
+{
+ if (initialized == false)
+ {
+ int8_t error = _nvs_storage_init();
+ if (error != 0) return error;
+ }
+
+ return nvs_calc_free_space(&fs);
+}
diff --git a/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.h b/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.h
index 08d284c3..b4676975 100644
--- a/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.h
+++ b/zephyr/modules/owntech_flash_driver/zephyr/public_api/nvs_storage.h
@@ -38,15 +38,17 @@ extern "C" {
/* Types definition */
/**
- * @brief Defines the NVS categories
- *
+ * @brief Defines the NVS categories
+ *
* - `VERSION` = 0x0100
- *
+ *
* - `ADC_CALIBRATION` = 0x0200
- *
+ *
* - `MEASURE_THRESHOLD` = 0x0300
- *
- *
+ *
+ * - `BOARD_METADATA` = 0x0400
+ *
+ *
* @note Must be on the upper half of the 2-bytes value, hence end with 00
*/
typedef enum
@@ -54,6 +56,7 @@ typedef enum
VERSION = 0x0100,
ADC_CALIBRATION = 0x0200,
MEASURE_THRESHOLD = 0x0300,
+ BOARD_METADATA = 0x0400,
}nvs_category_t;
/**
@@ -118,6 +121,17 @@ uint16_t nvs_storage_get_current_version();
*/
uint16_t nvs_storage_get_version_in_nvs();
+/**
+ * @brief Get the amount of free space left in the NVS partition.
+ *
+ * Useful to check how much of the storage budget remains before
+ * writing new data, since the NVS partition is shared by several
+ * modules (ADC calibration, safety thresholds, board metadata, ...).
+ *
+ * @return Free space in bytes on success, negative value on error.
+ */
+int32_t nvs_storage_get_free_space();
+
#ifdef __cplusplus
}
diff --git a/zephyr/modules/owntech_spin_api/zephyr/CMakeLists.txt b/zephyr/modules/owntech_spin_api/zephyr/CMakeLists.txt
index e6abe3ba..623cd1b6 100644
--- a/zephyr/modules/owntech_spin_api/zephyr/CMakeLists.txt
+++ b/zephyr/modules/owntech_spin_api/zephyr/CMakeLists.txt
@@ -18,6 +18,7 @@ if(CONFIG_OWNTECH_SPIN_API)
src/DacHAL.cpp
src/DataAPI.cpp
src/LedHAL.cpp
+ src/MetaDataAPI.cpp
src/PwmHAL.cpp
src/TimerHAL.cpp
)
diff --git a/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.cpp b/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.cpp
index 72fc44dc..32393c47 100644
--- a/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.cpp
+++ b/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.cpp
@@ -40,6 +40,8 @@ DacHAL SpinAPI::dac;
DataAPI SpinAPI::data;
+MetaDataAPI SpinAPI::metaData;
+
CompHAL SpinAPI::comp;
PwmHAL SpinAPI::pwm;
diff --git a/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.h b/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.h
index 7974c90e..509911dd 100644
--- a/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.h
+++ b/zephyr/modules/owntech_spin_api/zephyr/public_api/SpinAPI.h
@@ -34,6 +34,7 @@
#include "../src/DataAPI.h"
#include "../src/GpioHAL.h"
#include "../src/LedHAL.h"
+#include "../src/MetaDataAPI.h"
#include "../src/PwmHAL.h"
#include "../src/TimerHAL.h"
@@ -99,6 +100,12 @@ class SpinAPI
*/
static DataAPI data;
+ /**
+ * @brief Persist board/shield identity data (serial numbers,
+ * versions, shield password, extra slots) to flash.
+ */
+ static MetaDataAPI metaData;
+
};
diff --git a/zephyr/modules/owntech_spin_api/zephyr/src/MetaDataAPI.cpp b/zephyr/modules/owntech_spin_api/zephyr/src/MetaDataAPI.cpp
new file mode 100644
index 00000000..bb47a54d
--- /dev/null
+++ b/zephyr/modules/owntech_spin_api/zephyr/src/MetaDataAPI.cpp
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2026-present LAAS-CNRS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: LGPL-2.1
+ */
+
+/*
+ * @date 2026
+ *
+ * @author Luiz Villa
+ */
+
+
+/* Current class header */
+#include "MetaDataAPI.h"
+
+/* Low-level flash storage API */
+#include "nvs_storage.h"
+
+
+/**
+ * Sub-addressing of the individual metadata fields within the
+ * BOARD_METADATA NVS category, following the same bit-packing convention
+ * used for ADC_CALIBRATION in data_conversion.cpp: the category occupies
+ * the upper byte of the 16-bit NVS key, the field id the lower byte.
+ */
+enum : uint16_t
+{
+ META_SPIN_SERIAL = 0x00,
+ META_SHIELD_SERIAL = 0x01,
+ META_SPIN_VERSION = 0x02,
+ META_SHIELD_VERSION = 0x03,
+ META_SHIELD_PASSWORD = 0x04,
+ META_EXTRA_0 = 0x05,
+};
+
+static const uint16_t VERSION_FIELD_LEN = 3; /* major, minor, rev */
+
+int8_t MetaDataAPI::setSpinSerialNumber(const char* serial, uint8_t serial_size)
+{
+ if (serial_size != SPIN_SERIAL_LEN)
+ {
+ return -2;
+ }
+
+ int ret = nvs_storage_store_data(BOARD_METADATA | META_SPIN_SERIAL,
+ serial, SPIN_SERIAL_LEN);
+ return (ret < 0) ? -1 : 0;
+}
+
+int8_t MetaDataAPI::getSpinSerialNumber(char* buffer, uint8_t buffer_size)
+{
+ if (buffer_size < SPIN_SERIAL_LEN)
+ {
+ return -2;
+ }
+
+ int ret = nvs_storage_retrieve_data(BOARD_METADATA | META_SPIN_SERIAL,
+ buffer, buffer_size);
+ return (ret < 0) ? -1 : ret;
+}
+
+int8_t MetaDataAPI::setShieldSerialNumber(const char* serial, uint8_t serial_size)
+{
+ if (serial_size != SHIELD_SERIAL_LEN)
+ {
+ return -2;
+ }
+
+ int ret = nvs_storage_store_data(BOARD_METADATA | META_SHIELD_SERIAL,
+ serial, SHIELD_SERIAL_LEN);
+ return (ret < 0) ? -1 : 0;
+}
+
+int8_t MetaDataAPI::getShieldSerialNumber(char* buffer, uint8_t buffer_size)
+{
+ if (buffer_size < SHIELD_SERIAL_LEN)
+ {
+ return -2;
+ }
+
+ int ret = nvs_storage_retrieve_data(BOARD_METADATA | META_SHIELD_SERIAL,
+ buffer, buffer_size);
+ return (ret < 0) ? -1 : ret;
+}
+
+int8_t MetaDataAPI::setSpinVersion(uint8_t major, uint8_t minor, uint8_t rev)
+{
+ uint8_t version[VERSION_FIELD_LEN] = {major, minor, rev};
+
+ int ret = nvs_storage_store_data(BOARD_METADATA | META_SPIN_VERSION,
+ version, VERSION_FIELD_LEN);
+ return (ret < 0) ? -1 : 0;
+}
+
+int8_t MetaDataAPI::getSpinVersion(uint8_t* major, uint8_t* minor, uint8_t* rev)
+{
+ uint8_t version[VERSION_FIELD_LEN];
+
+ int ret = nvs_storage_retrieve_data(BOARD_METADATA | META_SPIN_VERSION,
+ version, VERSION_FIELD_LEN);
+ if (ret < 0)
+ {
+ return -1;
+ }
+
+ *major = version[0];
+ *minor = version[1];
+ *rev = version[2];
+ return 0;
+}
+
+int8_t MetaDataAPI::setShieldVersion(uint8_t major, uint8_t minor, uint8_t rev)
+{
+ uint8_t version[VERSION_FIELD_LEN] = {major, minor, rev};
+
+ int ret = nvs_storage_store_data(BOARD_METADATA | META_SHIELD_VERSION,
+ version, VERSION_FIELD_LEN);
+ return (ret < 0) ? -1 : 0;
+}
+
+int8_t MetaDataAPI::getShieldVersion(uint8_t* major, uint8_t* minor, uint8_t* rev)
+{
+ uint8_t version[VERSION_FIELD_LEN];
+
+ int ret = nvs_storage_retrieve_data(BOARD_METADATA | META_SHIELD_VERSION,
+ version, VERSION_FIELD_LEN);
+ if (ret < 0)
+ {
+ return -1;
+ }
+
+ *major = version[0];
+ *minor = version[1];
+ *rev = version[2];
+ return 0;
+}
+
+int8_t MetaDataAPI::setShieldPassword(const char* password, uint8_t password_size)
+{
+ if (password_size != SHIELD_PASSWORD_LEN)
+ {
+ return -2;
+ }
+
+ int ret = nvs_storage_store_data(BOARD_METADATA | META_SHIELD_PASSWORD,
+ password, SHIELD_PASSWORD_LEN);
+ return (ret < 0) ? -1 : 0;
+}
+
+int8_t MetaDataAPI::getShieldPassword(char* buffer, uint8_t buffer_size)
+{
+ if (buffer_size < SHIELD_PASSWORD_LEN)
+ {
+ return -2;
+ }
+
+ int ret = nvs_storage_retrieve_data(BOARD_METADATA | META_SHIELD_PASSWORD,
+ buffer, buffer_size);
+ return (ret < 0) ? -1 : ret;
+}
+
+int8_t MetaDataAPI::setExtraData(uint8_t index, const uint8_t* data, uint8_t data_size)
+{
+ if (index >= METADATA_EXTRA_COUNT)
+ {
+ return -3;
+ }
+
+ if (data_size > METADATA_EXTRA_MAX_LEN)
+ {
+ return -2;
+ }
+
+ uint16_t field_id = BOARD_METADATA | (META_EXTRA_0 + index);
+ int ret = nvs_storage_store_data(field_id, data, data_size);
+ return (ret < 0) ? -1 : 0;
+}
+
+int8_t MetaDataAPI::getExtraData(uint8_t index, uint8_t* buffer, uint8_t buffer_size)
+{
+ if (index >= METADATA_EXTRA_COUNT)
+ {
+ return -3;
+ }
+
+ if (buffer_size < METADATA_EXTRA_MAX_LEN)
+ {
+ return -2;
+ }
+
+ uint16_t field_id = BOARD_METADATA | (META_EXTRA_0 + index);
+ int ret = nvs_storage_retrieve_data(field_id, buffer, buffer_size);
+ return (ret < 0) ? -1 : ret;
+}
+
+int8_t MetaDataAPI::clearAllMetaData()
+{
+ static const uint16_t metadata_field_ids[] = {
+ META_SPIN_SERIAL,
+ META_SHIELD_SERIAL,
+ META_SPIN_VERSION,
+ META_SHIELD_VERSION,
+ META_SHIELD_PASSWORD,
+ META_EXTRA_0 + 0,
+ META_EXTRA_0 + 1,
+ META_EXTRA_0 + 2,
+ META_EXTRA_0 + 3,
+ META_EXTRA_0 + 4,
+ };
+
+ /* Writing with a size of 0 deletes the entry (see Zephyr's
+ * nvs_write() documentation), without touching any other module's
+ * data in the shared NVS partition. */
+ uint8_t dummy = 0;
+ int8_t result = 0;
+ for (uint16_t field_id : metadata_field_ids)
+ {
+ int ret = nvs_storage_store_data(BOARD_METADATA | field_id, &dummy, 0);
+ if (ret < 0)
+ {
+ result = -1;
+ }
+ }
+
+ return result;
+}
diff --git a/zephyr/modules/owntech_spin_api/zephyr/src/MetaDataAPI.h b/zephyr/modules/owntech_spin_api/zephyr/src/MetaDataAPI.h
new file mode 100644
index 00000000..fb79e141
--- /dev/null
+++ b/zephyr/modules/owntech_spin_api/zephyr/src/MetaDataAPI.h
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2026-present LAAS-CNRS
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ *
+ * SPDX-License-Identifier: LGPL-2.1
+ */
+
+/*
+ * @date 2026
+ *
+ * @author Luiz Villa
+ */
+
+
+#ifndef METADATAAPI_H_
+#define METADATAAPI_H_
+
+
+/* Stdlib */
+#include
+
+/**
+ * Constants definitions
+ */
+
+/* Fixed length, in bytes, of a serial number field (13 raw ASCII
+ * characters, not null-terminated). */
+static const uint8_t SPIN_SERIAL_LEN = 13;
+static const uint8_t SHIELD_SERIAL_LEN = 13;
+
+/* Fixed length, in bytes, of the shield password field (3 raw ASCII
+ * characters, not null-terminated). */
+static const uint8_t SHIELD_PASSWORD_LEN = 3;
+
+/* Number of generic extra metadata slots, and max size of each. */
+static const uint8_t METADATA_EXTRA_COUNT = 5;
+static const uint8_t METADATA_EXTRA_MAX_LEN = 16;
+
+/**
+ * Class definition
+ */
+
+/**
+ * @brief Persist board/shield identity data (serial numbers, versions,
+ * shield password, and generic extra slots) to flash.
+ *
+ * @note This is NOT secure storage: data is written in plain form to the
+ * NVS partition, with no secure element or read-protection involved.
+ * The shield password field is only meant to gate casual shield
+ * mismatches, not to protect a real secret.
+ */
+class MetaDataAPI
+{
+public:
+
+ /**
+ * @brief Store the Spin board serial number in persistent memory.
+ *
+ * @param[in] serial Pointer to a buffer of exactly
+ * `SPIN_SERIAL_LEN` (13) raw ASCII bytes. The
+ * buffer does not need to be null-terminated,
+ * and no null terminator is written; if fewer
+ * than 13 meaningful characters are needed, the
+ * caller must pad the buffer itself (e.g. with
+ * spaces or zeros) up to 13 bytes.
+ * @param[in] serial_size Size of `serial` in bytes, must be exactly
+ * `SPIN_SERIAL_LEN` (13).
+ *
+ * @return `0` if the serial number was correctly stored, negative
+ * value on error:
+ *
+ * - `-1`: underlying storage error,
+ *
+ * - `-2`: `serial_size` is not exactly `SPIN_SERIAL_LEN`.
+ */
+ int8_t setSpinSerialNumber(const char* serial, uint8_t serial_size);
+
+ /**
+ * @brief Retrieve the Spin board serial number from persistent memory.
+ *
+ * @param[in] buffer Buffer to receive the 13 raw ASCII bytes.
+ * @param[in] buffer_size Size of `buffer`, must be at least
+ * `SPIN_SERIAL_LEN` (13).
+ *
+ * @return Number of bytes read (13) on success, negative value on error:
+ *
+ * - `-1`: underlying storage error (nothing stored yet, corrupted
+ * data, or version mismatch),
+ *
+ * - `-2`: provided buffer is smaller than `SPIN_SERIAL_LEN`.
+ */
+ int8_t getSpinSerialNumber(char* buffer, uint8_t buffer_size);
+
+ /**
+ * @brief Store the shield serial number in persistent memory.
+ *
+ * @param[in] serial Pointer to a buffer of exactly
+ * `SHIELD_SERIAL_LEN` (13) raw ASCII bytes.
+ * Same padding rules as `setSpinSerialNumber()`.
+ * @param[in] serial_size Size of `serial` in bytes, must be exactly
+ * `SHIELD_SERIAL_LEN` (13).
+ *
+ * @return `0` if the serial number was correctly stored, negative
+ * value on error:
+ *
+ * - `-1`: underlying storage error,
+ *
+ * - `-2`: `serial_size` is not exactly `SHIELD_SERIAL_LEN`.
+ */
+ int8_t setShieldSerialNumber(const char* serial, uint8_t serial_size);
+
+ /**
+ * @brief Retrieve the shield serial number from persistent memory.
+ *
+ * @param[in] buffer Buffer to receive the 13 raw ASCII bytes.
+ * @param[in] buffer_size Size of `buffer`, must be at least
+ * `SHIELD_SERIAL_LEN` (13).
+ *
+ * @return Number of bytes read (13) on success, negative value on error:
+ *
+ * - `-1`: underlying storage error,
+ *
+ * - `-2`: provided buffer is smaller than `SHIELD_SERIAL_LEN`.
+ */
+ int8_t getShieldSerialNumber(char* buffer, uint8_t buffer_size);
+
+ /**
+ * @brief Store the Spin board hardware version in persistent memory.
+ *
+ * @param[in] major Major version number.
+ * @param[in] minor Minor version number.
+ * @param[in] rev Revision number.
+ *
+ * @return `0` if the version was correctly stored,
+ * `-1` if there was an underlying storage error.
+ */
+ int8_t setSpinVersion(uint8_t major, uint8_t minor, uint8_t rev);
+
+ /**
+ * @brief Retrieve the Spin board hardware version from persistent
+ * memory.
+ *
+ * @param[out] major Pointer to receive the major version number.
+ * @param[out] minor Pointer to receive the minor version number.
+ * @param[out] rev Pointer to receive the revision number.
+ *
+ * @return `0` if the version was correctly retrieved,
+ * `-1` if there was an underlying storage error (nothing
+ * stored yet, corrupted data, or version mismatch).
+ */
+ int8_t getSpinVersion(uint8_t* major, uint8_t* minor, uint8_t* rev);
+
+ /**
+ * @brief Store the shield hardware version in persistent memory.
+ *
+ * @param[in] major Major version number.
+ * @param[in] minor Minor version number.
+ * @param[in] rev Revision number.
+ *
+ * @return `0` if the version was correctly stored,
+ * `-1` if there was an underlying storage error.
+ */
+ int8_t setShieldVersion(uint8_t major, uint8_t minor, uint8_t rev);
+
+ /**
+ * @brief Retrieve the shield hardware version from persistent memory.
+ *
+ * @param[out] major Pointer to receive the major version number.
+ * @param[out] minor Pointer to receive the minor version number.
+ * @param[out] rev Pointer to receive the revision number.
+ *
+ * @return `0` if the version was correctly retrieved,
+ * `-1` if there was an underlying storage error.
+ */
+ int8_t getShieldVersion(uint8_t* major, uint8_t* minor, uint8_t* rev);
+
+ /**
+ * @brief Store the shield password in persistent memory.
+ *
+ * @note This is not a secure secret store: the password is written in
+ * plain form to flash, retrievable via `getShieldPassword()`.
+ * It is only meant to gate casual shield/board mismatches.
+ *
+ * @param[in] password Pointer to a buffer of exactly
+ * `SHIELD_PASSWORD_LEN` (3) raw ASCII bytes.
+ * @param[in] password_size Size of `password` in bytes, must be
+ * exactly `SHIELD_PASSWORD_LEN` (3).
+ *
+ * @return `0` if the password was correctly stored, negative value
+ * on error:
+ *
+ * - `-1`: underlying storage error,
+ *
+ * - `-2`: `password_size` is not exactly `SHIELD_PASSWORD_LEN`.
+ */
+ int8_t setShieldPassword(const char* password, uint8_t password_size);
+
+ /**
+ * @brief Retrieve the shield password from persistent memory.
+ *
+ * @param[in] buffer Buffer to receive the 3 raw ASCII bytes.
+ * @param[in] buffer_size Size of `buffer`, must be at least
+ * `SHIELD_PASSWORD_LEN` (3).
+ *
+ * @return Number of bytes read (3) on success, negative value on error:
+ *
+ * - `-1`: underlying storage error,
+ *
+ * - `-2`: provided buffer is smaller than `SHIELD_PASSWORD_LEN`.
+ */
+ int8_t getShieldPassword(char* buffer, uint8_t buffer_size);
+
+ /**
+ * @brief Store one of the generic extra metadata slots.
+ *
+ * @param[in] index Slot index, in `[0, METADATA_EXTRA_COUNT - 1]`.
+ * @param[in] data Pointer to the data to store.
+ * @param[in] data_size Size of `data` in bytes, at most
+ * `METADATA_EXTRA_MAX_LEN` (16).
+ *
+ * @return `0` if the data was correctly stored, negative value on error:
+ *
+ * - `-1`: underlying storage error,
+ *
+ * - `-2`: `data_size` exceeds `METADATA_EXTRA_MAX_LEN`,
+ *
+ * - `-3`: `index` is out of range.
+ */
+ int8_t setExtraData(uint8_t index, const uint8_t* data, uint8_t data_size);
+
+ /**
+ * @brief Retrieve one of the generic extra metadata slots.
+ *
+ * @param[in] index Slot index, in `[0, METADATA_EXTRA_COUNT - 1]`.
+ * @param[in] buffer Buffer to receive the stored data.
+ * @param[in] buffer_size Size of `buffer` in bytes, must be at least
+ * `METADATA_EXTRA_MAX_LEN` (16), since the
+ * actual stored size for this slot is not
+ * known ahead of the read.
+ *
+ * @return Number of bytes read on success, negative value on error:
+ *
+ * - `-1`: underlying storage error,
+ *
+ * - `-2`: `buffer_size` is smaller than `METADATA_EXTRA_MAX_LEN`,
+ *
+ * - `-3`: `index` is out of range.
+ */
+ int8_t getExtraData(uint8_t index, uint8_t* buffer, uint8_t buffer_size);
+
+ /**
+ * @brief Erase all board/shield metadata fields (serial numbers,
+ * versions, password, and all extra slots).
+ *
+ * @note This only erases the metadata fields owned by this class. It
+ * does not affect ADC calibration data or safety thresholds,
+ * which live under different NVS categories in the same
+ * partition.
+ *
+ * @return `0` if all fields were correctly erased,
+ * `-1` if there was an underlying storage error.
+ */
+ int8_t clearAllMetaData();
+
+};
+
+
+#endif /* METADATAAPI_H_ */