Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
---
## Unreleased

* Zephyr `zephyr-image` update module: optional RAM staging before secondary-slot
write (XIP-safe on FlexSPI), with `CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE` and
`CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES`, `mender_malloc` staging
buffer, direct-to-flash fallback, and unit tests for the staging helpers.
* ESP-IDF: fix `MENDER_STORAGE_PARTITION_LABEL` compile definition mismatch in
`component.cmake`; grow log formatting beyond a fixed 256-byte stack buffer.

## 1.0.0 - 2026-04-17

* The first stable release
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
> Machine SoT: [`DD_PIN`](./DD_PIN) · tag `dd-pin-64c10fa` ·
> [`scripts/check-consumer-pin.sh`](./scripts/check-consumer-pin.sh) ·
> [`scripts/bump-consumer-pins.sh`](./scripts/bump-consumer-pins.sh).
>
> Pin scripts accept `MENDER_MCU_PIN_BRANCH`, `DD_ROOT`, and
> `MENDER_MCU_CONSUMERS` overrides (see script headers).


## Overview
Expand Down Expand Up @@ -32,6 +35,20 @@ The decision to fork the original mender-mcu-client was made to:
* **Provide Official Support**: Ensure that the project receives the necessary attention and
resources from Northern.tech to meet the needs of the community and enterprise users.

## Zephyr RAM staging (optional)

When `CONFIG_MENDER_ZEPHYR_IMAGE_UPDATE_MODULE` is enabled, the default
`CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE` path accumulates the full artifact payload
in RAM (via `mender_malloc`) during download, then writes the secondary slot in
one pass at close. That avoids FlexSPI XIP stalls when the running image, OTA
slot, and MCUboot share the same NOR (for example i.MX RT).

- Disable with `CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE=n` for direct-to-flash writes.
- Cap staging with `CONFIG_MENDER_ZEPHYR_IMAGE_RAM_STAGE_MAX_BYTES` (0 = no cap).
- If allocation fails or the image exceeds the cap, the module falls back to
writing each chunk directly to flash. MCUboot still verifies the image on
boot; staging does not bypass signature checks.

## Get started

