mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 17:41:19 +00:00
Expose broker publish acknowledgments for MQTT validation
This commit is contained in:
1 parent
7acc637468
commit
ef80f29337
6 files changed
+452
-9
No files matched your search
@@ -196,6 +196,7 @@ jobs:
|
||||
python3 -B test/test_mqtt_connection_admission.py -v
|
||||
python3 -B test/test_mqtt_handshake_scheduler.py -v
|
||||
python3 -B test/test_mqtt_diagnostics.py -v
|
||||
python3 -B test/test_mqtt_publish_ack.py -v
|
||||
|
||||
- name: Verify DNS lease restoration and bounded diagnostics
|
||||
run: python3 -B test/test_network_dns.py -v
|
||||
|
||||
@@ -53,6 +53,9 @@ room-server and companion images that enable it.
|
||||
- Require byte-addressable internal memory for string/JSON/queue buffer
|
||||
fallbacks. The internal-memory capability alone also admits instruction RAM
|
||||
that only supports aligned 32-bit access.
|
||||
- Count broker QoS acknowledgments separately from accepted writes and expose
|
||||
them as `pub_ack` in `get mqttN.diag`. This lets hardware tests verify
|
||||
delivery without treating an expired outbox entry as an acknowledgment.
|
||||
- Add DMA free/largest readings to `get mqtt.stats` and diagnostic logs.
|
||||
Correct the operator-facing TLS error labels against SDK constants.
|
||||
|
||||
@@ -69,14 +72,46 @@ PSRAM and internal-only modes, fragmented heap, exhausted PSRAM, delayed
|
||||
connection callbacks, retry timing and bounded replies. They do not execute
|
||||
an ESP32 AES peripheral or qualify physical WiFi behavior.
|
||||
|
||||
All 1,794 native tests and the 31 memory-budget tests passed. The focused
|
||||
MQTT tests are also connected to the GitHub unit-test workflow.
|
||||
All 42 focused Python MQTT tests, 1,794 native tests and 31 memory-budget tests
|
||||
passed. The focused MQTT tests are also connected to the GitHub unit-test
|
||||
workflow.
|
||||
|
||||
The VM reached both reported broker TLS endpoints with certificate
|
||||
verification enabled. That is an endpoint check, not an ESP32 MQTT test.
|
||||
Three Full MQTT repeater builds with USB packet logging passed on 10 October
|
||||
2026 at revision `7acc63746`. Their build logs report:
|
||||
|
||||
MercerWoodMesh reported offline and its SSH connection timed out during this
|
||||
run. No live V4 flash, MQTT publication or soak result is claimed. The next
|
||||
hardware check must exercise the reported WSS presets together, verify rising
|
||||
publish counters and DMA headroom, then include a broker/WiFi reconnection
|
||||
and power-saving idle run with no reset or AES allocation error.
|
||||
| Build | Application flash bytes | Available internal heap bytes | Required heap bytes | Largest internal region bytes |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| Heltec V4 | 2,182,125 | 205,080 | 198,432 | 172,312 |
|
||||
| Heltec V3 | 2,125,149 | 211,296 | 209,184 | 178,528 |
|
||||
| Station G2 | 2,017,685 | 201,280 | 200,480 | 168,512 |
|
||||
|
||||
Application flash is the PlatformIO size-check reading. Internal heap is
|
||||
linked capacity before dynamic allocation, not free heap measured after boot.
|
||||
All three RAM gates passed the startup policy; they do not qualify maximum
|
||||
TLS/WSS/outbox load. The default MQTT startup allowance remains 24,576 bytes,
|
||||
with runtime admission for additional connections. See
|
||||
[firmware memory checks](../research/firmware_memory_budget.md#mqtt-startup-minimum-and-runtime-qualification)
|
||||
for the separate maximum-slot estimate and qualification limits.
|
||||
|
||||
Live TLS 1.2 certificate chains from 12 audited preset endpoints were
|
||||
verified with the exact CA PEMs extracted from `MQTTPresets.h` using a native
|
||||
host build of the pinned mbedTLS 2.28.7 submodule from ESP-IDF 4.4.7, commit
|
||||
`2b8e772fc1cb0732cda3bae7d1e9d6f4cfaf63d9`. Hostname verification was enabled.
|
||||
All 11 GTS Root R4 endpoints and the ISRG Root X1 Cascadia endpoint returned
|
||||
verification result 0 and flags `0x00000000`. Wrong-hostname and unrelated-anchor
|
||||
negative checks were rejected. WAEV also passed with the server root omitted
|
||||
from its supplied chain.
|
||||
|
||||
The existing cross-signed GTS Root R4 PEM is a valid trust anchor for that
|
||||
pinned mbedTLS verifier. Ordinary OpenSSL root-only verification rejected it,
|
||||
while partial-chain verification with the same PEM passed all 11 GTS endpoints.
|
||||
This host-probe policy difference does not establish a firmware CA failure or
|
||||
require a CA replacement for the reported AES allocation errors. The verifier
|
||||
used the native default mbedTLS configuration; this establishes pinned X.509
|
||||
chain and trust-anchor behavior, not an ESP32 hardware handshake.
|
||||
|
||||
MercerWoodMesh is now reachable and live V4 testing is in progress. Live flash,
|
||||
MQTT publication, reconnect and soak outcomes remain pending. The hardware
|
||||
check must exercise the reported WSS presets together, verify rising publish
|
||||
counters and DMA headroom, then include a broker/WiFi reconnection and
|
||||
power-saving idle run with no reset or AES allocation error.
|
||||
@@ -657,6 +657,11 @@ unsigned long PsychicMqttClient::getPublishOk()
|
||||
return _publish_ok;
|
||||
}
|
||||
|
||||
unsigned long PsychicMqttClient::getPublishAck()
|
||||
{
|
||||
return _publish_ack.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
unsigned long PsychicMqttClient::getPublishErr()
|
||||
{
|
||||
return _publish_err;
|
||||
@@ -832,6 +837,7 @@ void PsychicMqttClient::_onMessage(esp_mqtt_event_handle_t &event)
|
||||
|
||||
void PsychicMqttClient::_onPublish(esp_mqtt_event_handle_t &event)
|
||||
{
|
||||
_publish_ack.fetch_add(1, std::memory_order_relaxed);
|
||||
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
|
||||
for (uint8_t i = 0; i < _onPublishUserCallbackCount; ++i)
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
#include "Arduino.h"
|
||||
@@ -471,6 +472,13 @@ public:
|
||||
*/
|
||||
unsigned long getPublishOk();
|
||||
|
||||
/**
|
||||
* @brief Cumulative QoS 1/2 publishes acknowledged by the broker during
|
||||
* this client's lifetime. Counts MQTT_EVENT_PUBLISHED, not accepted writes
|
||||
* or outbox expiry. MeshCore's QoS 1 status messages complete on PUBACK.
|
||||
*/
|
||||
unsigned long getPublishAck();
|
||||
|
||||
/**
|
||||
* @brief Cumulative count of publishes that failed (negative return from the
|
||||
* synchronous write or async enqueue -- socket error / network timeout). Monotonic.
|
||||
@@ -495,6 +503,8 @@ private:
|
||||
// Cumulative publish accept/fail counts (any QoS, sync or async path).
|
||||
unsigned long _publish_ok = 0;
|
||||
unsigned long _publish_err = 0;
|
||||
// Written by the SDK event task and read by the bridge/CLI task.
|
||||
std::atomic<unsigned long> _publish_ack{0};
|
||||
|
||||
// Multipart message reassembly. _buffer is lazily allocated at connect() time
|
||||
// to match the configured buffer size, then reused for the client's lifetime.
|
||||
|
||||
@@ -721,6 +721,19 @@ void MQTTBridge::formatSlotDiagReply(char* buf, size_t bufsize, int slot_index)
|
||||
static_cast<unsigned long>(MQTTConnectionHealth::clampFailureTotal(cf)));
|
||||
}
|
||||
|
||||
// Keep error details first and never clip an acknowledgment count into a
|
||||
// different number. Accepted/enqueued publishes and outbox expiry do not
|
||||
// advance this broker acknowledgment counter.
|
||||
if (slot.client) {
|
||||
char ack_text[32];
|
||||
const int ack_len = snprintf(ack_text, sizeof(ack_text), ", pub_ack:%lu",
|
||||
slot.client->getPublishAck());
|
||||
const int remaining = pos < (int)bufsize ? (int)bufsize - 1 - pos : 0;
|
||||
if (ack_len > 0 && ack_len < (int)sizeof(ack_text) && ack_len <= remaining) {
|
||||
replyAppendf(buf, bufsize, &pos, "%s", ack_text);
|
||||
}
|
||||
}
|
||||
|
||||
// Appended last so it never displaces connection diagnostics. replyAppendf
|
||||
// clamps rather than overflows, but a clipped type list is worse than no
|
||||
// list: "...,13,14," parses as a real, different allowlist, and this is the
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the real MQTT publish-event path and bounded slot diagnostics on host.
|
||||
|
||||
The adapter uses its production header; SDK calls are controllable stubs. Only
|
||||
the unrelated receive/error handlers are replaced. No PlatformIO or broker is
|
||||
required, and both installed ESP-IDF configuration layouts are exercised.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ADAPTER = "lib/PsychicMqttClient/src/PsychicMqttClient.cpp"
|
||||
BRIDGE = "src/helpers/bridges/MQTTBridge.cpp"
|
||||
|
||||
|
||||
def method(path, signature):
|
||||
source = (ROOT / path).read_text(encoding="utf-8")
|
||||
start = source.index(signature)
|
||||
opening = source.index("{", start)
|
||||
depth = 1
|
||||
pos = opening + 1
|
||||
while depth:
|
||||
depth += (source[pos] == "{") - (source[pos] == "}")
|
||||
pos += 1
|
||||
return source[start:pos] + "\n"
|
||||
|
||||
|
||||
ARDUINO_HEADER = r'''
|
||||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
unsigned long millis();
|
||||
void vTaskDelay(unsigned);
|
||||
constexpr unsigned portTICK_PERIOD_MS = 1;
|
||||
'''
|
||||
|
||||
MQTT_HEADER = r'''
|
||||
#pragma once
|
||||
using esp_err_t = int;
|
||||
using esp_event_base_t = const char*;
|
||||
using esp_mqtt_client_handle_t = void*;
|
||||
constexpr int ESP_OK = 0, ESP_ERR_INVALID_STATE = 1, ESP_ERR_NO_MEM = 2;
|
||||
enum {
|
||||
MQTT_EVENT_ANY = -1, MQTT_EVENT_CONNECTED, MQTT_EVENT_DISCONNECTED,
|
||||
MQTT_EVENT_SUBSCRIBED, MQTT_EVENT_UNSUBSCRIBED, MQTT_EVENT_PUBLISHED,
|
||||
MQTT_EVENT_DATA, MQTT_EVENT_ERROR
|
||||
};
|
||||
struct esp_mqtt_client_config_t {
|
||||
const char* uri;
|
||||
int buffer_size;
|
||||
struct { struct { const char* uri; } address; } broker;
|
||||
struct { int size; } buffer;
|
||||
};
|
||||
struct esp_mqtt_error_codes_t { int unused; };
|
||||
struct esp_mqtt_event_t { int event_id, msg_id; bool session_present; };
|
||||
using esp_mqtt_event_handle_t = esp_mqtt_event_t*;
|
||||
'''
|
||||
|
||||
PREAMBLE = r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
// Preinclude the standard headers so this seam only opens adapter visibility.
|
||||
#define private public
|
||||
#include "PsychicMqttClient.h"
|
||||
#undef private
|
||||
#include "helpers/MQTTReplyFormat.h"
|
||||
#include "helpers/MQTTConnectionHealth.h"
|
||||
#include "helpers/MQTTPacketFilter.h"
|
||||
#include "helpers/bridges/MQTTErrorLabels.h"
|
||||
template<typename... Args> void test_log(Args&&...) {}
|
||||
static const char* TAG = "test";
|
||||
#define ESP_LOGE(...) test_log(__VA_ARGS__)
|
||||
#define ESP_LOGW(...) test_log(__VA_ARGS__)
|
||||
#define ESP_LOGI(...) test_log(__VA_ARGS__)
|
||||
#define ESP_LOGV(...) test_log(__VA_ARGS__)
|
||||
#define ESP_ERROR_CHECK_WITHOUT_ABORT(value) ((void)(value))
|
||||
static unsigned long now_ms = 121000;
|
||||
unsigned long millis() { return now_ms; }
|
||||
void vTaskDelay(unsigned) { assert(false && "stub disconnect must deliver its event"); }
|
||||
const char* esp_err_to_name(int) { return "mock"; }
|
||||
static int publish_result = 0, enqueue_result = 41, outbox_size = 0;
|
||||
static int publish_calls = 0, enqueue_calls = 0, config_calls = 0;
|
||||
static int start_calls = 0, reconnect_calls = 0, receive_events = 0, error_events = 0;
|
||||
using EventHandler = void(*)(void*, esp_event_base_t, int32_t, void*);
|
||||
static EventHandler sdk_event_handler = nullptr;
|
||||
static void* sdk_handler_args = nullptr;
|
||||
void* esp_mqtt_client_init(esp_mqtt_client_config_t*) {
|
||||
return reinterpret_cast<void*>(1);
|
||||
}
|
||||
int esp_mqtt_client_register_event(void*, int, EventHandler handler, void* args) {
|
||||
sdk_event_handler = handler;
|
||||
sdk_handler_args = args;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_mqtt_client_destroy(void*) { return ESP_OK; }
|
||||
int esp_mqtt_set_config(void*, esp_mqtt_client_config_t*) {
|
||||
++config_calls;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_mqtt_client_start(void*) { ++start_calls; return ESP_OK; }
|
||||
int esp_mqtt_client_reconnect(void*) { ++reconnect_calls; return ESP_OK; }
|
||||
int esp_mqtt_client_stop(void*) { return ESP_OK; }
|
||||
int esp_mqtt_client_disconnect(void*) {
|
||||
assert(sdk_event_handler != nullptr);
|
||||
esp_mqtt_event_t event{MQTT_EVENT_DISCONNECTED, 0, false};
|
||||
sdk_event_handler(sdk_handler_args, "MQTT", event.event_id, &event);
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_mqtt_client_subscribe(void*, const char*, int) { return 7; }
|
||||
int esp_mqtt_client_get_outbox_size(void*) { return outbox_size; }
|
||||
int esp_mqtt_client_publish(void*, const char*, const char*, int, int, bool) {
|
||||
++publish_calls;
|
||||
return publish_result;
|
||||
}
|
||||
int esp_mqtt_client_enqueue(void*, const char*, const char*, int, int, bool, bool store) {
|
||||
assert(store);
|
||||
++enqueue_calls;
|
||||
return enqueue_result;
|
||||
}
|
||||
void PsychicMqttClient::_onMessage(esp_mqtt_event_handle_t&) { ++receive_events; }
|
||||
void PsychicMqttClient::_onError(esp_mqtt_event_handle_t&) { ++error_events; }
|
||||
|
||||
constexpr int RUNTIME_MQTT_SLOTS = 2;
|
||||
struct MQTTPrefs {
|
||||
uint16_t mqtt_slot_packet_filter[RUNTIME_MQTT_SLOTS] = {
|
||||
MQTTPacketFilter::kAllPacketTypes, MQTTPacketFilter::kAllPacketTypes};
|
||||
};
|
||||
struct MQTTBridge {
|
||||
struct MQTTSlot {
|
||||
PsychicMqttClient* client = nullptr;
|
||||
bool enabled = true, connected = true, circuit_breaker_tripped = false;
|
||||
const void* preset = nullptr;
|
||||
char host[8] = "broker";
|
||||
uint32_t disconnect_count = 0, connect_failures = 0, start_failures = 0;
|
||||
unsigned long first_disconnect_time = 0, last_error_time = 0;
|
||||
int32_t last_tls_err = 0, last_tls_stack_err = 0;
|
||||
int last_sock_errno = 0;
|
||||
};
|
||||
MQTTSlot _slots[RUNTIME_MQTT_SLOTS];
|
||||
bool _initialized = true;
|
||||
bool ready = true;
|
||||
MQTTPrefs* _obs = nullptr;
|
||||
bool isSlotReady(int) const { return ready; }
|
||||
static const char* tlsErrorStr(int32_t);
|
||||
static void formatSlotDiagReply(char*, size_t, int);
|
||||
};
|
||||
static MQTTBridge* s_mqtt_bridge_instance = nullptr;
|
||||
'''
|
||||
|
||||
MAIN = r'''
|
||||
static void dispatch(PsychicMqttClient& client, int event_id, int msg_id = 0) {
|
||||
esp_mqtt_event_t event{event_id, msg_id, false};
|
||||
PsychicMqttClient::_onMqttEventStatic(&client, "MQTT", event_id, &event);
|
||||
}
|
||||
|
||||
int main() {
|
||||
PsychicMqttClient client;
|
||||
assert(client.getPublishAck() == 0);
|
||||
client._mqtt_cfg.uri = "mqtt://broker";
|
||||
client._mqtt_cfg.broker.address.uri = "mqtt://broker";
|
||||
assert(client.connect() == ESP_OK);
|
||||
dispatch(client, MQTT_EVENT_CONNECTED);
|
||||
assert(client.connected() && client.getPublishAck() == 0);
|
||||
|
||||
// Acceptance, synchronous success, failure, and outbox drops are distinct
|
||||
// from a broker acknowledgment. The real publish method exercises both paths.
|
||||
assert(client.publish("topic", 1, false, "body", 4, true) == 41);
|
||||
assert(client.publish("topic", 0, false, "body", 4, false) == 0);
|
||||
assert(client.getPublishOk() == 2 && client.getPublishAck() == 0);
|
||||
enqueue_result = -1;
|
||||
publish_result = -1;
|
||||
assert(client.publish("topic", 1, false, "body", 4, true) == -1);
|
||||
assert(client.publish("topic", 0, false, "body", 4, false) == -1);
|
||||
assert(client.getPublishErr() == 2 && client.getPublishAck() == 0);
|
||||
client.setOutboxLimit(10);
|
||||
outbox_size = 10;
|
||||
assert(client.publish("topic", 0, false, "body", 4, true) == -2);
|
||||
assert(client.getOutboxDrops() == 1 && client.getPublishAck() == 0);
|
||||
assert(publish_calls == 2 && enqueue_calls == 2);
|
||||
|
||||
// A published event counts even without user callbacks. User callbacks see
|
||||
// the updated total and additional callbacks do not multiply the counter.
|
||||
dispatch(client, MQTT_EVENT_PUBLISHED, 41);
|
||||
assert(client.getPublishAck() == 1);
|
||||
int callbacks = 0, extra_callbacks = 0;
|
||||
assert(&client.onPublish([&](int msg_id) {
|
||||
++callbacks;
|
||||
assert(msg_id == 41 + callbacks);
|
||||
assert(client.getPublishAck() == static_cast<unsigned long>(1 + callbacks));
|
||||
}) == &client);
|
||||
dispatch(client, MQTT_EVENT_PUBLISHED, 42);
|
||||
assert(client.getPublishAck() == 2 && callbacks == 1);
|
||||
client.onPublish([&](int msg_id) {
|
||||
++extra_callbacks;
|
||||
assert(msg_id == 43 && client.getPublishAck() == 3);
|
||||
});
|
||||
dispatch(client, MQTT_EVENT_PUBLISHED, 43);
|
||||
assert(client.getPublishAck() == 3 && callbacks == 2 && extra_callbacks == 1);
|
||||
|
||||
// Connection churn and unrelated events cannot clear or advance the total.
|
||||
const int unrelated_events[] = {
|
||||
MQTT_EVENT_DISCONNECTED, MQTT_EVENT_CONNECTED, MQTT_EVENT_SUBSCRIBED,
|
||||
MQTT_EVENT_UNSUBSCRIBED, MQTT_EVENT_DATA, MQTT_EVENT_ERROR, 12345};
|
||||
for (int event_id : unrelated_events) {
|
||||
dispatch(client, event_id);
|
||||
assert(client.getPublishAck() == 3);
|
||||
}
|
||||
assert(receive_events == 1 && error_events == 1);
|
||||
assert(client.publish("topic", 0, false, "body", 4, true) == -1);
|
||||
assert(client.getPublishAck() == 3);
|
||||
client.forceStop();
|
||||
assert(client.getPublishAck() == 3 && !client.isStarted());
|
||||
assert(client.connect() == ESP_OK && client.getPublishAck() == 3);
|
||||
assert(client.reconnect() == ESP_OK && client.getPublishAck() == 3);
|
||||
client._config_dirty = true;
|
||||
assert(client.connect() == ESP_OK && client.getPublishAck() == 3);
|
||||
assert(start_calls == 3 && reconnect_calls == 1 && config_calls == 1);
|
||||
PsychicMqttClient fresh;
|
||||
assert(fresh.getPublishAck() == 0);
|
||||
|
||||
char reply[160];
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), 0);
|
||||
assert(strcmp(reply, "> mqtt1: bridge not running") == 0);
|
||||
assert(strstr(reply, "pub_ack") == nullptr);
|
||||
MQTTBridge bridge;
|
||||
MQTTPrefs prefs;
|
||||
bridge._obs = &prefs;
|
||||
bridge._slots[0].client = &client;
|
||||
s_mqtt_bridge_instance = &bridge;
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), 0);
|
||||
assert(strcmp(reply, "> mqtt1: ok, no errors, pub_ack:3") == 0);
|
||||
bridge._slots[0].client = &fresh;
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), 0);
|
||||
assert(strcmp(reply, "> mqtt1: ok, no errors, pub_ack:0") == 0);
|
||||
bridge._slots[0].client = &client;
|
||||
bridge._slots[0].connect_failures = 2;
|
||||
bridge._slots[0].start_failures = 3;
|
||||
prefs.mqtt_slot_packet_filter[0] = (1U << 2) | (1U << 3);
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), 0);
|
||||
assert(strcmp(reply, "> mqtt1: ok, no errors, cf:5, pub_ack:3, filter:2,3") == 0);
|
||||
|
||||
// Without an allocated client there is no client-lifetime acknowledgment
|
||||
// total. Early-return replies also retain their established wording.
|
||||
bridge._slots[1].connected = false;
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), 1);
|
||||
assert(strcmp(reply, "> mqtt2: no client, no error info") == 0);
|
||||
bridge._slots[1].enabled = false;
|
||||
bridge._slots[1].host[0] = '\0';
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), 1);
|
||||
assert(strcmp(reply, "> mqtt2: not configured") == 0);
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), RUNTIME_MQTT_SLOTS);
|
||||
assert(strcmp(reply, "> invalid slot") == 0);
|
||||
|
||||
bridge._slots[0] = MQTTBridge::MQTTSlot{};
|
||||
bridge._slots[0].client = &client;
|
||||
prefs.mqtt_slot_packet_filter[0] = MQTTPacketFilter::kAllPacketTypes;
|
||||
client._publish_ack.store(4294967295UL, std::memory_order_relaxed);
|
||||
MQTTBridge::formatSlotDiagReply(reply, sizeof(reply), 0);
|
||||
const char* healthy_prefix = "> mqtt1: ok, no errors";
|
||||
const char* ack_field = ", pub_ack:4294967295";
|
||||
assert(strcmp(reply, "> mqtt1: ok, no errors, pub_ack:4294967295") == 0);
|
||||
const size_t complete_size = strlen(healthy_prefix) + strlen(ack_field) + 1;
|
||||
for (size_t capacity = 1; capacity <= complete_size + 1; ++capacity) {
|
||||
unsigned char bounded[168];
|
||||
memset(bounded, 0xA5, sizeof(bounded));
|
||||
char* output = reinterpret_cast<char*>(bounded);
|
||||
MQTTBridge::formatSlotDiagReply(output, capacity, 0);
|
||||
assert(memchr(output, '\0', capacity) != nullptr);
|
||||
for (size_t i = capacity; i < sizeof(bounded); ++i) assert(bounded[i] == 0xA5);
|
||||
if (capacity < complete_size) {
|
||||
assert(strstr(output, "pub_") == nullptr);
|
||||
assert(strncmp(output, healthy_prefix, capacity - 1) == 0);
|
||||
} else {
|
||||
assert(strcmp(output, "> mqtt1: ok, no errors, pub_ack:4294967295") == 0);
|
||||
}
|
||||
}
|
||||
unsigned char untouched[16];
|
||||
memset(untouched, 0xA5, sizeof(untouched));
|
||||
MQTTBridge::formatSlotDiagReply(reinterpret_cast<char*>(untouched), 0, 0);
|
||||
for (unsigned char byte : untouched) assert(byte == 0xA5);
|
||||
MQTTBridge::formatSlotDiagReply(nullptr, 16, 0);
|
||||
|
||||
// Connection diagnostics and failure totals keep priority in the 160-byte
|
||||
// LoRa reply. A long error tail must omit the whole ACK field, not its digits.
|
||||
auto& slot = bridge._slots[0];
|
||||
slot.connected = false;
|
||||
slot.disconnect_count = 4294967295U;
|
||||
slot.first_disconnect_time = 1;
|
||||
slot.last_error_time = 1;
|
||||
slot.last_tls_err = 0x8003;
|
||||
slot.last_tls_stack_err = -0x7F00;
|
||||
slot.last_sock_errno = -2147483647;
|
||||
slot.connect_failures = 4294967295U;
|
||||
slot.start_failures = 4294967295U;
|
||||
now_ms = 4294967295UL;
|
||||
unsigned char long_reply[168];
|
||||
memset(long_reply, 0xA5, sizeof(long_reply));
|
||||
MQTTBridge::formatSlotDiagReply(reinterpret_cast<char*>(long_reply), 160, 0);
|
||||
const char* diagnostic = reinterpret_cast<char*>(long_reply);
|
||||
assert(strstr(diagnostic, "unsupported protocol family (0x8003)") != nullptr);
|
||||
assert(strstr(diagnostic, "mbedtls:-0x7F00") != nullptr);
|
||||
assert(strstr(diagnostic, "sock:-2147483647") != nullptr);
|
||||
assert(strstr(diagnostic, "cf:2147483647") != nullptr);
|
||||
assert(strstr(diagnostic, "pub_") == nullptr);
|
||||
assert(memchr(diagnostic, '\0', 160) != nullptr);
|
||||
for (size_t i = 160; i < sizeof(long_reply); ++i) assert(long_reply[i] == 0xA5);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class MqttPublishAckTests(unittest.TestCase):
|
||||
def test_production_publish_events_and_slot_diagnostics(self):
|
||||
compiler = shutil.which("g++")
|
||||
if not compiler:
|
||||
self.skipTest("g++ is unavailable")
|
||||
source = PREAMBLE + "\n".join(method(ADAPTER, signature) for signature in (
|
||||
"PsychicMqttClient::PsychicMqttClient()",
|
||||
"PsychicMqttClient::~PsychicMqttClient()",
|
||||
"PsychicMqttClient &PsychicMqttClient::onPublish(",
|
||||
"bool PsychicMqttClient::connected()",
|
||||
"esp_err_t PsychicMqttClient::applyConfig()",
|
||||
"esp_err_t PsychicMqttClient::connect()",
|
||||
"esp_err_t PsychicMqttClient::reconnect()",
|
||||
"void PsychicMqttClient::disconnect()",
|
||||
"void PsychicMqttClient::forceStop()",
|
||||
"int PsychicMqttClient::subscribe(",
|
||||
"int PsychicMqttClient::publish(",
|
||||
"PsychicMqttClient &PsychicMqttClient::setOutboxLimit(",
|
||||
"unsigned long PsychicMqttClient::getOutboxDrops()",
|
||||
"unsigned long PsychicMqttClient::getPublishOk()",
|
||||
"unsigned long PsychicMqttClient::getPublishErr()",
|
||||
"unsigned long PsychicMqttClient::getPublishAck()",
|
||||
"void PsychicMqttClient::_onMqttEventStatic(",
|
||||
"void PsychicMqttClient::_onMqttEvent(",
|
||||
"void PsychicMqttClient::_onConnect(",
|
||||
"void PsychicMqttClient::_onDisconnect(",
|
||||
"void PsychicMqttClient::_onSubscribe(",
|
||||
"void PsychicMqttClient::_onUnsubscribe(",
|
||||
"void PsychicMqttClient::_onPublish(",
|
||||
)) + "\n".join(method(BRIDGE, signature) for signature in (
|
||||
"const char* MQTTBridge::tlsErrorStr(",
|
||||
"void MQTTBridge::formatSlotDiagReply(",
|
||||
)) + MAIN
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-mqtt-publish-ack-") as directory:
|
||||
fixture = Path(directory)
|
||||
(fixture / "Arduino.h").write_text(ARDUINO_HEADER, encoding="ascii")
|
||||
(fixture / "mqtt_client.h").write_text(MQTT_HEADER, encoding="ascii")
|
||||
(fixture / "esp_crt_bundle.h").write_text("#pragma once\n", encoding="ascii")
|
||||
test_source = fixture / "publish_ack.cpp"
|
||||
test_source.write_text(source, encoding="ascii")
|
||||
for idf in (4, 5):
|
||||
with self.subTest(idf=idf):
|
||||
executable = fixture / f"idf{idf}"
|
||||
# publish() already compares the SDK's signed outbox size
|
||||
# against size_t; this unrelated warning is outside the seam.
|
||||
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-sign-compare",
|
||||
"-DARDUINO_ARCH_ESP32=1", f"-DESP_IDF_VERSION_MAJOR={idf}",
|
||||
"-I", str(fixture),
|
||||
"-I", str(ROOT / "lib/PsychicMqttClient/src"),
|
||||
"-I", str(ROOT / "src"), str(test_source), "-o", str(executable)]
|
||||
result = subprocess.run(command, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run([str(executable)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user