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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ All notable changes to this project will be documented in this file.
- Sync `send()` API returning structured `WebPushResult`.
- Retry/backoff handling for network/transport failures.
- Basic example sketch and CI workflows.
- Teardown lifecycle tests for pre-init `deinit()`, idempotent `deinit()`, re-init, and destructor teardown.

### Changed
- Teardown contract now uses `isInitialized()` and removes the old `initialized()` naming.
- `deinit()` now always converges teardown, including worker/queue/crypto cleanup and runtime config/key release.

### Notes
- JWT signing requires a valid system clock (SNTP).
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ if (!result.ok()) {
}
```

### Teardown

```cpp
if (webPush.isInitialized()) {
webPush.deinit();
}
```

## Configuration

`WebPushConfig` lets you tune the worker and queue:
Expand All @@ -106,7 +114,7 @@ if (!result.ok()) {
- `bool init(contactEmail, publicKeyBase64, privateKeyBase64, config)`
- `bool send(const PushMessage&, WebPushResultCB cb)` (async)
- `WebPushResult send(const PushMessage&)` (sync)
- `void deinit()` / `bool initialized() const`
- `void deinit()` / `bool isInitialized() const`
- `const char* errorToString(WebPushError)`

## Restrictions
Expand Down
8 changes: 8 additions & 0 deletions examples/basic_web_push/basic_web_push.ino
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#include <ESPWebPush.h>

ESPWebPush webPush;
bool tornDown = false;
uint32_t teardownAtMs = 0;

void setup() {
Serial.begin(115200);
Expand Down Expand Up @@ -46,8 +48,14 @@ void setup() {
} else {
Serial.printf("[webpush] sync ok: %d\n", syncResult.statusCode);
}

teardownAtMs = millis() + 5000;
}

void loop() {
if (!tornDown && webPush.isInitialized() && teardownAtMs != 0 && millis() >= teardownAtMs) {
webPush.deinit();
tornDown = true;
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
41 changes: 25 additions & 16 deletions src/esp_webPush/webPush.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,44 +68,49 @@ bool ESPWebPush::init(const std::string &contactEmail,
}

_stopRequested.store(false, std::memory_order_release);
_workerTask = nullptr;
TaskHandle_t workerTask = nullptr;
const char *taskName = _config.worker.name.empty() ? "webpush" : _config.worker.name.c_str();
const BaseType_t created = xTaskCreatePinnedToCore(
&ESPWebPush::workerLoopThunk,
taskName,
_config.worker.stackSizeBytes,
this,
_config.worker.priority,
&_workerTask,
&workerTask,
_config.worker.coreId);
if (created != pdPASS) {
ESP_LOGE(kTag, "init: failed to start worker task");
_workerTask = nullptr;
_workerTask.store(nullptr, std::memory_order_release);
vQueueDelete(_queue);
_queue = nullptr;
return false;
}

_workerTask.store(workerTask, std::memory_order_release);
_initialized.store(true, std::memory_order_release);
ESP_LOGI(kTag, "ESPWebPush initialized");
return true;
}

void ESPWebPush::deinit() {
if (!_initialized.load(std::memory_order_acquire)) {
return;
}

_initialized.store(false, std::memory_order_release);
_stopRequested.store(true, std::memory_order_release);

if (_workerTask != nullptr) {
TaskHandle_t workerTask = _workerTask.load(std::memory_order_acquire);
if (workerTask != nullptr) {
if (_queue != nullptr) {
QueueItem *wake = nullptr;
(void)xQueueSend(_queue, &wake, 0);
}
TickType_t start = xTaskGetTickCount();
while (_workerTask != nullptr && (xTaskGetTickCount() - start) <= pdMS_TO_TICKS(2000)) {
while (_workerTask.load(std::memory_order_acquire) != nullptr &&
(xTaskGetTickCount() - start) <= pdMS_TO_TICKS(2000)) {
vTaskDelay(pdMS_TO_TICKS(10));
}
if (_workerTask != nullptr) {
vTaskDelete(_workerTask);
_workerTask = nullptr;
workerTask = _workerTask.load(std::memory_order_acquire);
if (workerTask != nullptr) {
vTaskDelete(workerTask);
_workerTask.store(nullptr, std::memory_order_release);
}
}

Expand All @@ -122,11 +127,15 @@ void ESPWebPush::deinit() {

deinitCrypto();

_initialized.store(false, std::memory_order_release);
std::string().swap(_vapidPublicKey);
std::string().swap(_vapidPrivateKey);
std::string().swap(_vapidEmail);
_config = WebPushConfig{};
_stopRequested.store(false, std::memory_order_release);
}

bool ESPWebPush::send(const PushMessage &msg, WebPushResultCB callback) {
if (!_initialized.load(std::memory_order_acquire) || !_queue) {
if (!isInitialized() || !_queue) {
ESP_LOGW(kTag, "send: not initialized");
return false;
}
Expand All @@ -152,7 +161,7 @@ bool ESPWebPush::send(const PushMessage &msg, WebPushResultCB callback) {
}

WebPushResult ESPWebPush::send(const PushMessage &msg) {
if (!_initialized.load(std::memory_order_acquire)) {
if (!isInitialized()) {
WebPushResult result{};
result.error = WebPushError::NotInitialized;
result.message = errorToString(result.error);
Expand Down Expand Up @@ -360,7 +369,7 @@ void ESPWebPush::workerLoop() {
}
freeItem(item);
}
_workerTask = nullptr;
_workerTask.store(nullptr, std::memory_order_release);
vTaskDelete(nullptr);
}

Expand Down
4 changes: 2 additions & 2 deletions src/esp_webPush/webPush.h
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class ESPWebPush {
const WebPushConfig &config = WebPushConfig{});

void deinit();
bool initialized() const { return _initialized.load(std::memory_order_acquire); }
bool isInitialized() const { return _initialized.load(std::memory_order_acquire); }

bool send(const PushMessage &msg, WebPushResultCB callback);
WebPushResult send(const PushMessage &msg);
Expand Down Expand Up @@ -189,7 +189,7 @@ class ESPWebPush {
std::string _vapidEmail{};
WebPushConfig _config{};

TaskHandle_t _workerTask = nullptr;
std::atomic<TaskHandle_t> _workerTask{nullptr};
QueueHandle_t _queue = nullptr;
std::atomic<bool> _initialized{false};
std::atomic<bool> _stopRequested{false};
Expand Down
88 changes: 88 additions & 0 deletions test/test_esp_webPush/test_esp_webPush.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include <Arduino.h>
#include <ESPWebPush.h>
#include <unity.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

namespace {

constexpr const char *kContact = "notify@example.com";
constexpr const char *kPublicKey =
"BAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0A";
constexpr const char *kPrivateKey = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA";

WebPushConfig testConfig() {
WebPushConfig cfg{};
cfg.queueLength = 2;
cfg.queueMemory = WebPushQueueMemory::Internal;
cfg.requireNetworkReady = false;
cfg.worker.stackSizeBytes = 4096;
cfg.worker.priority = 2;
cfg.worker.name = "wp-test";
return cfg;
}

void test_deinit_is_safe_before_init() {
ESPWebPush webPush;
TEST_ASSERT_FALSE(webPush.isInitialized());

webPush.deinit();
TEST_ASSERT_FALSE(webPush.isInitialized());
}

void test_deinit_is_idempotent() {
ESPWebPush webPush;
TEST_ASSERT_TRUE(webPush.init(kContact, kPublicKey, kPrivateKey, testConfig()));
TEST_ASSERT_TRUE(webPush.isInitialized());

webPush.deinit();
TEST_ASSERT_FALSE(webPush.isInitialized());

webPush.deinit();
TEST_ASSERT_FALSE(webPush.isInitialized());
}

void test_reinit_after_deinit() {
ESPWebPush webPush;
TEST_ASSERT_TRUE(webPush.init(kContact, kPublicKey, kPrivateKey, testConfig()));
TEST_ASSERT_TRUE(webPush.isInitialized());
webPush.deinit();
TEST_ASSERT_FALSE(webPush.isInitialized());

TEST_ASSERT_TRUE(webPush.init(kContact, kPublicKey, kPrivateKey, testConfig()));
TEST_ASSERT_TRUE(webPush.isInitialized());
webPush.deinit();
}

void test_destructor_deinits_active_instance() {
{
ESPWebPush first;
TEST_ASSERT_TRUE(first.init(kContact, kPublicKey, kPrivateKey, testConfig()));
TEST_ASSERT_TRUE(first.isInitialized());
}

ESPWebPush second;
TEST_ASSERT_TRUE(second.init(kContact, kPublicKey, kPrivateKey, testConfig()));
TEST_ASSERT_TRUE(second.isInitialized());
second.deinit();
}

} // namespace

void setUp() {}
void tearDown() {}

void setup() {
delay(2000);
UNITY_BEGIN();
RUN_TEST(test_deinit_is_safe_before_init);
RUN_TEST(test_deinit_is_idempotent);
RUN_TEST(test_reinit_after_deinit);
RUN_TEST(test_destructor_deinits_active_instance);
UNITY_END();
}

void loop() {
vTaskDelay(pdMS_TO_TICKS(1000));
}