Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5191a2a
Adopt Strata memory policy in Phase API
zekageri Sep 6, 2026
0f4c4d2
Route Phase ownership through Strata
zekageri Sep 6, 2026
10b2420
Create Phase task through Strata
zekageri Sep 6, 2026
301a7af
Use external Strata task teardown handoff
zekageri Sep 6, 2026
802d3c9
Use Strata diagnostics and callback ownership
zekageri Sep 6, 2026
983d909
Place Phase graph nodes through Strata
zekageri Sep 6, 2026
d91d00c
Remove Phase-specific mutex wrapper
zekageri Sep 6, 2026
4e05d96
Remove Phase-specific task allocation support
zekageri Sep 6, 2026
be06632
Add host Strata API test double
zekageri Sep 6, 2026
008d1f7
Add host Strata mutex test double
zekageri Sep 6, 2026
12d9a9c
Add host Strata task test double
zekageri Sep 6, 2026
6460efe
Support external task teardown in host stubs
zekageri Sep 6, 2026
9e5c2bb
Model suspended Strata task teardown in host tests
zekageri Sep 6, 2026
7a9bdaa
Declare Strata dependency for Phase v0.2.0
zekageri Sep 6, 2026
0aad512
Bump Phase metadata to v0.2.0
zekageri Sep 6, 2026
e53c50f
Validate Strata-backed Phase builds
zekageri Sep 6, 2026
d378680
Test Phase Strata memory policy
zekageri Sep 6, 2026
b7aa061
Document Strata-backed Phase v0.2.0
zekageri Sep 6, 2026
8367729
Add Phase Strata memory policy example
zekageri Sep 6, 2026
71e06c1
Document Phase Strata memory configuration
zekageri Sep 6, 2026
82d2923
Document Strata-backed Phase API
zekageri Sep 6, 2026
577dd5b
Update Phase getting started for Strata
zekageri Sep 6, 2026
83b7ad7
Document Phase memory policy example
zekageri Sep 6, 2026
90740c5
Document Phase Strata placement troubleshooting
zekageri Sep 6, 2026
9cfa406
Fix PhasePlacedVector linkage in host builds
zekageri Sep 7, 2026
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: 17 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff 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 @@ -26,12 +27,21 @@ jobs:
- name: Audit production sources
shell: bash
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 '#include[[:space:]]+[<"](Fresh|Pulse|Signal|Tempo|Trace|Worker|Vault|Link|Courier|Flow|Lingo)\.h[>"]' src; then
echo "Embedded safety audit failed: Phase must not depend on other ZekStack libraries"
echo "Embedded safety audit failed: Phase must not depend on other ZekStack libraries except Strata"
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 "Phase 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 "Phase must not depend on ESP-IDF allocation internals"
exit 1
fi
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
Expand Down Expand Up @@ -102,6 +112,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 Down Expand Up @@ -146,12 +157,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/Phase"
rsync -a --delete --exclude ".git" ./ "$SKETCHBOOK_DIR/libraries/Phase/"
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
169 changes: 102 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Phase is an async application lifecycle orchestration library for ESP32.

