mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 19:05:07 +00:00
Mesh gets contacts, channels, delivery receipts, QR sharing and an ESP-NOW radio. Mesh Chat is split into Mesh Contacts and Mesh Channels, `mesh inspect` replaces the Mesh Inspect app, and ESP-NOW and Wi-Fi now share the Wi-Fi radio safely.
592 lines
25 KiB
C++
592 lines
25 KiB
C++
#include "doctest.h"
|
|
|
|
#include <mesh/backend.h>
|
|
#include <mesh/mesh.h>
|
|
#include <mesh/mesh_service.h>
|
|
#include <mesh/mesh_settings.h>
|
|
|
|
#include <service/manager.h>
|
|
|
|
#include <tactility/concurrent/thread.h>
|
|
#include <tactility/delay.h>
|
|
#include <tactility/device.h>
|
|
#include <tactility/drivers/radio.h>
|
|
#include <tactility/filesystem/file_system.h>
|
|
#include <tactility/paths.h>
|
|
|
|
#include <atomic>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
// region Fake data filesystem, so the service can store messages in a temporary directory
|
|
|
|
error_t fakeMount(void*) { return ERROR_NONE; }
|
|
error_t fakeUnmount(void*) { return ERROR_NONE; }
|
|
bool fakeIsMounted(void*) { return false; }
|
|
|
|
error_t fakeGetPath(void* data, char* out_path, size_t out_path_size) {
|
|
const char* path = static_cast<const char*>(data);
|
|
if (strlen(path) + 1 > out_path_size) {
|
|
return ERROR_BUFFER_OVERFLOW;
|
|
}
|
|
strcpy(out_path, path);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
const FileSystemApi fakeDataApi = {
|
|
.mount = fakeMount,
|
|
.unmount = fakeUnmount,
|
|
.is_mounted = fakeIsMounted,
|
|
.get_path = fakeGetPath,
|
|
};
|
|
|
|
// endregion
|
|
|
|
// region Loopback backend: a frame is a 1-byte channel followed by the text
|
|
|
|
int startCount = 0;
|
|
|
|
void* loopbackCreate(MeshHost* host) {
|
|
// Backends load their state here
|
|
char path[FILE_MAX_PATH_STRING_LENGTH];
|
|
if (mesh_host_get_data_path(host, "state.bin", path, sizeof(path)) != ERROR_NONE) {
|
|
return nullptr;
|
|
}
|
|
return host;
|
|
}
|
|
void loopbackDestroy(void*) {}
|
|
|
|
const char* const loopbackPresets[] = { "Default", "Other", nullptr };
|
|
|
|
error_t loopbackGetRadioProfile(void*, RadioRegion, RadioModulation modulation, size_t preset, MeshRadioProfile* profile) {
|
|
if (modulation == RADIO_MODULATION_ESPNOW) {
|
|
*profile = { .modulation = RADIO_MODULATION_ESPNOW, .power = 20, .channel = 6 };
|
|
return ERROR_NONE;
|
|
}
|
|
*profile = {
|
|
.modulation = RADIO_MODULATION_LORA,
|
|
.frequency = 869525000,
|
|
.bandwidth = 250000,
|
|
.spreading_factor = (preset == 1) ? 7 : 11,
|
|
.coding_rate = 5,
|
|
.sync_word = 0x2B,
|
|
.preamble_length = 16,
|
|
.power = 14,
|
|
};
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
void loopbackOnStart(void*) { startCount++; }
|
|
|
|
// Channel 0xFF stands for frames the backend can't read, 0xFE for adverts
|
|
constexpr uint8_t UNREADABLE_CHANNEL = 0xFF;
|
|
constexpr uint8_t ADVERT_CHANNEL = 0xFE;
|
|
|
|
bool loopbackOnReceive(void* backend, const uint8_t* data, size_t length, float rssi, float snr) {
|
|
auto* host = static_cast<MeshHost*>(backend);
|
|
if (length < 1 || data[0] == UNREADABLE_CHANNEL) {
|
|
return false;
|
|
}
|
|
if (data[0] == ADVERT_CHANNEL) {
|
|
MeshAddress from = { .length = 1, .bytes = { 0x43 } };
|
|
mesh_host_advert_heard(host, &from, "advertiser", rssi, snr);
|
|
return true;
|
|
}
|
|
std::string text(reinterpret_cast<const char*>(data + 1), length - 1);
|
|
MeshAddress from = { .length = 1, .bytes = { 0x42 } };
|
|
mesh_host_node_heard(host, &from, "peer", rssi, snr);
|
|
mesh_host_receive_text(host, &from, data[0], "peer", text.c_str(), rssi, snr, 0);
|
|
return true;
|
|
}
|
|
|
|
void loopbackOnTick(void*) {}
|
|
|
|
error_t loopbackGetContactUri(void*, const MeshAddress* contact, const char* name, char* uri, size_t uriSize) {
|
|
snprintf(uri, uriSize, "loopback://%s/%02x", name, contact != nullptr ? contact->bytes[0] : 0);
|
|
return ERROR_NONE;
|
|
}
|
|
|
|
bool loopbackIsValidContactAddress(void*, const MeshAddress* address) { return address->length == 1; }
|
|
|
|
std::atomic<int> nameChangeCount = 0;
|
|
void loopbackOnNodeNameChanged(void*) { nameChangeCount++; }
|
|
|
|
error_t loopbackSendText(void* backend, uint32_t seq, const MeshAddress*, uint8_t channel, const char* text) {
|
|
uint8_t frame[1 + MESH_TEXT_MAX_LENGTH];
|
|
frame[0] = channel;
|
|
const size_t length = strlen(text);
|
|
memcpy(frame + 1, text, length);
|
|
return mesh_host_transmit(static_cast<MeshHost*>(backend), frame, length + 1, seq);
|
|
}
|
|
|
|
const MeshBackendApi loopbackApi = {
|
|
.id = "loopback",
|
|
.create = loopbackCreate,
|
|
.destroy = loopbackDestroy,
|
|
.presets = loopbackPresets,
|
|
.get_radio_profile = loopbackGetRadioProfile,
|
|
.on_start = loopbackOnStart,
|
|
.on_receive = loopbackOnReceive,
|
|
.on_tick = loopbackOnTick,
|
|
.send_text = loopbackSendText,
|
|
.get_channels = nullptr,
|
|
.set_channel = nullptr,
|
|
.get_contact_uri = loopbackGetContactUri,
|
|
.is_valid_contact_address = loopbackIsValidContactAddress,
|
|
.on_node_name_changed = loopbackOnNodeNameChanged,
|
|
};
|
|
|
|
// endregion
|
|
|
|
// region A radio that only does ESP-NOW
|
|
|
|
struct EspNowFake {
|
|
bool enabled = false;
|
|
RadioModulation modulation = RADIO_MODULATION_NONE;
|
|
int32_t channel = 0;
|
|
int32_t power = 0;
|
|
};
|
|
EspNowFake espNowFake;
|
|
|
|
error_t espNowGetState(Device*, RadioState* state) {
|
|
*state = espNowFake.enabled ? RADIO_STATE_ON : RADIO_STATE_OFF;
|
|
return ERROR_NONE;
|
|
}
|
|
error_t espNowSetEnabled(Device*, bool enabled) {
|
|
espNowFake.enabled = enabled;
|
|
return ERROR_NONE;
|
|
}
|
|
error_t espNowSetModulation(Device*, RadioModulation modulation) {
|
|
if (modulation != RADIO_MODULATION_ESPNOW) {
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
espNowFake.modulation = modulation;
|
|
return ERROR_NONE;
|
|
}
|
|
error_t espNowGetModulation(Device*, RadioModulation* modulation) {
|
|
*modulation = espNowFake.modulation;
|
|
return ERROR_NONE;
|
|
}
|
|
bool espNowCanDo(Device*, RadioModulation modulation) { return modulation == RADIO_MODULATION_ESPNOW; }
|
|
error_t espNowSetParameter(Device*, RadioParameter parameter, int32_t value) {
|
|
if (parameter == RADIO_PARAMETER_CHANNEL) {
|
|
espNowFake.channel = value;
|
|
return ERROR_NONE;
|
|
}
|
|
if (parameter == RADIO_PARAMETER_POWER) {
|
|
espNowFake.power = value;
|
|
return ERROR_NONE;
|
|
}
|
|
return ERROR_NOT_SUPPORTED;
|
|
}
|
|
error_t espNowGetParameter(Device*, RadioParameter, int32_t*) { return ERROR_NOT_SUPPORTED; }
|
|
error_t espNowTransmit(Device*, const uint8_t*, size_t, RadioTxId*) { return ERROR_NONE; }
|
|
|
|
const RadioApi espNowApi = {
|
|
.get_radio_state = espNowGetState,
|
|
.set_enabled = espNowSetEnabled,
|
|
.set_modulation = espNowSetModulation,
|
|
.get_modulation = espNowGetModulation,
|
|
.can_transmit = espNowCanDo,
|
|
.can_receive = espNowCanDo,
|
|
.set_parameter = espNowSetParameter,
|
|
.get_parameter = espNowGetParameter,
|
|
.transmit = espNowTransmit,
|
|
.get_rssi = nullptr,
|
|
.is_receiving = nullptr,
|
|
};
|
|
|
|
Driver espNowDriver = {
|
|
.name = "mesh_test_espnow_driver",
|
|
.compatible = (const char*[]) { "mesh_test,espnow", nullptr },
|
|
.start_device = nullptr,
|
|
.stop_device = nullptr,
|
|
.api = &espNowApi,
|
|
.device_type = &RADIO_TYPE,
|
|
.internal = nullptr,
|
|
};
|
|
|
|
// endregion
|
|
|
|
bool waitFor(const std::function<bool()>& condition) {
|
|
for (int i = 0; i < 300; i++) {
|
|
if (condition()) {
|
|
return true;
|
|
}
|
|
delay_millis(10);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
MeshDeliveryState getDelivery(uint32_t seq) {
|
|
MeshMessage message {};
|
|
mesh_message_get(seq, &message);
|
|
return message.delivery;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("mesh service sends and receives through the active backend") {
|
|
char dataPath[] = "/tmp/mesh_service_test_XXXXXX";
|
|
REQUIRE_NE(mkdtemp(dataPath), nullptr);
|
|
FileSystem* dataFs = file_system_add("data", &fakeDataApi, dataPath);
|
|
|
|
// The service uses the first radio, the second one plays the remote node
|
|
static Device local { .name = "radio_local", .config = nullptr, .parent = nullptr };
|
|
static Device remote { .name = "radio_remote", .config = nullptr, .parent = nullptr };
|
|
CHECK_FALSE(mesh_is_available());
|
|
REQUIRE_EQ(device_construct_add(&local, "mock,radio"), ERROR_NONE);
|
|
REQUIRE_EQ(device_construct_add(&remote, "mock,radio"), ERROR_NONE);
|
|
REQUIRE_EQ(device_start(&local), ERROR_NONE);
|
|
REQUIRE_EQ(device_start(&remote), ERROR_NONE);
|
|
CHECK(mesh_is_available());
|
|
|
|
REQUIRE_EQ(mesh_backend_register(&loopbackApi), ERROR_NONE);
|
|
CHECK_EQ(mesh_backend_register(&loopbackApi), ERROR_ALREADY_EXISTS);
|
|
REQUIRE_EQ(service_manager_add(&mesh_service_manifest, false), ERROR_NONE);
|
|
|
|
SUBCASE("without a region the radio stays off") {
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
CHECK(waitFor([] { return mesh_get_state() == MESH_STATE_NO_REGION; }));
|
|
CHECK_EQ(mesh_send_text(nullptr, 0, "hi", nullptr), ERROR_INVALID_STATE);
|
|
|
|
// Setting a region later brings the service up
|
|
CHECK_EQ(radio_set_region(&local, RADIO_REGION_EU_868), ERROR_NONE);
|
|
CHECK(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
}
|
|
|
|
SUBCASE("a radio that starts after the service is picked up") {
|
|
REQUIRE_EQ(device_stop(&local), ERROR_NONE);
|
|
REQUIRE_EQ(device_stop(&remote), ERROR_NONE);
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
CHECK(waitFor([] { return mesh_get_state() == MESH_STATE_NO_RADIO; }));
|
|
|
|
REQUIRE_EQ(device_start(&local), ERROR_NONE);
|
|
REQUIRE_EQ(device_start(&remote), ERROR_NONE);
|
|
REQUIRE_EQ(radio_set_region(&local, RADIO_REGION_EU_868), ERROR_NONE);
|
|
CHECK(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
}
|
|
|
|
SUBCASE("selecting a preset reconfigures the radio") {
|
|
REQUIRE_EQ(radio_set_region(&local, RADIO_REGION_EU_868), ERROR_NONE);
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
REQUIRE(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
|
|
char name[MESH_NAME_MAX_LENGTH + 1];
|
|
CHECK_EQ(mesh_get_preset_count(), 2);
|
|
CHECK_EQ(mesh_get_preset_name(1, name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "Other");
|
|
CHECK_EQ(mesh_get_preset_name(2, name, sizeof(name)), ERROR_NOT_FOUND);
|
|
CHECK_EQ(mesh_get_preset(name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "Default");
|
|
|
|
CHECK_EQ(mesh_set_preset("Missing"), ERROR_NOT_FOUND);
|
|
REQUIRE_EQ(mesh_set_preset("Other"), ERROR_NONE);
|
|
CHECK_EQ(mesh_get_preset(name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "Other");
|
|
CHECK(waitFor([] {
|
|
int32_t spreadingFactor = 0;
|
|
return radio_get_parameter(&local, RADIO_PARAMETER_SPREADING_FACTOR, &spreadingFactor) == ERROR_NONE && spreadingFactor == 7;
|
|
}));
|
|
CHECK(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
REQUIRE_EQ(mesh_set_preset("Default"), ERROR_NONE);
|
|
}
|
|
|
|
SUBCASE("a new node name is announced by the backend") {
|
|
REQUIRE_EQ(radio_set_region(&local, RADIO_REGION_EU_868), ERROR_NONE);
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
REQUIRE(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
|
|
// Without a chosen name, a random one is made up once
|
|
char generated[MESH_NAME_MAX_LENGTH + 1];
|
|
CHECK_EQ(mesh_get_node_name(generated, sizeof(generated)), ERROR_NONE);
|
|
const std::string generatedName = generated;
|
|
REQUIRE_EQ(generatedName.size(), std::string("TactilityUser-").size() + 8);
|
|
CHECK_EQ(generatedName.substr(0, 14), "TactilityUser-");
|
|
CHECK_EQ(generatedName.substr(14).find_first_not_of("0123456789abcdef"), std::string::npos);
|
|
// The service keeps its settings for the whole process, so the data folder of this test is read directly
|
|
MeshSettings first;
|
|
MeshSettings second;
|
|
mesh_settings_load(&first);
|
|
mesh_settings_load(&second);
|
|
CHECK_EQ(std::string(first.node_name).substr(0, 14), "TactilityUser-");
|
|
CHECK_EQ(std::string(first.node_name), std::string(second.node_name));
|
|
|
|
const int before = nameChangeCount;
|
|
CHECK_EQ(mesh_set_node_name(""), ERROR_INVALID_ARGUMENT);
|
|
CHECK_EQ(mesh_set_node_name("Ken's deck"), ERROR_NONE);
|
|
char name[MESH_NAME_MAX_LENGTH + 1];
|
|
CHECK_EQ(mesh_get_node_name(name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "Ken's deck");
|
|
CHECK(waitFor([before] { return nameChangeCount > before; }));
|
|
}
|
|
|
|
SUBCASE("the radio can be selected") {
|
|
static Device espNow { .name = "radio_espnow", .config = nullptr, .parent = nullptr };
|
|
espNowFake = {};
|
|
REQUIRE_EQ(driver_construct_add(&espNowDriver), ERROR_NONE);
|
|
REQUIRE_EQ(device_construct_add(&espNow, "mesh_test,espnow"), ERROR_NONE);
|
|
REQUIRE_EQ(device_start(&espNow), ERROR_NONE);
|
|
REQUIRE_EQ(radio_set_region(&local, RADIO_REGION_EU_868), ERROR_NONE);
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
REQUIRE(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
|
|
// The default is the first radio that isn't ESP-NOW
|
|
CHECK_EQ(mesh_get_radio_count(), 3);
|
|
char name[MESH_NAME_MAX_LENGTH + 1];
|
|
CHECK_EQ(mesh_get_radio_name(2, name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "radio_espnow");
|
|
CHECK_EQ(mesh_get_radio_name(3, name, sizeof(name)), ERROR_NOT_FOUND);
|
|
MeshRadioKind kind;
|
|
CHECK_EQ(mesh_get_radio_kind(0, &kind), ERROR_NONE);
|
|
CHECK_EQ(kind, MESH_RADIO_LORA);
|
|
CHECK_EQ(mesh_get_radio_kind(2, &kind), ERROR_NONE);
|
|
CHECK_EQ(kind, MESH_RADIO_ESPNOW);
|
|
CHECK_EQ(mesh_get_radio_kind(3, &kind), ERROR_NOT_FOUND);
|
|
CHECK_EQ(mesh_get_radio(name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "radio_local");
|
|
CHECK_EQ(mesh_get_preset_count(), 2);
|
|
CHECK_FALSE(espNowFake.enabled);
|
|
|
|
CHECK_EQ(mesh_set_radio("missing"), ERROR_NOT_FOUND);
|
|
|
|
// A radio that doesn't do LoRa gets the ESP-NOW profile of the backend, and has no presets
|
|
REQUIRE_EQ(mesh_set_radio("radio_espnow"), ERROR_NONE);
|
|
CHECK(waitFor([] { return espNowFake.enabled && mesh_get_state() == MESH_STATE_ON; }));
|
|
CHECK_EQ(espNowFake.modulation, RADIO_MODULATION_ESPNOW);
|
|
CHECK_EQ(espNowFake.channel, 6);
|
|
CHECK_EQ(espNowFake.power, 20);
|
|
CHECK_EQ(mesh_get_radio(name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "radio_espnow");
|
|
CHECK_EQ(mesh_get_preset_count(), 0);
|
|
CHECK_EQ(radio_acquire(&local, "someone-else"), ERROR_NONE);
|
|
CHECK_EQ(radio_release(&local), ERROR_NONE);
|
|
|
|
// Back to the default
|
|
REQUIRE_EQ(mesh_set_radio(""), ERROR_NONE);
|
|
CHECK(waitFor([] { return !espNowFake.enabled && mesh_get_state() == MESH_STATE_ON; }));
|
|
CHECK_EQ(mesh_get_radio(name, sizeof(name)), ERROR_NONE);
|
|
CHECK_EQ(std::string(name), "radio_local");
|
|
CHECK_EQ(mesh_get_preset_count(), 2);
|
|
|
|
REQUIRE_EQ(service_manager_stop(MESH_SERVICE_ID), ERROR_NONE);
|
|
REQUIRE_EQ(radio_set_region(&espNow, RADIO_REGION_UNSET), ERROR_NONE);
|
|
REQUIRE_EQ(device_stop(&espNow), ERROR_NONE);
|
|
REQUIRE_EQ(device_remove(&espNow), ERROR_NONE);
|
|
REQUIRE_EQ(device_destruct(&espNow), ERROR_NONE);
|
|
REQUIRE_EQ(driver_remove_destruct(&espNowDriver), ERROR_NONE);
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
}
|
|
|
|
SUBCASE("concurrent synchronous calls finish") {
|
|
REQUIRE_EQ(radio_set_region(&local, RADIO_REGION_EU_868), ERROR_NONE);
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
REQUIRE(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
|
|
const auto caller = [](void*) -> int32_t {
|
|
for (int i = 0; i < 200; i++) {
|
|
MeshChannel channels[1];
|
|
size_t count = 0;
|
|
mesh_get_channels(channels, 1, &count);
|
|
mesh_get_state();
|
|
}
|
|
return 0;
|
|
};
|
|
Thread* threads[4];
|
|
for (auto& thread : threads) {
|
|
thread = thread_alloc_full("mesh_caller", 4096, caller, nullptr, -1);
|
|
REQUIRE_EQ(thread_start(thread), ERROR_NONE);
|
|
}
|
|
for (auto* thread : threads) {
|
|
CHECK_EQ(thread_join(thread, pdMS_TO_TICKS(5000), 1), ERROR_NONE);
|
|
thread_free(thread);
|
|
}
|
|
}
|
|
|
|
SUBCASE("messages travel both ways") {
|
|
REQUIRE_EQ(radio_set_region(&local, RADIO_REGION_EU_868), ERROR_NONE);
|
|
REQUIRE_EQ(service_manager_start(MESH_SERVICE_ID), ERROR_NONE);
|
|
REQUIRE(waitFor([] { return mesh_get_state() == MESH_STATE_ON; }));
|
|
|
|
char backend[MESH_BACKEND_ID_MAX_LENGTH + 1];
|
|
CHECK_EQ(mesh_get_backend(backend, sizeof(backend)), ERROR_NONE);
|
|
CHECK_EQ(std::string(backend), "loopback");
|
|
|
|
// The service owns the radio
|
|
CHECK_EQ(radio_acquire(&local, "someone-else"), ERROR_RESOURCE_BUSY);
|
|
|
|
// Tune the remote radio like the backend profile
|
|
MeshRadioProfile profile;
|
|
loopbackGetRadioProfile(nullptr, RADIO_REGION_EU_868, RADIO_MODULATION_LORA, 0, &profile);
|
|
REQUIRE_EQ(radio_set_modulation(&remote, profile.modulation), ERROR_NONE);
|
|
radio_set_parameter(&remote, RADIO_PARAMETER_FREQUENCY, profile.frequency);
|
|
radio_set_parameter(&remote, RADIO_PARAMETER_BANDWIDTH, profile.bandwidth);
|
|
radio_set_parameter(&remote, RADIO_PARAMETER_SPREADING_FACTOR, profile.spreading_factor);
|
|
radio_set_parameter(&remote, RADIO_PARAMETER_SYNC_WORD, profile.sync_word);
|
|
REQUIRE_EQ(radio_set_enabled(&remote, true), ERROR_NONE);
|
|
|
|
TaskEventGroup group {};
|
|
task_event_group_construct(&group);
|
|
static RadioRxEventSubscription remoteRx {};
|
|
REQUIRE_EQ(radio_rx_event_subscribe(&remoteRx, &group, &remote), ERROR_NONE);
|
|
MeshEventSubscription meshEvents {};
|
|
REQUIRE_EQ(mesh_event_subscribe(&meshEvents, &group), ERROR_NONE);
|
|
|
|
// Outgoing
|
|
uint32_t seq = 0;
|
|
REQUIRE_EQ(mesh_send_text(nullptr, 3, "hello mesh", &seq), ERROR_NONE);
|
|
CHECK(waitFor([seq] { return getDelivery(seq) == MESH_DELIVERY_SENT; }));
|
|
|
|
static RadioRxEvent rxEvent;
|
|
REQUIRE_EQ(radio_rx_event_poll(&remoteRx, &rxEvent), ERROR_NONE);
|
|
CHECK_EQ(rxEvent.data[0], 3);
|
|
CHECK_EQ(std::string(reinterpret_cast<const char*>(rxEvent.data + 1), rxEvent.length - 1), "hello mesh");
|
|
|
|
// Incoming
|
|
const uint8_t reply[] = { 3, 'h', 'i', '!' };
|
|
REQUIRE_EQ(radio_transmit(&remote, reply, sizeof(reply), nullptr), ERROR_NONE);
|
|
MeshMessage history[4];
|
|
size_t count = 0;
|
|
CHECK(waitFor([&] {
|
|
return mesh_history_get(0, history, 4, &count) == ERROR_NONE && count == 2;
|
|
}));
|
|
CHECK_FALSE(history[0].outgoing);
|
|
CHECK_EQ(std::string(history[0].text), "hi!");
|
|
CHECK_EQ(std::string(history[0].sender_name), "peer");
|
|
CHECK_EQ(history[0].channel, 3);
|
|
CHECK_EQ(history[0].delivery, MESH_DELIVERY_RECEIVED);
|
|
CHECK(history[1].outgoing);
|
|
|
|
MeshNode nodes[4];
|
|
CHECK_EQ(mesh_get_nodes(nodes, 4, &count), ERROR_NONE);
|
|
CHECK_EQ(count, 1);
|
|
CHECK_EQ(std::string(nodes[0].name), "peer");
|
|
|
|
// Events: state, message, delivery and node events were queued
|
|
MeshEvent event {};
|
|
bool sawMessage = false;
|
|
bool sawDelivery = false;
|
|
while (mesh_event_poll(&meshEvents, &event) == ERROR_NONE) {
|
|
sawMessage |= event.type == MESH_EVENT_MESSAGE;
|
|
sawDelivery |= event.type == MESH_EVENT_DELIVERY;
|
|
}
|
|
CHECK(sawMessage);
|
|
CHECK(sawDelivery);
|
|
|
|
// Frames that aren't messages are only reported as events
|
|
const uint8_t unreadable[] = { UNREADABLE_CHANNEL, 1, 2, 3, 4 };
|
|
REQUIRE_EQ(radio_transmit(&remote, unreadable, sizeof(unreadable), nullptr), ERROR_NONE);
|
|
const uint8_t advert[] = { ADVERT_CHANNEL };
|
|
REQUIRE_EQ(radio_transmit(&remote, advert, sizeof(advert), nullptr), ERROR_NONE);
|
|
bool sawUnreadable = false;
|
|
bool sawAdvert = false;
|
|
CHECK(waitFor([&] {
|
|
while (mesh_event_poll(&meshEvents, &event) == ERROR_NONE) {
|
|
if (event.type == MESH_EVENT_FRAME && event.frame.kind == MESH_FRAME_UNREADABLE) {
|
|
sawUnreadable = event.frame.length == sizeof(unreadable);
|
|
}
|
|
if (event.type == MESH_EVENT_FRAME && event.frame.kind == MESH_FRAME_ADVERT) {
|
|
sawAdvert = std::string(event.frame.name) == "advertiser" && event.frame.from.bytes[0] == 0x43;
|
|
}
|
|
}
|
|
return sawUnreadable && sawAdvert;
|
|
}));
|
|
CHECK(mesh_history_get(0, history, 4, &count) == ERROR_NONE);
|
|
CHECK_EQ(count, 2);
|
|
|
|
// The sender of the incoming message was saved as a contact
|
|
MeshContact contacts[4];
|
|
REQUIRE_EQ(mesh_get_contacts(contacts, 4, &count), ERROR_NONE);
|
|
REQUIRE_EQ(count, 1);
|
|
CHECK_EQ(std::string(contacts[0].name), "peer");
|
|
CHECK_EQ(std::string(contacts[0].backend), "loopback");
|
|
|
|
// Contacts can be added, renamed and removed
|
|
const MeshAddress alice = { .length = 1, .bytes = { 0x50 } };
|
|
const MeshAddress unknown = { .length = 1, .bytes = { 0x51 } };
|
|
const MeshAddress tooLong = { .length = 2, .bytes = { 0x52, 0x53 } };
|
|
CHECK_EQ(mesh_add_contact(&tooLong, "bad"), ERROR_INVALID_ARGUMENT);
|
|
CHECK_EQ(mesh_add_contact(&alice, "alice"), ERROR_NONE);
|
|
CHECK_EQ(mesh_add_contact(&alice, nullptr), ERROR_NONE);
|
|
CHECK_EQ(mesh_add_contact(&unknown, nullptr), ERROR_NONE);
|
|
REQUIRE_EQ(mesh_get_contacts(contacts, 4, &count), ERROR_NONE);
|
|
REQUIRE_EQ(count, 3);
|
|
CHECK_EQ(std::string(contacts[1].name), "alice");
|
|
// Without a name the address identifies the contact
|
|
CHECK_EQ(std::string(contacts[2].name), "51");
|
|
|
|
// Conversations only hold their own messages
|
|
uint32_t directSeq = 0;
|
|
uint32_t channelSeq = 0;
|
|
REQUIRE_EQ(mesh_send_text(&alice, 0, "hi alice", &directSeq), ERROR_NONE);
|
|
REQUIRE_EQ(mesh_send_text(nullptr, 5, "hello channel", &channelSeq), ERROR_NONE);
|
|
MeshMessage conversation[4];
|
|
CHECK_EQ(mesh_conversation_get(&alice, 0, 0, conversation, 4, &count), ERROR_NONE);
|
|
REQUIRE_EQ(count, 1);
|
|
CHECK_EQ(conversation[0].seq, directSeq);
|
|
CHECK_EQ(std::string(conversation[0].text), "hi alice");
|
|
CHECK_EQ(mesh_conversation_get(nullptr, 5, 0, conversation, 4, &count), ERROR_NONE);
|
|
REQUIRE_EQ(count, 1);
|
|
CHECK_EQ(conversation[0].seq, channelSeq);
|
|
CHECK_EQ(mesh_conversation_get(nullptr, 6, 0, conversation, 4, &count), ERROR_NONE);
|
|
CHECK_EQ(count, 0);
|
|
CHECK_EQ(mesh_conversation_get(&unknown, 0, 0, conversation, 4, &count), ERROR_NONE);
|
|
CHECK_EQ(count, 0);
|
|
// The delivery state shows how far the message got
|
|
CHECK(waitFor([directSeq] { return getDelivery(directSeq) == MESH_DELIVERY_SENT; }));
|
|
|
|
// The URI of a saved contact and of this node come from the backend
|
|
char uri[64];
|
|
CHECK_EQ(mesh_get_contact_uri(&alice, uri, sizeof(uri)), ERROR_NONE);
|
|
CHECK_EQ(std::string(uri), "loopback://alice/50");
|
|
CHECK_EQ(mesh_get_contact_uri(nullptr, uri, sizeof(uri)), ERROR_NONE);
|
|
CHECK_EQ(std::string(uri).substr(0, 11), "loopback://");
|
|
CHECK_EQ(mesh_get_contact_uri(&unknown, uri, sizeof(uri)), ERROR_NONE);
|
|
const MeshAddress stranger = { .length = 1, .bytes = { 0x77 } };
|
|
CHECK_EQ(mesh_get_contact_uri(&stranger, uri, sizeof(uri)), ERROR_NOT_FOUND);
|
|
|
|
CHECK_EQ(mesh_remove_contact(&alice), ERROR_NONE);
|
|
CHECK_EQ(mesh_remove_contact(&alice), ERROR_NOT_FOUND);
|
|
REQUIRE_EQ(mesh_get_contacts(contacts, 4, &count), ERROR_NONE);
|
|
CHECK_EQ(count, 2);
|
|
// The messages remain
|
|
CHECK_EQ(mesh_conversation_get(&alice, 0, 0, conversation, 4, &count), ERROR_NONE);
|
|
CHECK_EQ(count, 1);
|
|
|
|
// The primary channel stays, and the loopback backend has no channels
|
|
CHECK_EQ(mesh_remove_channel(0), ERROR_INVALID_ARGUMENT);
|
|
CHECK_EQ(mesh_remove_channel(2), ERROR_NOT_SUPPORTED);
|
|
|
|
CHECK_EQ(mesh_cancel(seq), ERROR_INVALID_STATE);
|
|
CHECK_EQ(mesh_raw_send("other", reply, sizeof(reply)), ERROR_INVALID_STATE);
|
|
CHECK_EQ(mesh_raw_send("loopback", reply, sizeof(reply)), ERROR_NONE);
|
|
|
|
CHECK_EQ(mesh_event_unsubscribe(&meshEvents), ERROR_NONE);
|
|
CHECK_EQ(radio_rx_event_unsubscribe(&remoteRx), ERROR_NONE);
|
|
task_event_group_destruct(&group);
|
|
}
|
|
|
|
CHECK_EQ(service_manager_stop(MESH_SERVICE_ID), ERROR_NONE);
|
|
CHECK_EQ(mesh_get_state(), MESH_STATE_OFF);
|
|
// Released on stop
|
|
CHECK_EQ(radio_acquire(&local, "someone-else"), ERROR_NONE);
|
|
CHECK_EQ(radio_release(&local), ERROR_NONE);
|
|
|
|
CHECK_EQ(service_manager_remove(MESH_SERVICE_ID), ERROR_NONE);
|
|
CHECK_EQ(mesh_backend_unregister(&loopbackApi), ERROR_NONE);
|
|
radio_set_region(&local, RADIO_REGION_UNSET);
|
|
radio_set_enabled(&remote, false);
|
|
device_stop(&remote);
|
|
device_stop(&local);
|
|
device_remove(&remote);
|
|
device_remove(&local);
|
|
device_destruct(&remote);
|
|
device_destruct(&local);
|
|
file_system_remove(dataFs);
|
|
}
|