Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
19 changes: 18 additions & 1 deletion .github/workflows/ci.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,7 @@ env:
PIOARDUINO_PLATFORM_URL: https://github.com/pioarduino/platform-espressif32/releases/download/55.03.39/platform-espressif32.zip
PIOARDUINO_PLATFORM_VERSION: 55.03.39
PIOARDUINO_VERSION: 6.1.19
STRATA_VERSION: v0.1.1

jobs:
source-audit:
Expand All@@ -25,11 +26,22 @@ jobs:

- name: Audit production sources
run: |
set -e
if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then
echo "Embedded safety audit failed"
exit 1
fi

if grep -RInE 'heap_caps_|MALLOC_CAP_|ps_malloc|xTaskCreate|vTaskDelete|xQueueCreate|xSemaphoreCreate|std::make_unique|std::make_shared|(^|[^[:alnum:]_])malloc[[:space:]]*\(|(^|[^[:alnum:]_])calloc[[:space:]]*\(|(^|[^[:alnum:]_])realloc[[:space:]]*\(|(^|[^[:alnum:]_])free[[:space:]]*\(|(^|[^[:alnum:]_])new[[:space:](]|(^|[^[:alnum:]_])delete[[:space:](]' src; then
echo "Worker allocations and owned FreeRTOS primitives must route through Strata"
exit 1
fi

if grep -RInE '#include[[:space:]]+[<"]esp_heap_caps\.h[>"]|freertos/idf_additions\.h' src; then
echo "Worker must not depend on ESP-IDF allocation internals"
exit 1
fi

build-examples:
runs-on: ubuntu-latest
needs: source-audit
Expand DownExpand Up@@ -72,6 +84,7 @@ jobs:
--board ${{ matrix.board }} \
--lib="." \
--project-option "platform=${PIOARDUINO_PLATFORM_URL}" \
--project-option "lib_deps=https://github.com/ZekStack/strata.git#${STRATA_VERSION}" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++20"
fi
Expand DownExpand Up@@ -152,12 +165,16 @@ jobs:
arduino-cli core update-index
arduino-cli core install "esp32:esp32@${ESP32_CORE_VERSION}"

- name: Add local library to sketchbook
- name: Add local libraries to sketchbook
run: |
set -e
SKETCHBOOK_DIR="${HOME}/Arduino"
mkdir -p "$SKETCHBOOK_DIR/libraries/Worker"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Worker/"
rm -rf "$SKETCHBOOK_DIR/libraries/Strata"
git clone --depth 1 --branch "${STRATA_VERSION}" \
https://github.com/ZekStack/strata.git \
"$SKETCHBOOK_DIR/libraries/Strata"

- name: Build examples (${{ matrix.board.name }})
env:
Expand Down
141 changes: 106 additions & 35 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,7 @@