Phase helps you boot and shut down larger Arduino ESP32 applications in a predictable order. It is designed for projects with multiple modules that depend on each other and focuses on dependency-ordered lifecycle steps, readiness gates, cooperative pause/resume, rollback, and result-based errors.
Phase helps you boot and shut down larger Arduino ESP32 applications in a predictable order. It owns lifecycle orchestration and dependency policy while [Strata](https://github.com/ZekStack/strata) owns Phase memory placement and low-level FreeRTOS storage.

[![CI](https://github.com/ZekStack/phase/actions/workflows/ci.yml/badge.svg)](https://github.com/ZekStack/phase/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ZekStack/phase?sort=semver)](https://github.com/ZekStack/phase/releases)
Expand All @@ -14,9 +14,13 @@ Phase helps you boot and shut down larger Arduino ESP32 applications in a predic
* **Dependency order** - steps and groups declare what must be ready first.
* **Two-layer lifecycle** - simple modules use init/deinit, advanced modules add start/stop.
* **Readiness groups** - wait for virtual gates such as network link or internet access.
* **Production-minded** - thread-safe internals, no exceptions, rollback, diagnostics, and progress callbacks.
* **Consistent memory policy** - `Strata::MemoryPolicy` controls graph allocations and task-stack placement.
* **Strata-owned FreeRTOS storage** - the Phase task stack, task control block, and recursive mutex storage are owned by Strata.
* **Production-minded** - thread-safe internals, rollback, diagnostics, progress callbacks, and allocation-free lifecycle execution after registration.

## Install
## Dependency

Phase `v0.2.0` requires Strata `v0.1.1`.

### PlatformIO

Expand All @@ -27,24 +31,27 @@ board = esp32dev
framework = arduino

lib_deps =
https://github.com/ZekStack/phase.git
https://github.com/ZekStack/phase.git#v0.2.0

build_flags =
-std=gnu++20
build_unflags =
-std=gnu++11
```

### Arduino IDE
Phase's `library.json` pins Strata `v0.1.1`, so PlatformIO resolves it transitively.

Phase is not published to Arduino Library Manager yet.
### Arduino IDE

Install it by downloading the repository ZIP or cloning it into your Arduino libraries folder.
Phase 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/Phase
```

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

## Quick start

```cpp
Expand All @@ -54,37 +61,73 @@ Arduino/libraries/Phase
Phase phase;

void setup() {
Serial.begin(115200);

PhaseResult initResult = phase.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}

phase.add("storage", []() {
Serial.println("storage init");
});

phase.add("network", []() {
Serial.println("network init");
}).start([]() {
Serial.println("network start");
});

phase.onReady([]() {
Serial.println("app ready");
});

phase.start();
Serial.println("setup continues while Phase boots");
Serial.begin(115200);

PhaseResult initResult = phase.init();
if (!initResult) {
Serial.println(initResult.message);
return;
}

phase.add("storage", []() {
Serial.println("storage init");
});

phase.add("network", []() {
Serial.println("network init");
}).start([]() {
Serial.println("network start");
});

phase.onReady([]() {
Serial.println("app ready");
});

phase.start();
Serial.println("setup continues while Phase boots");
}

void loop() {
delay(1000);
delay(1000);
}
```

## Memory policy

Phase uses the ZekStack-standard Strata configuration shape:

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

PhaseResult result = phase.init(config);
```

`memory.allocation` controls movable Phase-owned graph storage: node records, node/dependency names, dependency indexes, lifecycle order, and validation backing.

`memory.taskStack` controls the Phase task stack. Task control-block and mutex control storage remain internal through Strata.

The default policy preserves Phase v0.1.0 behavior:

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

`PreferExternal` falls back to internal memory when external memory is unavailable. `RequireExternal` fails task creation rather than consuming internal memory.

Diagnostics report requested placement separately from the observed memory region:

```cpp
PhaseDiag diag = phase.getDiagnostics();
Serial.printf(
"requested=%s actual=%s\n",
Strata::toString(diag.requestedStackPlacement),
Strata::toString(diag.stackRegion)
);
```

## Important notes

> [!IMPORTANT]
Expand All @@ -95,11 +138,26 @@ void loop() {
* Group condition polling timeouts are enforced by the Phase task.
* Registration closes after a successful `start()` request.
* `stop()`, `pause()`, and `resume()` may be called from Phase callbacks. `end()` must be called from another task and returns `Busy` when called from the Phase task.
* The destructor waits for the Phase task to stop using its internal state. Destruction from a Phase callback is deferred safely until the worker exits.
* Registration and graph preparation use `std::vector`, `std::string`, and `std::function`. Node storage, dependency indexes, and lifecycle order are preallocated before the worker starts; lifecycle execution does not allocate.
* A `Phase` object must not be destroyed from one of its own Phase callbacks. Strata task storage can only be reclaimed safely from another task context.
* Graph storage is allocated during initialization/registration and pre-reserved from the configured limits. Lifecycle execution remains allocation-free.
* Callback callables remain `std::function`; allocations performed internally by an arbitrary callable representation are outside Phase's placement contract.
* `PhaseChange` string pointers are valid for the complete callback invocation. Event messages and pause reasons are copied into bounded internal snapshots and may be truncated to 191 characters.
* Stop/deinit failures are best-effort and are reported through `onChange()` while remaining cleanup continues.
* Phase does not depend on other ZekStack libraries.
* Phase depends only on Strata within the ZekStack library ecosystem.

## Migrating from v0.1.x

Phase `v0.2.0` removes `PhaseStackType` and `PhaseConfig::stackType`.

| v0.1.x | v0.2.0 |
| --- | --- |
| `PhaseStackType::Auto` | `Strata::Placement::PreferExternal` |
| `PhaseStackType::Internal` | `Strata::Placement::Internal` |
| `PhaseStackType::Psram` | `Strata::Placement::RequireExternal` |
| `diag.requestedStackType` | `diag.requestedStackPlacement` |
| `diag.actualStackType` | `diag.stackRegion` |

For the old default behavior, no configuration change is required: the v0.2.0 default task placement is already `PreferExternal`.

## Examples

Expand All @@ -112,12 +170,7 @@ void loop() {
| `OptionalNodes` | Optional node failure and skipped dependent behavior. |
| `BindableCallbacks` | Bind private class methods with lambdas. |
| `ManualShutdown` | Request reverse stop/deinit from `loop()`. |

Start with:

```txt
examples/Basic
```
| `MemoryPolicy` | Configure Strata graph/task placement and inspect diagnostics. |

## Documentation

Expand All @@ -126,7 +179,7 @@ Detailed documentation is available in the `docs/` folder.
| Document | Description |
| --- | --- |
| [`docs/getting-started.md`](docs/getting-started.md) | Step-by-step setup and first lifecycle flow. |
| [`docs/configuration.md`](docs/configuration.md) | Task, timeout, limit, and polling options. |
| [`docs/configuration.md`](docs/configuration.md) | Memory, task, timeout, limit, and polling options. |
| [`docs/api.md`](docs/api.md) | Public classes, methods, callbacks, and result types. |
| [`docs/examples.md`](docs/examples.md) | Explanation of all included examples. |
| [`docs/troubleshooting.md`](docs/troubleshooting.md) | Common issues and behavior notes. |
Expand All @@ -144,8 +197,6 @@ phase.onFailed([](PhaseResult result) {});
phase.start();
```

For the full API, see [`docs/api.md`](docs/api.md).

## Compatibility

| Item | Support |
Expand All @@ -154,26 +205,10 @@ For the full API, see [`docs/api.md`](docs/api.md).
| Platform | `espressif32` |
| Language | C++20 |
| Filesystem | none |
| PSRAM | Optional for task stacks when ESP-IDF support is available |
| Dependencies | none |
| Exceptions | Not used |
| Status | `0.1.0` release candidate |

## Configuration

```cpp
PhaseConfig config;
config.stackSizeBytes = 4096;
config.priority = 1;
config.coreId = tskNO_AFFINITY;
config.stackType = PhaseStackType::Auto;
config.defaultInitTimeoutMs = 30000;
config.conditionPollIntervalMs = 100;

PhaseResult result = phase.init(config);
```

For all options, see [`docs/configuration.md`](docs/configuration.md).
| PSRAM | Optional; controlled through Strata placement |
| Dependencies | Strata `v0.1.1` |
| Exceptions | Not intentionally used by Phase |
| Status | `0.2.0` |

## Error handling

Expand All @@ -183,8 +218,8 @@ Phase reports operation status through `PhaseResult`.
PhaseResult result = phase.start();

if (!result) {
Serial.println(result.message);
return;
Serial.println(result.message);
return;
}
```

Expand Down
Loading
Loading