This guide is based on our Zephyr reference application [mender-mcu-integration](https://github.com/mendersoftware/mender-mcu-integration).
Expand Down
1 change: 1 addition & 0 deletions cmake/mender_mcu_sources.txt
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ file(GLOB MENDER_MCU_SOURCES
"${MENDER_MCU_ROOT}/src/core/client.c"
"${MENDER_MCU_ROOT}/src/core/deployment-data.c"
"${MENDER_MCU_ROOT}/src/core/error-counters.c"
"${MENDER_MCU_ROOT}/src/core/image-ram-stage.c"
"${MENDER_MCU_ROOT}/src/core/update-module.c"
"${MENDER_MCU_ROOT}/src/core/utils.c"
"${MENDER_MCU_ROOT}/src/platform/log/${CONFIG_MENDER_PLATFORM_LOG_TYPE}/log.c"
Expand Down
22 changes: 17 additions & 5 deletions scripts/bump-consumer-pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
# ./scripts/bump-consumer-pins.sh <full-40-char-sha> # dry-run
# ./scripts/bump-consumer-pins.sh <full-40-char-sha> --apply # write files
#
# Expects sibling checkouts under DD_ROOT (default /data_drive/dd):
# zephyr-rt1186-f1
# zephyr-rt1170-room-display
# zephyr-rt1170-eink
# Env:
# DD_ROOT parent of consumer checkouts (default: /data_drive/dd)
# MENDER_MCU_CONSUMERS override consumer list as "repo|rel[,repo|rel...]"
#
# Default consumers (under DD_ROOT):
# zephyr-rt1186-f1|f1-controller/west.yml
# zephyr-rt1170-room-display|room-display/west.yml
# zephyr-rt1170-eink|mender-mcu-integration/west.yml
#
# Does not git commit/push — review, then commit each repo (and retag dd-pin-*).
set -euo pipefail
Expand All @@ -27,15 +31,23 @@ SHORT=${NEW:0:7}
ROOT=$(cd "$(dirname "$0")/.." && pwd)
DD_ROOT=${DD_ROOT:-/data_drive/dd}

CONSUMERS=(
DEFAULT_CONSUMERS=(
"zephyr-rt1186-f1|f1-controller/west.yml"
"zephyr-rt1170-room-display|room-display/west.yml"
"zephyr-rt1170-eink|mender-mcu-integration/west.yml"
)

CONSUMERS=()
if [[ -n "${MENDER_MCU_CONSUMERS:-}" ]]; then
IFS=',' read -r -a CONSUMERS <<<"$MENDER_MCU_CONSUMERS"
else
CONSUMERS=("${DEFAULT_CONSUMERS[@]}")
fi

OLD=$(tr -d '[:space:]' < "$ROOT/DD_PIN" || true)
echo "mender-mcu pin: ${OLD:-"(none)"} → $NEW"
echo "tag suggestion: dd-pin-$SHORT (annotate at $NEW)"
echo "DD_ROOT=$DD_ROOT"
echo

replace_revision() {
Expand Down
8 changes: 6 additions & 2 deletions scripts/check-consumer-pin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
# curl -fsSL …/scripts/check-consumer-pin.sh | bash -s -- f1-controller/west.yml
#
# Env:
# MENDER_MCU_PIN_BRANCH branch hosting DD_PIN / PIN-POLICY (default: feature/zephyr-ram-stage-on-main)
# MENDER_MCU_DD_PIN_URL override raw DD_PIN URL
# MENDER_MCU_DD_PIN_FILE read pin from this file instead of URL
# MENDER_MCU_PIN_POLICY_URL override policy URL shown on drift (optional)
set -euo pipefail

WEST_YML=${1:?usage: check-consumer-pin.sh <west.yml>}
PIN_URL=${MENDER_MCU_DD_PIN_URL:-https://raw.githubusercontent.com/DynamicDevices/mender-mcu/feature/zephyr-ram-stage-on-main/DD_PIN}
PIN_BRANCH=${MENDER_MCU_PIN_BRANCH:-feature/zephyr-ram-stage-on-main}
PIN_URL=${MENDER_MCU_DD_PIN_URL:-https://raw.githubusercontent.com/DynamicDevices/mender-mcu/${PIN_BRANCH}/DD_PIN}
POLICY_URL=${MENDER_MCU_PIN_POLICY_URL:-https://github.com/DynamicDevices/mender-mcu/blob/${PIN_BRANCH}/PIN-POLICY.md}

if [[ ! -f "$WEST_YML" ]]; then
echo "error: west.yml not found: $WEST_YML" >&2
Expand Down Expand Up @@ -70,7 +74,7 @@ if [[ "$ACTUAL" != "$EXPECTED" ]]; then
echo "error: mender-mcu pin drift" >&2
echo " west.yml ($WEST_YML): $ACTUAL" >&2
echo " DD_PIN ($PIN_SRC): $EXPECTED" >&2
echo " policy: https://github.com/DynamicDevices/mender-mcu/blob/feature/zephyr-ram-stage-on-main/PIN-POLICY.md" >&2
echo " policy: $POLICY_URL" >&2
exit 5
fi

Expand Down
80 changes: 80 additions & 0 deletions src/core/image-ram-stage.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @file image-ram-stage.c
* @brief Whole-image RAM staging helpers (platform-independent)
*
* Copyright Northern.tech AS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include <string.h>

#include "alloc.h"
#include "image-ram-stage.h"

mender_err_t
mender_image_ram_stage_begin(mender_image_ram_stage_t *stage, size_t size, bool enabled, size_t max_size) {
if (NULL == stage) {
return MENDER_FAIL;
}

mender_image_ram_stage_reset(stage);

if (!enabled || 0 == size) {
return MENDER_OK;
}
if ((max_size > 0) && (size > max_size)) {
return MENDER_OK;
}

stage->buf = mender_malloc(size);
if (NULL == stage->buf) {
return MENDER_OK;
}
stage->capacity = size;
stage->length = 0;
return MENDER_OK;
}

bool
mender_image_ram_stage_active(const mender_image_ram_stage_t *stage) {
return (NULL != stage) && (NULL != stage->buf);
}

mender_err_t
mender_image_ram_stage_write(mender_image_ram_stage_t *stage, const void *data, size_t index, size_t length) {
if ((NULL == stage) || (NULL == stage->buf) || (NULL == data)) {
return MENDER_FAIL;
}
if (index + length > stage->capacity) {
return MENDER_FAIL;
}
memcpy(stage->buf + index, data, length);
if (index + length > stage->length) {
stage->length = index + length;
}
return MENDER_OK;
}

void
mender_image_ram_stage_reset(mender_image_ram_stage_t *stage) {
if (NULL == stage) {
return;
}
if (NULL != stage->buf) {
mender_free(stage->buf);
}
stage->buf = NULL;
stage->capacity = 0;
stage->length = 0;
}
66 changes: 66 additions & 0 deletions src/include/image-ram-stage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @file image-ram-stage.h
* @brief Whole-image RAM staging helpers (platform-independent)
*
* Copyright Northern.tech AS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#ifndef __MENDER_IMAGE_RAM_STAGE_PRIV_H__
#define __MENDER_IMAGE_RAM_STAGE_PRIV_H__

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

#include <mender/utils.h>

typedef struct mender_image_ram_stage {
uint8_t *buf;
size_t capacity;
size_t length;
} mender_image_ram_stage_t;

/**
* @brief Try to allocate a staging buffer for @p size bytes
* @param stage Staging state (must not be NULL)
* @param size Artifact payload size
* @param enabled When false, leave stage inactive (direct-to-flash)
* @param max_size When > 0 and size > max_size, leave stage inactive
* @return MENDER_OK always (inactive stage is a valid outcome); MENDER_FAIL on bad args
*
* Allocation uses mender_malloc. Failure to allocate leaves the stage inactive.
*/
mender_err_t mender_image_ram_stage_begin(mender_image_ram_stage_t *stage, size_t size, bool enabled, size_t max_size);

bool mender_image_ram_stage_active(const mender_image_ram_stage_t *stage);

/**
* @brief Copy a download chunk into the staging buffer
* @return MENDER_OK, or MENDER_FAIL on overflow / inactive stage / bad args
*/
mender_err_t mender_image_ram_stage_write(mender_image_ram_stage_t *stage, const void *data, size_t index, size_t length);

/** Free staging buffer and clear state. */
void mender_image_ram_stage_reset(mender_image_ram_stage_t *stage);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __MENDER_IMAGE_RAM_STAGE_PRIV_H__ */
44 changes: 40 additions & 4 deletions src/platform/log/esp-idf/log.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,54 @@

#include "esp_log.h"

#include "alloc.h"
#include "log.h"

/* Cap heap-backed messages so a runaway format cannot exhaust memory. */
#define MENDER_ESP_LOG_MAX_MSG 2048

mender_err_t
mender_log_init(void) {
/* Nothing to do */
return MENDER_OK;
}

mender_err_t
mender_log_print(uint8_t level, MENDER_ARG_UNUSED const char *filename, const char *function, int line, char *format, ...) {
char msg[256] = "";
mender_log_print(uint8_t level, const char *filename, const char *function, int line, char *format, ...) {
char stack_msg[256];
char *msg = stack_msg;
char *heap_msg = NULL;
va_list args;
va_list args_copy;
int needed;

va_start(args, format);
vsnprintf(msg, sizeof(msg), format, args);
va_copy(args_copy, args);
needed = vsnprintf(NULL, 0, format, args_copy);
va_end(args_copy);

if (needed < 0) {
va_end(args);
return MENDER_FAIL;
}

if ((size_t)needed + 1 > sizeof(stack_msg)) {
size_t alloc_len = (size_t)needed + 1;
if (alloc_len > MENDER_ESP_LOG_MAX_MSG) {
alloc_len = MENDER_ESP_LOG_MAX_MSG;
}
heap_msg = mender_malloc(alloc_len);
if (NULL != heap_msg) {
msg = heap_msg;
vsnprintf(msg, alloc_len, format, args);
} else {
/* Fall back to truncated stack buffer if heap is exhausted. */
vsnprintf(stack_msg, sizeof(stack_msg), format, args);
msg = stack_msg;
}
} else {
vsnprintf(stack_msg, sizeof(stack_msg), format, args);
}
va_end(args);

esp_log_level_t esp_level;
Expand All @@ -55,8 +89,10 @@ mender_log_print(uint8_t level, MENDER_ARG_UNUSED const char *filename, const ch
break;
}

ESP_LOG_LEVEL(esp_level, "mender", "%s:%d: %s", function, line, msg);
/* Include source filename in the tag path so it is not dropped. */
ESP_LOG_LEVEL(esp_level, "mender", "%s:%s:%d: %s", filename ? filename : "?", function, line, msg);

mender_free(heap_msg);
return MENDER_OK;
}

Expand Down
Loading