Worker is a FreeRTOS task and cooperative job execution library for ESP32.

Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. It is designed for products that need predictable task behavior without spreading raw FreeRTOS task management across the application.
Worker runs one-off and recurring background work with explicit task configuration, cooperative stop and sleep controls, event reporting, runtime diagnostics, and automatic task cleanup. Worker owns job orchestration and lifecycle policy while [Strata](https://github.com/ZekStack/strata) owns memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/worker/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/worker/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/worker?sort=semver)](https://github.com/ZekStack/worker/releases)
Expand All@@ -12,13 +12,16 @@ Worker runs one-off and recurring background work with explicit task configurati

- **Task-per-job execution** — each `once()` and `every()` job owns a FreeRTOS task.
- **Automatic cleanup** — callers never need to reap completed jobs.
- **Correct PSRAM teardown** — capability-created tasks are externally deleted with `vTaskDeleteWithCaps()`.
- **Consistent memory policy** — `Strata::MemoryPolicy` controls ordinary Worker allocations and task-stack placement.
- **Portable placement** — use `Default`, `Internal`, `PreferExternal`, and `RequireExternal` instead of Worker-specific PSRAM enums.
- **Strata-owned FreeRTOS storage** — task stacks, task control blocks, cleanup queue storage, and mutex control storage use Strata ownership primitives.
- **Safe recurring jobs** — `every()` applies the interval after each callback.
- **ESP32 task control** — configure byte stack size, priority, core affinity, and stack memory preference.
- **Cooperative lifecycle** — jobs can stop or sleep through `WorkerJobContext`.
- **Runtime visibility** — current job and cleanup-task diagnostics without retained job history.
- **Runtime visibility** — diagnostics expose requested stack placement and observed memory regions.

## Install
## Dependency

Worker `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All@@ -37,14 +40,19 @@ build_unflags =
-std=gnu++11
```

Worker's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it as a transitive dependency.

### Arduino IDE

Worker is not published to Arduino Library Manager yet. Install it by downloading the repository ZIP or cloning it into the Arduino libraries directory.
Worker and Strata are not published to Arduino Library Manager yet. Install both repositories into the Arduino libraries directory:

```txt
```text
Arduino/libraries/Strata
Arduino/libraries/Worker
```

Use Strata `v0.1.1` or a compatible later release.

## Quick start

```cpp
Expand All@@ -59,7 +67,7 @@ void setup() {

WorkerResult initResult = worker.init();
if (!initResult) {
Serial.println(initResult.message.c_str());
Serial.println(initResult.message);
return;
}

Expand All@@ -84,23 +92,62 @@ void loop() {
}
```

No cleanup call is required after `once()` or `every()`. Worker releases callback captures, task stacks, task TCBs, and active job records automatically.
No cleanup call is required after `once()` or `every()`. Worker releases callback captures, Strata task stacks and TCBs, and active job records automatically.

## Memory policy

Worker uses the ZekStack-standard `Strata::MemoryPolicy` configuration shape:

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init(config);
```

`memory.allocation` controls movable Worker-owned storage such as job records, registry/completion container backing, and cleanup-queue item storage. `memory.taskStack` is the inherited default for job and cleanup task stacks.

Worker's default policy preserves the old `WorkerStackType::Auto` behavior:

```cpp
allocation = Strata::Placement::Default;
taskStack = Strata::Placement::PreferExternal;
```

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails instead of consuming internal memory.

A job can override only its own stack placement:

```cpp
WorkerJobConfig job;
job.stackPlacement = Strata::Placement::Internal;
worker.once(job, [](WorkerJobContext &) {});
```

`std::nullopt` means inherit `WorkerConfig::memory.taskStack`. `Strata::Placement::Default` never means inherit; it explicitly requests the Strata backend default.

The cleanup task can be overridden independently when needed:

```cpp
config.cleanupTaskStackPlacement = Strata::Placement::Internal;
```

## Cleanup model

Worker creates one long-lived internal cleanup task during `init()`.
Worker creates one long-lived cleanup task during `init()` using `Strata::FreeRTOS::Task` and a task-only `Strata::FreeRTOS::Queue`.

When a job callback finishes, the job:

1. releases its stored callback;
2. queues its handle and immutable allocation type;
3. suspends itself.
2. records its final state and stack high-water mark;
3. queues its job record to the cleanup task;
4. reaches the external-deletion handoff and suspends.

The cleanup task then deletes the job externally with the correct FreeRTOS API. Worker emits the completion event and removes the active record only after deletion returns.
The cleanup task then externally resets the job's `Strata::FreeRTOS::Task`. Strata deletes the FreeRTOS task and releases its placed stack and internal task control block. Worker records the completion token and removes the active job record only after that reset returns.

This avoids the ESP-IDF temporary-task path used when a capability-created task calls `vTaskDeleteWithCaps()` on itself.

`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical task cleanup; they do not perform cleanup.
`waitFor()` and `stopAndWait()` are optional synchronization APIs. They wait for physical cleanup; they do not perform cleanup.

## Important notes

Expand All@@ -110,49 +157,73 @@ This avoids the ESP-IDF temporary-task path used when a capability-created task
- A callback that blocks forever prevents timed `stopAndWait()` and `end()` calls from completing.
- The destructor waits without a timeout so tasks cannot outlive Worker internals.
- `every(intervalMs, callback)` delays after each callback.
- `WorkerStackType::Auto` prefers PSRAM task stacks when supported and falls back to internal RAM.
- Stack sizes are FreeRTOS byte sizes on ESP32 and must be at least 1024 bytes.
- Stack sizes remain FreeRTOS byte sizes on ESP32, must be at least 1024 bytes, and must be aligned to `sizeof(StackType_t)`.
- `maxConcurrentJobs` bounds active jobs and guarantees cleanup queue capacity.
- `clearFinished()` is retained only as a deprecated compatibility no-op.
- Completion synchronization uses a small bounded token window and never retains callbacks or full completed records.
- Worker APIs use result objects for normal failures. Catastrophic STL allocation failure is not recoverable on platforms where the standard library aborts.
- `WorkerResult::message` is a non-owning static status string in `v0.2.0`; use `result.message` directly.
- Worker no longer contains direct PSRAM allocation logic, capability-created task handling, or dynamic FreeRTOS queue/mutex creation.
- `std::function` remains the callback surface. Allocation performed by a caller while constructing a callback is outside Worker's owned allocation boundary.

## Diagnostics

`WorkerJobDiag` separates requested policy from actual storage:

```cpp
WorkerJobDiag diag;
if (worker.getJobDiagnostics(jobId, diag)) {
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion));
}
```

`WorkerDiag` also reports cleanup-task stack placement/region and cleanup-queue storage placement/region so applications can verify memory policy at runtime.

## Examples

| Example | Description |
| --- | --- |
| `Basic` | Minimal initialization, one-off job, recurring job, wait, and cooperative stop. |
| `JobConfig` | Stack size, priority, core affinity, internal stack, and PSRAM stack request. |
| `JobConfig` | Worker memory policy and per-job internal/required-external stack overrides. |
| `Events` | Event callback and error event handling. |
| `SleepAndWait` | Context sleep, external sleep, wait, and timeout behavior. |
| `Diagnostics` | Current job and cleanup-task diagnostics. |
| `Diagnostics` | Requested Strata placement and observed stack/cleanup regions. |
| `BindableCallbacks` | `std::bind` with private class methods. |
| `TaskCleanupSentinel` | Fire-and-forget capture, heap, PSRAM, and task-count cleanup checks. |
| `TaskCleanupSentinel` | Fire-and-forget capture, internal/external heap, and task-count cleanup checks under `PreferExternal`. |

Start with:

```txt
```text
examples/Basic
```

## Documentation

| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Job defaults and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API and cleanup semantics. |
| [`docs/getting-started.md`](docs/getting-started.md) | Setup, Strata dependency, and first jobs. |
| [`docs/configuration.md`](docs/configuration.md) | Worker memory policy, job defaults, and cleanup infrastructure. |
| [`docs/api.md`](docs/api.md) | Public API, placement diagnostics, and cleanup semantics. |
| [`docs/examples.md`](docs/examples.md) | Example descriptions. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle and configuration issues. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common lifecycle, placement, and configuration issues. |

## API overview

```cpp
WorkerConfig config;
config.memory.allocation = Strata::Placement::PreferExternal;
config.memory.taskStack = Strata::Placement::PreferExternal;

Worker worker;
worker.init();
worker.init(config);
worker.onEvent([](WorkerEvent event) {});

WorkerJobResult once = worker.once([](WorkerJobContext &ctx) {});
WorkerJobConfig jobConfig;
jobConfig.stackPlacement = Strata::Placement::Internal;

WorkerJobResult once = worker.once(jobConfig, [](WorkerJobContext &ctx) {});
WorkerJobResult loop = worker.every(1000, [](WorkerJobContext &ctx) {});

worker.sleep(loop.jobId, 5000);
Expand All@@ -170,16 +241,16 @@ worker.getJobDiagnostics(loop.jobId, jobDiag);
| Framework | Arduino ESP32 |
| Platform | `espressif32` / PIOArduino |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional task stacks through ESP-IDF capability APIs |
| Dependencies | none |
| Exceptions | Not used |
| Status | Early-stage `0.1.0` |
| Memory layer | Strata `v0.1.1` |
| External memory | Optional through Strata placement policies |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Worker APIs |
| Status | `v0.2.0` API |

## License

MIT — see [`LICENSE.md`](LICENSE.md).

## ZekStack

Part of the ZekStack ESP32 library stack.
Part of the ZekStack ESP32 library stack. Worker is the reference adoption of the shared Strata memory-policy contract for higher-level ZekStack libraries.
Loading
Loading