Fix WisBlock GPS power policy and preserve shared peripherals
This commit is contained in:
1 parent
9079d6f47e
commit
368770656f
26 files changed
+1830
-107
No files matched your search
@@ -459,6 +459,8 @@ jobs:
|
||||
python3 -B test/test_esp32_gps_discovery.py -v
|
||||
python3 -B test/test_gps_sync_interval.py -v
|
||||
python3 -B test/test_gps_sync_preferences.py -v
|
||||
python3 -B test/test_rak_gps_power.py -v
|
||||
python3 -B test/test_peripheral_mode_prefs.py -v
|
||||
python3 -B test/test_t1000e_sensor_power.py -v
|
||||
python3 -B test/test_sensor_battery_alerts.py -v
|
||||
python3 -B test/test_sensor_subscriptions.py -v
|
||||
|
||||
+42
-3
@@ -4715,12 +4715,51 @@ clear recent.repeater
|
||||
|
||||
**Note:** These command names are shared with GPS-capable Companion builds.
|
||||
Companion reports `on` or `off`; infrastructure includes receiver details:
|
||||
- `off` when the GPS hardware is disabled
|
||||
- `on, {active|deactivated}, {fix|no fix}, {sat count} sats` when the GPS hardware is enabled
|
||||
- `off, {powered|unpowered}` when GPS is manually disabled
|
||||
- `on, {powered|unpowered}, {fix|no fix}, {sat count} sats` when GPS is enabled
|
||||
|
||||
`powered` describes the supply, not proof that the receiver is tracking or
|
||||
asleep. Shared-power hardware can remain powered while GPS processing is off.
|
||||
|
||||
On repeaters, `gps off` cancels any acquisition already in progress. A later
|
||||
authorized location-telemetry request may wake the receiver, but only until its
|
||||
first valid fix is cached; the receiver then powers down again.
|
||||
first valid fix is cached; GPS processing then stops again. Whether its supply
|
||||
can also turn off depends on the board's power policy below.
|
||||
|
||||
---
|
||||
|
||||
#### RAK4631 shared peripheral power
|
||||
|
||||
**Usage:**
|
||||
|
||||
```text
|
||||
get peripherals.mode
|
||||
set peripherals.mode gps-only
|
||||
set peripherals.mode shared
|
||||
reboot
|
||||
```
|
||||
|
||||
**Default:** `gps-only` on RAK4631. The saved mode takes effect only after
|
||||
reboot; setting it does not switch power immediately. `get peripherals.mode`
|
||||
reports the saved choice, which may differ from the current boot's mode.
|
||||
|
||||
- `gps-only`: when no protected power owner exists, GPS-off and GPS sleep can
|
||||
turn off the shared WisBlock `3V3_S` supply. Select this only when other
|
||||
attached peripherals do not need that supply.
|
||||
- `shared`: keeps `3V3_S` powered for other peripherals. Save this mode and
|
||||
reboot when using other WisBlock modules; firmware does not infer this choice
|
||||
from an I2C scan.
|
||||
|
||||
Ethernet/PoE builds and detected external NOR storage automatically keep the
|
||||
rail on, even in `gps-only` mode or when the network service is disabled.
|
||||
RAK3401 permanently protects the same rail because its radio frontend needs
|
||||
it; `peripherals.mode` is a RAK4631 setting, not an override of that protection.
|
||||
|
||||
On a powered shared rail, RAK12501/L76K cannot be independently switched off:
|
||||
GPS-off stops MCU parsing, not physical GNSS operation. Other receivers may use
|
||||
internal power-saving commands, but `shared` does not promise physical sleep.
|
||||
UART1 bridge ownership remains protected; changing this mode does not grant
|
||||
permission to connect two transmitters to the GPS UART.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+36
-2
@@ -50,10 +50,11 @@ GPS on, and manual GPS-on still keeps the cache updated.
|
||||
|
||||
Repeaters use a shorter on-demand policy when GPS is manually off. An authorized
|
||||
location-telemetry request wakes the receiver only when its cached position is
|
||||
stale. The first valid fix is cached and the receiver immediately powers down;
|
||||
stale. The first valid fix is cached and GPS processing immediately stops;
|
||||
the next request can return that fix without another wake. `gps off` also
|
||||
cancels an acquisition already in progress. An acquisition that never obtains a
|
||||
fix retains the normal 15-minute safety timeout.
|
||||
fix retains the normal 15-minute safety timeout. Physical supply switching
|
||||
depends on the board and its shared peripheral policy.
|
||||
|
||||
When GPS and device power saving are both enabled, a repeater wakes GPS for up
|
||||
to 10 minutes to obtain a valid time and then sleeps it for seven days. A valid
|
||||
@@ -61,6 +62,39 @@ GPS time may correct the RTC either forward or backward. With device power
|
||||
saving off, an enabled GPS remains active and refreshes the RTC every 30
|
||||
minutes.
|
||||
|
||||
## RAK WisBlock Power Supply
|
||||
|
||||
RAK4631 defaults to saved peripheral mode `gps-only`. With no protected power
|
||||
owner, stopping or sleeping GPS can turn off the shared `3V3_S` rail. If other
|
||||
attached modules need that rail, explicitly select shared mode:
|
||||
|
||||
```text
|
||||
get peripherals.mode
|
||||
set peripherals.mode shared
|
||||
reboot
|
||||
```
|
||||
|
||||
Use `set peripherals.mode gps-only` and reboot to return to the default.
|
||||
Changes are saved for the next boot, not applied live; the query reports the
|
||||
saved choice. Other peripheral requirements are not guessed from an I2C scan.
|
||||
Boot and discovery keep the rail on until the power policy is established.
|
||||
|
||||
This saved setting is available in Companion and Common CLI roles (repeater,
|
||||
room server, and sensor). Raw KISS and legacy Terminal Chat do not configure
|
||||
it and retain the powered rail. Shared mode controls the supply; it does not
|
||||
add Ethernet support to a non-Ethernet firmware target.
|
||||
|
||||
Ethernet/PoE builds and detected external NOR storage automatically retain
|
||||
power regardless of this setting. RAK3401 always retains the rail for its radio
|
||||
frontend and does not offer this override.
|
||||
|
||||
In shared mode, GPS-off does not mean the supply is off. In particular, the
|
||||
[RAK12501/L76K module](https://docs.rakwireless.com/product-categories/wisblock/rak12501/datasheet/)
|
||||
has no independent power switch: stopping MCU parsing leaves the receiver
|
||||
powered. Internal sleep support on other GNSS modules is receiver-dependent.
|
||||
This setting does not remove the UART1 bridge conflict or authorize another
|
||||
transmitter on the GPS wiring.
|
||||
|
||||
## Companion/Client Nodes
|
||||
|
||||
Companion/client telemetry uses the existing companion telemetry permission
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <new>
|
||||
#include "DataStore.h"
|
||||
#include "PrefsStorageLayout.h"
|
||||
#if defined(RAK_4631)
|
||||
#include <helpers/PeripheralModePrefs.h>
|
||||
#endif
|
||||
#include <helpers/FileRead.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <helpers/LazyPersistence.h>
|
||||
@@ -1104,6 +1107,9 @@ bool DataStore::loadPrefsInt(const char *filename,
|
||||
loaded_prefs.usb_debug_enabled = 0;
|
||||
// Old images cannot opt in using an unrelated runtime preference value.
|
||||
loaded_prefs.lost_reply = 0;
|
||||
#if defined(RAK_4631)
|
||||
loaded_prefs.peripherals_shared = 0;
|
||||
#endif
|
||||
double loaded_lat = node_lat;
|
||||
double loaded_lon = node_lon;
|
||||
// The original image ended after ble_pin at byte 84. Later releases only
|
||||
@@ -1197,6 +1203,11 @@ bool DataStore::loadPrefsInt(const char *filename,
|
||||
break;
|
||||
}
|
||||
}
|
||||
#if defined(RAK_4631)
|
||||
// Only the RAK4631 publishes this footer. Other targets retain their
|
||||
// exact existing image sizes, including the compact STM32 wire table.
|
||||
known_size = known_size || prefs_size == 236 + mesh::peripherals::FooterSize;
|
||||
#endif
|
||||
if (prefs_size < MIN_PREFS_SIZE || !known_size) {
|
||||
file.close();
|
||||
return false;
|
||||
@@ -1348,6 +1359,14 @@ bool DataStore::loadPrefsInt(const char *filename,
|
||||
readOptionalField(&loaded_prefs.usb_debug_enabled,
|
||||
sizeof(loaded_prefs.usb_debug_enabled));
|
||||
readOptionalField(&loaded_prefs.lost_reply, sizeof(loaded_prefs.lost_reply));
|
||||
#if defined(RAK_4631)
|
||||
if (file.available() != 0) {
|
||||
uint8_t peripheral_footer[mesh::peripherals::FooterSize] = {};
|
||||
success = success && readField(peripheral_footer, sizeof(peripheral_footer))
|
||||
&& mesh::peripherals::decodeFooter(peripheral_footer,
|
||||
sizeof(peripheral_footer), loaded_prefs.peripherals_shared);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Any bytes left over form only part of a historically appended field.
|
||||
@@ -1543,6 +1562,11 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou
|
||||
memcpy(tail + 6, &_prefs.gps_sync_interval_hours,
|
||||
sizeof(_prefs.gps_sync_interval_hours));
|
||||
success = success && writeField(tail, sizeof(tail));
|
||||
#if defined(RAK_4631)
|
||||
uint8_t peripheral_footer[mesh::peripherals::FooterSize];
|
||||
mesh::peripherals::encodeFooter(peripheral_footer, _prefs.peripherals_shared);
|
||||
success = success && writeField(peripheral_footer, sizeof(peripheral_footer));
|
||||
#endif
|
||||
|
||||
#if defined(STM32_PLATFORM)
|
||||
success = success && image_length == sizeof(image)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#include <helpers/MorseCLI.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#if defined(RAK_4631)
|
||||
#include <helpers/PeripheralModePrefs.h>
|
||||
#endif
|
||||
#include <helpers/ui/StartupScreen.h>
|
||||
#include "NotificationSettingsFile.h"
|
||||
#include "MyMesh.h"
|
||||
@@ -2758,6 +2761,9 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_prefs.gps_enabled = 0; // GPS disabled by default
|
||||
_prefs.gps_interval = 0; // Use the default 1-second fix-processing interval
|
||||
_prefs.gps_sync_interval_hours = 0; // Retain the board's default time-sync cadence
|
||||
#if defined(RAK_4631)
|
||||
_prefs.peripherals_shared = 0; // A genuinely fresh missing image is GPS-only.
|
||||
#endif
|
||||
_prefs.autoadd_config = DEFAULT_AUTOADD_CONFIG;
|
||||
_prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE;
|
||||
#ifdef RADIO_FEM_TXGAIN
|
||||
@@ -2934,6 +2940,12 @@ void MyMesh::begin(bool has_display, bool radio_available,
|
||||
// load persisted prefs
|
||||
const bool prefs_ready =
|
||||
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
|
||||
#if defined(RAK_4631)
|
||||
// Only a verified image (or a fresh missing file) may permit GPS-only power.
|
||||
// A failed load never replaces a stored shared policy with an unsafe cut.
|
||||
if (!prefs_ready) _prefs.peripherals_shared = 1;
|
||||
sensors.configureSharedPeripheralMode(_prefs.peripherals_shared != 0);
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// A volatile primary filesystem is the final recovery mode after the
|
||||
@@ -3654,6 +3666,19 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
|
||||
size_t reply_size) {
|
||||
if (!command || !reply || reply_size == 0) return false;
|
||||
while (*command == ' ') command++;
|
||||
#if defined(RAK_4631)
|
||||
// This helper is also reached only after normal remote-admin admission.
|
||||
if (mesh::peripherals::handleCommand(_prefs.peripherals_shared, command,
|
||||
reply, reply_size, sensors.supportsSharedPeripheralMode(),
|
||||
[this]() { return savePrefs(); })) return true;
|
||||
#else
|
||||
if (strcmp(command, "get peripherals.mode") == 0
|
||||
|| (strncmp(command, "set peripherals.mode", 20) == 0
|
||||
&& (command[20] == 0 || command[20] == ' ' || command[20] == '\t'))) {
|
||||
snprintf(reply, reply_size, "Error: unsupported");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strcmp(command, "get public.key") == 0) {
|
||||
char public_key_hex[PUB_KEY_SIZE * 2 + 1];
|
||||
|
||||
@@ -117,6 +117,10 @@ public:
|
||||
uint8_t usb_debug_enabled = 0;
|
||||
// Appended opt-in private-message response: 0=off, 1=no, 2=yes.
|
||||
uint8_t lost_reply = 0;
|
||||
#if defined(RAK_4631)
|
||||
// A checked footer follows every existing on-flash field, never an insertion.
|
||||
uint8_t peripherals_shared = 0; // applied at boot, not on a live CLI save
|
||||
#endif
|
||||
|
||||
private:
|
||||
class RadioPrefs : public CommonRadioPrefs {
|
||||
@@ -255,10 +259,15 @@ public:
|
||||
// untouched, and leave a normally constructed snapshot's adapters local.
|
||||
bool copyPersistedValuesFrom(const CompanionNodePrefs& source) {
|
||||
if (&source == this) return true;
|
||||
#if defined(RAK_4631)
|
||||
const auto& last_value = peripherals_shared;
|
||||
#else
|
||||
const auto& last_value = lost_reply;
|
||||
#endif
|
||||
static_assert(sizeof(lost_reply) == sizeof(uint8_t),
|
||||
"The published lost reply preference is one byte");
|
||||
const uintptr_t first = reinterpret_cast<uintptr_t>(&airtime_factor);
|
||||
const uintptr_t last = reinterpret_cast<uintptr_t>(&lost_reply);
|
||||
const uintptr_t last = reinterpret_cast<uintptr_t>(&last_value);
|
||||
if (last < first
|
||||
|| last - first > sizeof(*this) - sizeof(lost_reply)) {
|
||||
return false;
|
||||
@@ -269,8 +278,13 @@ public:
|
||||
const uintptr_t object = reinterpret_cast<uintptr_t>(&values);
|
||||
const uintptr_t begin =
|
||||
reinterpret_cast<uintptr_t>(&values.airtime_factor);
|
||||
const uintptr_t end =
|
||||
reinterpret_cast<uintptr_t>(&values.lost_reply);
|
||||
const uintptr_t end = reinterpret_cast<uintptr_t>(
|
||||
#if defined(RAK_4631)
|
||||
&values.peripherals_shared
|
||||
#else
|
||||
&values.lost_reply
|
||||
#endif
|
||||
);
|
||||
const uintptr_t radio_begin = reinterpret_cast<uintptr_t>(&values.radio);
|
||||
const uintptr_t custom_begin = reinterpret_cast<uintptr_t>(&values.custom);
|
||||
// Subtractions happen only after the corresponding ordering checks.
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <helpers/BatteryChargeCLI.h>
|
||||
#include <helpers/MorseCLI.h>
|
||||
#include <helpers/MorseLoopPolicy.h>
|
||||
#if defined(RAK_4631)
|
||||
#include <helpers/PeripheralModePrefs.h>
|
||||
#endif
|
||||
#include "CLICommandUtils.h"
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
#include "bridges/RS232PowerSettings.h"
|
||||
@@ -811,6 +814,9 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
#endif
|
||||
_prefs->bridge_format = mesh::bridge::ESPNOW_FORMAT_WRAPPED;
|
||||
_prefs->gps_sync_interval_hours = 0;
|
||||
#if defined(RAK_4631)
|
||||
_prefs->peripherals_shared = 0;
|
||||
#endif
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool node_prefs_needs_migration = false;
|
||||
@@ -1017,6 +1023,10 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
if (loaded) syncOtaConfigFromPrefs(); // persisted OTA policy/keys -> OtaContext (else keep safe defaults)
|
||||
#endif
|
||||
mesh::hilStartupTrace("prefs_apply_begin");
|
||||
#if defined(RAK_4631)
|
||||
// Boot-time only. Later CLI saves must not cut a live peripheral/flash rail.
|
||||
_sensors->configureSharedPeripheralMode(_prefs->peripherals_shared != 0);
|
||||
#endif
|
||||
#if MESH_USB_LOGGING_AVAILABLE
|
||||
mesh::setUsbDebugEnabled(_prefs->usb_debug_enabled != 0);
|
||||
mesh::setUsbLoggingEnabled(_prefs->usb_logging_enabled != 0);
|
||||
@@ -1057,6 +1067,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->gps_sync_interval_hours = 0;
|
||||
_prefs->ota_channel = 0;
|
||||
_prefs->rs232_bridge_enabled = 0;
|
||||
#if defined(RAK_4631)
|
||||
// Failure to read an existing policy cannot authorize a shared-rail cut.
|
||||
_prefs->peripherals_shared = 1;
|
||||
#endif
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
_prefs->rs232_heartbeat = 1;
|
||||
_prefs->rs232_autosleep = 1;
|
||||
@@ -1753,6 +1767,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
&_prefs->rx_ps_rx_us,
|
||||
&_prefs->rx_ps_sleep_us); // retune level-based timings to the loaded SF/BW
|
||||
|
||||
// Read the footer independently of optional legacy UART tails/profile.
|
||||
#if defined(RAK_4631)
|
||||
_prefs->peripherals_shared = mesh::peripherals::readCommonMode(file);
|
||||
#endif
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
@@ -1918,6 +1936,11 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
prefs->rs232_idle_seconds);
|
||||
WRITE_COMMON_PREFS_BYTES(power_tail, sizeof(power_tail));
|
||||
#endif
|
||||
#if defined(RAK_4631)
|
||||
uint8_t peripheral_footer[mesh::peripherals::FooterSize];
|
||||
mesh::peripherals::encodeFooter(peripheral_footer, prefs->peripherals_shared);
|
||||
WRITE_COMMON_PREFS_BYTES(peripheral_footer, sizeof(peripheral_footer));
|
||||
#endif
|
||||
|
||||
#undef WRITE_COMMON_PREFS_BYTES
|
||||
#undef WRITE_COMMON_PREFS
|
||||
@@ -2110,6 +2133,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
_prefs->rs232_idle_seconds);
|
||||
file.write(power_tail, sizeof(power_tail));
|
||||
#endif
|
||||
#if defined(RAK_4631)
|
||||
uint8_t peripheral_footer[mesh::peripherals::FooterSize];
|
||||
mesh::peripherals::encodeFooter(peripheral_footer, _prefs->peripherals_shared);
|
||||
file.write(peripheral_footer, sizeof(peripheral_footer));
|
||||
#endif
|
||||
|
||||
_common_save_succeeded = file.commit();
|
||||
if (!_common_save_succeeded) {
|
||||
@@ -3451,7 +3479,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
} else if (memcmp(command, "gps", 3) == 0) {
|
||||
LocationProvider * l = _sensors->getLocationProvider();
|
||||
if (l != NULL) {
|
||||
bool enabled = l->isEnabled(); // is EN pin on ?
|
||||
bool enabled = _sensors->isGpsPowered(); // physical rail/enable policy
|
||||
bool fix = l->isValid(); // has fix ?
|
||||
int sats = l->satellitesCount();
|
||||
const char* gps_setting = _sensors->getSettingByKey("gps");
|
||||
@@ -3737,6 +3765,17 @@ bool CommonCLI::handleSdCardGetCmd(const char* config, char* reply) {
|
||||
|
||||
void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
#if defined(RAK_4631)
|
||||
if (mesh::peripherals::handleCommand(_prefs->peripherals_shared, command,
|
||||
reply, 160, _sensors->supportsSharedPeripheralMode(),
|
||||
[this]() { return trySavePrefs(); })) return;
|
||||
#else
|
||||
if (strncmp(config, "peripherals.mode", 16) == 0
|
||||
&& (config[16] == 0 || config[16] == ' ' || config[16] == '\t')) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (strncmp(config, "repeat.trace", 12) == 0
|
||||
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
|
||||
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
|
||||
@@ -5280,6 +5319,16 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
|
||||
void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* reply) {
|
||||
const char* config = &command[4];
|
||||
#if defined(RAK_4631)
|
||||
if (mesh::peripherals::handleCommand(_prefs->peripherals_shared, command,
|
||||
reply, 160, _sensors->supportsSharedPeripheralMode(),
|
||||
[this]() { return trySavePrefs(); })) return;
|
||||
#else
|
||||
if (strcmp(config, "peripherals.mode") == 0) {
|
||||
strcpy(reply, "Error: unsupported");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (strncmp(config, "repeat.trace", 12) == 0
|
||||
&& (config[12] == 0 || config[12] == ' ' || config[12] == '\t')) {
|
||||
if (strcmp(_callbacks->getRole(), "repeater") != 0) {
|
||||
|
||||
@@ -278,6 +278,10 @@ public:
|
||||
uint8_t flood_retry_group_max_path = 0; // PAYLOAD_TYPE_GRP_DATA retry path gate; 0xFF = use only the general gate
|
||||
uint8_t rx_watchdog_enabled = mesh::RepeaterRadioTiming::DEFAULT_RX_WATCHDOG_ENABLED;
|
||||
uint8_t system_watchdog_enabled = 0; // nRF52 main-loop hardware watchdog (boolean; default on)
|
||||
#if defined(RAK_4631)
|
||||
// Checked append-only file footer; never shifts existing preference offsets.
|
||||
uint8_t peripherals_shared = 0; // 0=GPS-only, 1=shared; applied only at boot
|
||||
#endif
|
||||
|
||||
private:
|
||||
class RadioPrefs : public CommonRadioPrefs {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace peripherals {
|
||||
|
||||
// RAK4631 only: append this checked policy after every previously published
|
||||
// preference byte. Missing historical policy means GPS-only; an unreadable or
|
||||
// damaged new policy must retain the shared supply instead of removing power.
|
||||
static constexpr size_t FooterSize = 8;
|
||||
static constexpr size_t CommonLegacySize = 877;
|
||||
static constexpr size_t LegacyRs232TailSize = 10;
|
||||
|
||||
inline uint16_t checksum(const uint8_t* bytes, size_t length) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
crc ^= static_cast<uint16_t>(bytes[i]) << 8;
|
||||
for (unsigned bit = 0; bit < 8; ++bit)
|
||||
crc = (crc & 0x8000) ? static_cast<uint16_t>((crc << 1) ^ 0x1021)
|
||||
: static_cast<uint16_t>(crc << 1);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
inline void encodeFooter(uint8_t out[FooterSize], uint8_t shared) {
|
||||
out[0] = 'P'; out[1] = 'M'; out[2] = 'O'; out[3] = 1;
|
||||
out[4] = shared == 0 ? 0 : 1;
|
||||
out[5] = 0;
|
||||
const uint16_t crc = checksum(out, 6);
|
||||
out[6] = static_cast<uint8_t>(crc >> 8);
|
||||
out[7] = static_cast<uint8_t>(crc);
|
||||
}
|
||||
|
||||
inline bool decodeFooter(const uint8_t* in, size_t size, uint8_t& shared) {
|
||||
if (!in || size != FooterSize || in[0] != 'P' || in[1] != 'M'
|
||||
|| in[2] != 'O' || in[3] != 1 || in[4] > 1 || in[5] != 0
|
||||
|| checksum(in, 6) != static_cast<uint16_t>((in[6] << 8) | in[7]))
|
||||
return false;
|
||||
shared = in[4];
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename File> uint8_t readCommonMode(File& file) {
|
||||
const size_t size = file.size();
|
||||
if (size <= CommonLegacySize) return 0;
|
||||
// A historical RS232-only image has no peripheral policy. This check is
|
||||
// independent of the current profile: UART -> no-UART upgrades must work.
|
||||
if (size == CommonLegacySize + LegacyRs232TailSize) {
|
||||
uint8_t header[4] = {};
|
||||
if (!file.seek(CommonLegacySize) || file.read(header, sizeof(header)) != sizeof(header))
|
||||
return 1;
|
||||
if (header[0] == 'R' && header[1] == 'S' && header[2] == 'P' && header[3] == 1)
|
||||
return 0;
|
||||
}
|
||||
uint8_t footer[FooterSize] = {};
|
||||
uint8_t shared = 1;
|
||||
if (size < FooterSize || !file.seek(size - FooterSize)
|
||||
|| file.read(footer, sizeof(footer)) != sizeof(footer)
|
||||
|| !decodeFooter(footer, sizeof(footer), shared)) return 1;
|
||||
return shared;
|
||||
}
|
||||
|
||||
template <typename Save>
|
||||
bool handleCommand(uint8_t& shared, const char* command, char* reply,
|
||||
size_t capacity, bool supported, Save save) {
|
||||
if (!command || !reply || capacity == 0) return false;
|
||||
const bool query = strcmp(command, "get peripherals.mode") == 0;
|
||||
static const char prefix[] = "set peripherals.mode";
|
||||
const bool setter = strncmp(command, prefix, sizeof(prefix) - 1) == 0
|
||||
&& (command[sizeof(prefix) - 1] == 0 || command[sizeof(prefix) - 1] == ' '
|
||||
|| command[sizeof(prefix) - 1] == '\t');
|
||||
if (!query && !setter) return false;
|
||||
if (!supported) {
|
||||
snprintf(reply, capacity, "Error: peripheral mode unsupported");
|
||||
return true;
|
||||
}
|
||||
if (query) {
|
||||
snprintf(reply, capacity, "> %s (saved; applied at boot)", shared ? "shared" : "gps-only");
|
||||
return true;
|
||||
}
|
||||
const char* value = command + sizeof(prefix) - 1;
|
||||
while (*value == ' ' || *value == '\t') ++value;
|
||||
if (strcmp(value, "gps-only") != 0 && strcmp(value, "shared") != 0) {
|
||||
snprintf(reply, capacity, "Error: use set peripherals.mode gps-only|shared");
|
||||
return true;
|
||||
}
|
||||
const uint8_t previous = shared;
|
||||
shared = strcmp(value, "shared") == 0 ? 1 : 0;
|
||||
if (!save()) {
|
||||
shared = previous;
|
||||
snprintf(reply, capacity, "Error: peripheral mode could not be saved");
|
||||
} else {
|
||||
snprintf(reply, capacity, "OK - %s saved; reboot to apply", value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace peripherals
|
||||
} // namespace mesh
|
||||
@@ -162,6 +162,13 @@ public:
|
||||
return false;
|
||||
}
|
||||
virtual LocationProvider* getLocationProvider() { return NULL; }
|
||||
// WisBlock slot power is a boot-time choice, separate from logical GPS on/off.
|
||||
virtual bool supportsSharedPeripheralMode() const { return false; }
|
||||
virtual void configureSharedPeripheralMode(bool shared) { (void)shared; }
|
||||
virtual bool isGpsPowered() const {
|
||||
LocationProvider* location = const_cast<SensorManager*>(this)->getLocationProvider();
|
||||
return location != NULL && location->isEnabled();
|
||||
}
|
||||
virtual void setPowerSavingEnabled(bool enabled) { powersaving_enabled = enabled; }
|
||||
// Runtime bridges may temporarily own the UART used by a GPS receiver. A
|
||||
// sensor manager that actually detected a GPS on that UART overrides these
|
||||
|
||||
@@ -318,6 +318,16 @@ static bool serialGPSFlag = false;
|
||||
#include <SparkFun_u-blox_GNSS_Arduino_Library.h>
|
||||
static SFE_UBLOX_GNSS ublox_GNSS;
|
||||
|
||||
static bool configureRakUbloxReceiver() {
|
||||
// Retain the receiver's supported constellation defaults/BBR selection.
|
||||
// ZOE-M8Q cannot run GPS/Galileo/GLONASS/BeiDou all at once, and blindly
|
||||
// enabling every constellation ignores CFG-GNSS constraints/errata. Only
|
||||
// require the transport and cadence we actually consume after a cold wake.
|
||||
// Do not repeatedly write receiver nonvolatile storage on wake.
|
||||
return ublox_GNSS.setI2COutput(COM_TYPE_UBX)
|
||||
&& ublox_GNSS.setMeasurementRate(1000);
|
||||
}
|
||||
|
||||
#ifndef RAK_UART_GPS_PROBE_TIMEOUT_MS
|
||||
#define RAK_UART_GPS_PROBE_TIMEOUT_MS 1200UL
|
||||
#endif
|
||||
@@ -334,8 +344,8 @@ static void setRakGpsControl(uint8_t pin, bool shared_power_rail,
|
||||
bool enabled) {
|
||||
pinMode(pin, OUTPUT);
|
||||
if (shared_power_rail) {
|
||||
// WB_IO2 controls every 3V3_S peripheral, not just the GPS. It must never
|
||||
// be dropped as a GPS power-saving operation.
|
||||
// Discovery cannot drop this shared rail. The manager's frozen ownership
|
||||
// policy, not a provider's enable pin, handles later GPS-only shutdown.
|
||||
digitalWrite(pin, HIGH);
|
||||
return;
|
||||
}
|
||||
@@ -343,6 +353,14 @@ static void setRakGpsControl(uint8_t pin, bool shared_power_rail,
|
||||
digitalWrite(pin, enabled ? active : static_cast<uint8_t>(!active));
|
||||
}
|
||||
|
||||
static void stopRakGpsUart() {
|
||||
Serial1.end();
|
||||
// UARTE teardown disconnects PSEL but does not restore GPIO configuration.
|
||||
// Remove output drive and pulls so serial pads cannot back-power an off GPS.
|
||||
pinMode(PIN_GPS_TX, INPUT);
|
||||
pinMode(PIN_GPS_RX, INPUT);
|
||||
}
|
||||
|
||||
static bool serialHasValidGpsSentence(Stream& serial, uint32_t timeout_ms) {
|
||||
mesh::NmeaSentenceProbe probe;
|
||||
const uint32_t started = millis();
|
||||
@@ -956,6 +974,10 @@ static const size_t SENSOR_TABLE_SIZE = (sizeof(SENSOR_TABLE) / sizeof(SENSOR_TA
|
||||
|
||||
bool EnvironmentSensorManager::begin() {
|
||||
_active_sensor_count = 0;
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
_wisblock_power.setDiscoveryActive(true);
|
||||
applyWisBlockRailPower();
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
@@ -973,7 +995,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
#ifdef NRF52_PLATFORM
|
||||
if (!isValidNrfI2cPinPair(ENV_PIN_SDA, ENV_PIN_SCL)) {
|
||||
MESH_DEBUG_PRINTLN("Second I2C skipped: invalid SDA/SCL pin pair");
|
||||
return true;
|
||||
return finishSensorDiscovery();
|
||||
}
|
||||
Wire1.setPins(static_cast<uint8_t>(ENV_PIN_SDA),
|
||||
static_cast<uint8_t>(ENV_PIN_SCL));
|
||||
@@ -987,13 +1009,13 @@ bool EnvironmentSensorManager::begin() {
|
||||
|
||||
// Avoid touching a shared I2C bus when no environmental drivers were built.
|
||||
if (SENSOR_TABLE_SIZE == 0) {
|
||||
return true;
|
||||
return finishSensorDiscovery();
|
||||
}
|
||||
|
||||
if (!ensureI2cBusReleased(TELEM_WIRE)) {
|
||||
MESH_DEBUG_PRINTLN(
|
||||
"I2C discovery skipped: bus pins unavailable or SDA/SCL remained low");
|
||||
return true;
|
||||
return finishSensorDiscovery();
|
||||
}
|
||||
|
||||
// Probe only unique addresses compiled into this image before invoking any
|
||||
@@ -1054,7 +1076,7 @@ bool EnvironmentSensorManager::begin() {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return finishSensorDiscovery();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -1070,6 +1092,10 @@ bool EnvironmentSensorManager::querySensors(uint8_t requester_permissions, Cayen
|
||||
queryGpsTelemetry(requester_permissions, telemetry);
|
||||
#endif
|
||||
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
if (!_wisblock_power.railEnabled()) return true;
|
||||
#endif
|
||||
|
||||
if (requester_permissions & TELEM_PERM_ENVIRONMENT) {
|
||||
for (int i = 0; i < _active_sensor_count; i++) {
|
||||
_active_sensors[i].query(next_available_channel, _active_sensors[i].sub_channel, telemetry);
|
||||
@@ -1095,6 +1121,9 @@ uint8_t EnvironmentSensorManager::getVoltageSensorChannels(
|
||||
|
||||
uint8_t EnvironmentSensorManager::queryVoltageSensors(
|
||||
VoltageSensorReading readings[], uint8_t capacity) {
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
if (!_wisblock_power.railEnabled()) return 0;
|
||||
#endif
|
||||
uint8_t count = 0;
|
||||
for (int i = 0; i < _active_sensor_count && count < capacity; i++) {
|
||||
const ActiveSensor& sensor = _active_sensors[i];
|
||||
@@ -1149,17 +1178,6 @@ bool EnvironmentSensorManager::setSettingValue(const char* name, const char* val
|
||||
gps_discovery_preference_known = true;
|
||||
#endif
|
||||
bool enabled = strcmp(value, "0") != 0;
|
||||
#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)
|
||||
// A UART L76K cannot remove power from the shared 3V3_S rail. "Off" stops
|
||||
// parsing and releases the MCU UART, but the still-powered receiver keeps
|
||||
// driving its TX pin. It therefore does not make that pin safe for an
|
||||
// external UART bridge.
|
||||
if (serialGPSFlag) {
|
||||
_location->setGPSPowerSaving(false);
|
||||
setGpsTelemetryUserEnabled(enabled);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
bool was_active = gps_active;
|
||||
_location->setGPSPowerSaving(enabled && powersaving_enabled);
|
||||
setGpsTelemetryUserEnabled(enabled);
|
||||
@@ -1183,17 +1201,6 @@ void EnvironmentSensorManager::setPowerSavingEnabled(bool enabled) {
|
||||
return;
|
||||
}
|
||||
bool gps_user_enabled = isGpsTelemetryUserEnabled();
|
||||
#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)
|
||||
if (serialGPSFlag) {
|
||||
_location->setGPSPowerSaving(false);
|
||||
if (gps_user_enabled) {
|
||||
if (!gps_active) start_gps();
|
||||
} else if (gps_active) {
|
||||
stop_gps();
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
_location->setGPSPowerSaving(enabled && gps_user_enabled);
|
||||
if (!gps_user_enabled) return;
|
||||
|
||||
@@ -1327,6 +1334,67 @@ void EnvironmentSensorManager::initBasicGPS() {
|
||||
#endif
|
||||
}
|
||||
|
||||
bool EnvironmentSensorManager::supportsSharedPeripheralMode() const {
|
||||
#if defined(RAK_4631) && !defined(RAK_WISMESH_TAG)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
void EnvironmentSensorManager::applyWisBlockRailPower() {
|
||||
pinMode(WB_IO2, OUTPUT);
|
||||
digitalWrite(WB_IO2, _wisblock_power.railEnabled() ? HIGH : LOW);
|
||||
}
|
||||
|
||||
uint8_t EnvironmentSensorManager::detectWisBlockStorageHolds() {
|
||||
uint8_t holds = 0;
|
||||
#if defined(OTA_RAK_AUTO_STORE)
|
||||
uint32_t rak_id = 0, w25_id = 0;
|
||||
const uint8_t selected = mesh::ota::OtaStoreQspiNrf52::autoDetect();
|
||||
mesh::ota::OtaStoreQspiNrf52::autoProbeIds(rak_id, w25_id);
|
||||
// Unsupported but responding NOR parts must not lose their supply either.
|
||||
// All-zero/all-one reads are the probe's absent-chip values.
|
||||
if (selected != 0 || (rak_id != 0 && rak_id != 0xFFFFFFUL)
|
||||
|| (w25_id != 0 && w25_id != 0xFFFFFFUL)) {
|
||||
holds |= mesh::gps::ExternalStorage;
|
||||
}
|
||||
#elif defined(OTA_QSPI_SHARED_WISBLOCK_SPI) || defined(OTA_SD_STORE)
|
||||
holds |= mesh::gps::ExternalStorage;
|
||||
#endif
|
||||
return holds;
|
||||
}
|
||||
#endif
|
||||
|
||||
void EnvironmentSensorManager::configureSharedPeripheralMode(bool shared) {
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
// Called once after boot discovery and validated preference loading. Live
|
||||
// setters only save the next-boot choice; they never revoke active owners.
|
||||
if (!_wisblock_power.configure(shared ? mesh::gps::WisBlockPeripheralMode::Shared
|
||||
: mesh::gps::WisBlockPeripheralMode::GpsOnly,
|
||||
detectWisBlockStorageHolds())) return;
|
||||
_wisblock_power.setGpsActive(gps_active);
|
||||
// Companion loads preferences before sensor discovery, whereas repeaters
|
||||
// load them afterwards. Only finishSensorDiscovery releases that interlock.
|
||||
applyWisBlockRailPower();
|
||||
#else
|
||||
(void)shared;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool EnvironmentSensorManager::isGpsPowered() const {
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
||||
return digitalRead(PIN_GPS_EN) == rakGpsControlActiveLevel();
|
||||
#else
|
||||
return digitalRead(WB_IO2) == HIGH;
|
||||
#endif
|
||||
#else
|
||||
return SensorManager::isGpsPowered();
|
||||
#endif
|
||||
}
|
||||
|
||||
// gps code for rak might be moved to MicroNMEALoactionProvider
|
||||
// or make a new location provider ...
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
@@ -1382,27 +1450,21 @@ void EnvironmentSensorManager::rakGPSInit() {
|
||||
#endif
|
||||
|
||||
if (gpsIsAwake(gps_control_pin, shared_power_rail)) {
|
||||
_location->setPinEn(gps_control_pin);
|
||||
// WB_IO2 is not an independently owned GPS pin. A provider must never
|
||||
// lower it during reset/begin/stop behind the shared-power policy's back.
|
||||
_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);
|
||||
if (i2cGPSFlag) stopRakGpsUart();
|
||||
} else {
|
||||
MESH_DEBUG_PRINTLN("No GPS found");
|
||||
gps_active = false;
|
||||
gps_detected = false;
|
||||
Serial1.end();
|
||||
stopRakGpsUart();
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef FORCE_GPS_ALIVE
|
||||
if (i2cGPSFlag) {
|
||||
// The u-blox receiver can sleep while the shared rail remains available.
|
||||
stop_gps();
|
||||
} else if (serialGPSFlag) {
|
||||
// The UART L76K cannot be isolated from the RAK3401 radio power rail.
|
||||
setGpsTelemetryUserEnabled(true);
|
||||
}
|
||||
#else
|
||||
// Now that GPS is found and set up, set it to sleep for the initial state.
|
||||
stop_gps();
|
||||
#endif
|
||||
// Keep discovery power available for the remaining slot probes. Loading the
|
||||
// saved mode and GPS preference later performs the initial stop, without
|
||||
// ever manufacturing a GPS-on preference from a successful UART probe.
|
||||
}
|
||||
|
||||
bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin,
|
||||
@@ -1467,17 +1529,9 @@ bool EnvironmentSensorManager::gpsIsAwake(uint8_t ioPin,
|
||||
// After two successful non-INA manufacturer reads, use the u-blox-specific
|
||||
// probe. No software can make two physical devices at one address safe.
|
||||
if (probe_i2c_gps
|
||||
&& ublox_GNSS.begin(Wire, gps_i2c_address) == true) {
|
||||
&& ublox_GNSS.begin(Wire, gps_i2c_address) == true
|
||||
&& configureRakUbloxReceiver()) {
|
||||
MESH_DEBUG_PRINTLN("RAK12500 GPS init correctly with pin %i", ioPin);
|
||||
ublox_GNSS.setI2COutput(COM_TYPE_UBX);
|
||||
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GPS);
|
||||
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GALILEO);
|
||||
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_GLONASS);
|
||||
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_SBAS);
|
||||
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_BEIDOU);
|
||||
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_IMES);
|
||||
ublox_GNSS.enableGNSS(true, SFE_UBLOX_GNSS_ID_QZSS);
|
||||
ublox_GNSS.setMeasurementRate(1000);
|
||||
ublox_GNSS.saveConfigSelective(VAL_CFG_SUBSEC_IOPORT);
|
||||
gpsResetPin = ioPin;
|
||||
i2cGPSFlag = true;
|
||||
@@ -1506,6 +1560,40 @@ void EnvironmentSensorManager::armGpsPowerSavingCycle() {
|
||||
|
||||
void EnvironmentSensorManager::start_gps() {
|
||||
if (gps_active || gps_serial_transport_blocked) return;
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
const bool was_powered = isGpsPowered();
|
||||
_wisblock_power.setGpsActive(true);
|
||||
applyWisBlockRailPower();
|
||||
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
||||
setRakGpsControl(PIN_GPS_EN, false, true);
|
||||
#endif
|
||||
// Supply first, UART second. A cold receiver must settle before its bus or
|
||||
// serial transport is used, including a wake after full GPS-only shutdown.
|
||||
if (!was_powered) delay(500);
|
||||
if (i2cGPSFlag) {
|
||||
if (!was_powered) {
|
||||
if (!ensureI2cBusReleased(&Wire)
|
||||
|| !ublox_GNSS.begin(Wire, TELEM_RAK12500_ADDRESS)
|
||||
|| !configureRakUbloxReceiver()) {
|
||||
// Do not spin on a failed cold wake on every mesh loop. Keep saved
|
||||
// intent, fail this receiver closed, and allow rediscovery on reboot.
|
||||
gps_detected = false;
|
||||
_wisblock_power.setGpsActive(false);
|
||||
applyWisBlockRailPower();
|
||||
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
||||
setRakGpsControl(PIN_GPS_EN, false, false);
|
||||
#endif
|
||||
_location->stopTimeSync();
|
||||
MESH_DEBUG_PRINTLN("RAK12500 cold wake failed; GPS rediscovery needs reboot");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Shared mode retains the historical u-blox internal PSM. This is not
|
||||
// a promise of full rail shutdown, nor does it silence a UART receiver.
|
||||
ublox_GNSS.powerSaveMode(false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (gps_serial_transport) {
|
||||
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
|
||||
#ifdef GPS_BAUD_RATE
|
||||
@@ -1516,13 +1604,7 @@ void EnvironmentSensorManager::start_gps() {
|
||||
}
|
||||
gps_active = true;
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
#ifdef FORCE_GPS_ALIVE
|
||||
// The UART L76K has no power-save command. Keep the shared rail enabled;
|
||||
// an I2C u-blox receiver can also be explicitly returned to full power.
|
||||
if (i2cGPSFlag) ublox_GNSS.powerSaveMode(false);
|
||||
#else
|
||||
setRakGpsControl(gpsResetPin, false, true);
|
||||
#endif
|
||||
_location->clearPositionFix();
|
||||
#else
|
||||
_location->begin();
|
||||
_location->reset();
|
||||
@@ -1546,18 +1628,17 @@ void EnvironmentSensorManager::stop_gps() {
|
||||
}
|
||||
|
||||
#ifdef RAK_WISBLOCK_GPS
|
||||
#ifdef FORCE_GPS_ALIVE
|
||||
// Keep the shared rail alive for the RAK3401 radio FEM. The I2C u-blox can
|
||||
// still enter its internal power-save mode. Ending Serial1 stops the MCU
|
||||
// parser, but a UART L76K remains powered and continues driving its TX pin.
|
||||
if (i2cGPSFlag) {
|
||||
ublox_GNSS.powerSaveMode(true);
|
||||
} else if (serialGPSFlag) {
|
||||
Serial1.end();
|
||||
}
|
||||
// End the MCU UART before removing receiver power, never the reverse.
|
||||
if (serialGPSFlag) stopRakGpsUart();
|
||||
_wisblock_power.setGpsActive(false);
|
||||
#if defined(PIN_GPS_EN) && PIN_GPS_EN >= 0
|
||||
setRakGpsControl(PIN_GPS_EN, false, false);
|
||||
#else
|
||||
setRakGpsControl(gpsResetPin, false, false);
|
||||
if (i2cGPSFlag && _wisblock_power.railEnabled()) {
|
||||
ublox_GNSS.powerSaveMode(true);
|
||||
}
|
||||
#endif
|
||||
applyWisBlockRailPower();
|
||||
#else
|
||||
_location->stop();
|
||||
#endif
|
||||
@@ -1603,11 +1684,10 @@ bool EnvironmentSensorManager::gpsSerialTransportMayConflict(
|
||||
bool EnvironmentSensorManager::gpsSerialTransportCanYield(uint8_t uart) const {
|
||||
#if ENV_INCLUDE_GPS
|
||||
if (!gpsUsesSerialUart(uart)) return false;
|
||||
#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)
|
||||
// RAK12501 exposes no independently controlled standby/power pin. WB_IO2 is
|
||||
// the shared 3V3_S rail and must remain high, so its L76K keeps transmitting
|
||||
// NMEA after Serial1.end(). Two TX drivers on UART1 would electrically
|
||||
// contend; require UART2 or physical removal/isolation of this GPS instead.
|
||||
#if defined(RAK_WISBLOCK_GPS) && !(defined(PIN_GPS_EN) && PIN_GPS_EN >= 0)
|
||||
// WB_IO2 is not independent isolation even in GPS-only mode: a bridge or
|
||||
// future GPS request can repower the same rail. Reserve UART1 to avoid two
|
||||
// TX drivers; use UART2 or a physically independent GPS enable instead.
|
||||
return false;
|
||||
#elif defined(RAK_WISBLOCK_GPS)
|
||||
// This profile is allowed to remove power with its validated GPS control.
|
||||
@@ -1637,7 +1717,9 @@ bool EnvironmentSensorManager::setGpsSerialTransportBlocked(uint8_t uart,
|
||||
if (gps_discovery_pending) _location->stop();
|
||||
#endif
|
||||
if (gps_active) stop_gps();
|
||||
#ifndef RAK_WISBLOCK_GPS
|
||||
Serial1.end();
|
||||
#endif
|
||||
} else {
|
||||
gps_serial_transport_blocked = false;
|
||||
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) && !defined(RAK_WISBLOCK_GPS)
|
||||
@@ -1722,6 +1804,9 @@ void EnvironmentSensorManager::loop() {
|
||||
}
|
||||
#endif
|
||||
#if ENV_INCLUDE_BME680_BSEC
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
if (!_wisblock_power.railEnabled()) return;
|
||||
#endif
|
||||
if (bsec_active && bsec_iaq.run()) {
|
||||
uint8_t prev_accuracy = bsec_accuracy;
|
||||
bsec_temperature = bsec_iaq.temperature;
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <Mesh.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
#include "WisBlockSharedPowerPolicy.h"
|
||||
#endif
|
||||
|
||||
class EnvironmentSensorManager : public SensorManager {
|
||||
protected:
|
||||
@@ -24,6 +27,28 @@ protected:
|
||||
bool gps_active = false;
|
||||
bool gps_serial_transport = false;
|
||||
bool gps_serial_transport_blocked = false;
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
mesh::gps::WisBlockSharedPowerPolicy _wisblock_power;
|
||||
bool _wisblock_discovery_completed = false;
|
||||
void applyWisBlockRailPower();
|
||||
uint8_t detectWisBlockStorageHolds();
|
||||
#endif
|
||||
bool finishSensorDiscovery() {
|
||||
#if defined(RAK_BOARD) && !defined(RAK_WISMESH_TAG)
|
||||
#if ENV_INCLUDE_GPS
|
||||
// A repeated probe must not override the loaded manual preference or an
|
||||
// established telemetry demand. Initial boot waits for preference load.
|
||||
if (_wisblock_discovery_completed && _wisblock_power.isConfigured() && gps_active
|
||||
&& !isGpsTelemetryUserEnabled()
|
||||
&& !gpsTelemetryReceiverRequired(millis())) stop_gps();
|
||||
#endif
|
||||
_wisblock_power.setGpsActive(gps_active);
|
||||
_wisblock_discovery_completed = true;
|
||||
_wisblock_power.setDiscoveryActive(false);
|
||||
applyWisBlockRailPower();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS
|
||||
#if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) \
|
||||
@@ -75,6 +100,9 @@ public:
|
||||
const char* getSettingValue(int i) const override;
|
||||
bool setSettingValue(const char* name, const char* value) override;
|
||||
void setPowerSavingEnabled(bool enabled) override;
|
||||
bool supportsSharedPeripheralMode() const override;
|
||||
void configureSharedPeripheralMode(bool shared) override;
|
||||
bool isGpsPowered() const override;
|
||||
bool gpsUsesSerialUart(uint8_t uart) const override;
|
||||
bool gpsSerialTransportMayConflict(uint8_t uart) const override;
|
||||
bool gpsSerialTransportCanYield(uint8_t uart) const override;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace gps {
|
||||
|
||||
enum class WisBlockPeripheralMode : uint8_t {
|
||||
GpsOnly = 0,
|
||||
Shared = 1,
|
||||
SharedPeripherals = Shared,
|
||||
};
|
||||
|
||||
enum WisBlockRailHold : uint8_t {
|
||||
RadioPower = 1U << 0,
|
||||
EthernetPower = 1U << 1,
|
||||
ExternalStorage = 1U << 2,
|
||||
};
|
||||
|
||||
// These are hardware dependencies, not whether the corresponding service is
|
||||
// enabled at runtime. In particular, removing Ethernet power can brown out a
|
||||
// PoE-powered core even when its network service is disabled.
|
||||
constexpr uint8_t wisBlockCompileRailHolds() {
|
||||
return 0
|
||||
#if defined(RAK_3401)
|
||||
| RadioPower
|
||||
#endif
|
||||
#if defined(ETHERNET_ENABLED) || defined(ETHERNET_USE_RAK13800) \
|
||||
|| defined(RAK4631_COMBINED_ETHERNET)
|
||||
| EthernetPower
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
// No GPIO, bus probes, allocation, or persistence belongs in this policy.
|
||||
// Integrators must finish their read-only external-storage probe while the
|
||||
// discovery hold is set, then freeze its result together with the saved mode.
|
||||
// A powered/shared GNSS UART still cannot be safely handed to another driver.
|
||||
class WisBlockSharedPowerPolicy {
|
||||
public:
|
||||
explicit WisBlockSharedPowerPolicy(uint8_t initial_holds = 0)
|
||||
: _holds(static_cast<uint8_t>(initial_holds | wisBlockCompileRailHolds())),
|
||||
_state(DiscoveryActive) {}
|
||||
|
||||
// Configuration is boot-only. Reject repeated or invalid configuration
|
||||
// without changing the established owners; changing mode requires reboot.
|
||||
bool configure(WisBlockPeripheralMode mode, uint8_t detected_holds = 0) {
|
||||
if (isConfigured()
|
||||
|| (mode != WisBlockPeripheralMode::GpsOnly
|
||||
&& mode != WisBlockPeripheralMode::Shared)
|
||||
|| (detected_holds & static_cast<uint8_t>(~KnownHolds)) != 0) {
|
||||
return false;
|
||||
}
|
||||
_holds = static_cast<uint8_t>(_holds | detected_holds);
|
||||
_state = static_cast<uint8_t>(_state | Configured);
|
||||
if (mode == WisBlockPeripheralMode::Shared) _state |= SharedMode;
|
||||
return true;
|
||||
}
|
||||
|
||||
void setDiscoveryActive(bool active) { setFlag(DiscoveryActive, active); }
|
||||
void setGpsActive(bool active) { setFlag(GpsActive, active); }
|
||||
|
||||
bool isConfigured() const { return (_state & Configured) != 0; }
|
||||
bool discoveryActive() const { return (_state & DiscoveryActive) != 0; }
|
||||
bool gpsActive() const { return (_state & GpsActive) != 0; }
|
||||
WisBlockPeripheralMode mode() const {
|
||||
return (_state & SharedMode) != 0 ? WisBlockPeripheralMode::Shared
|
||||
: WisBlockPeripheralMode::GpsOnly;
|
||||
}
|
||||
uint8_t holds() const { return _holds; }
|
||||
|
||||
// Permission to remove GPS power, independent of its current requested
|
||||
// state. This is not proof that a GNSS TX pin has already become isolated.
|
||||
bool gpsPowerIndependentlyControllable() const {
|
||||
return isConfigured() && !discoveryActive()
|
||||
&& mode() == WisBlockPeripheralMode::GpsOnly && _holds == 0;
|
||||
}
|
||||
bool canPowerDown() const {
|
||||
return gpsPowerIndependentlyControllable() && !gpsActive();
|
||||
}
|
||||
bool railEnabled() const { return !canPowerDown(); }
|
||||
|
||||
private:
|
||||
enum State : uint8_t {
|
||||
Configured = 1U << 0,
|
||||
SharedMode = 1U << 1,
|
||||
GpsActive = 1U << 2,
|
||||
DiscoveryActive = 1U << 3,
|
||||
};
|
||||
static constexpr uint8_t KnownHolds =
|
||||
RadioPower | EthernetPower | ExternalStorage;
|
||||
|
||||
// Retain unknown constructor hold bits fail-closed. They must never become
|
||||
// permission to cut power merely because a future owner is not recognized.
|
||||
uint8_t _holds;
|
||||
uint8_t _state;
|
||||
|
||||
void setFlag(uint8_t flag, bool enabled) {
|
||||
if (enabled) _state |= flag;
|
||||
else _state &= static_cast<uint8_t>(~flag);
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(sizeof(WisBlockSharedPowerPolicy) == 2,
|
||||
"WisBlock rail ownership must stay allocation-free and compact");
|
||||
|
||||
} // namespace gps
|
||||
} // namespace mesh
|
||||
Vendored
+444
@@ -0,0 +1,444 @@
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#include <helpers/sensors/I2CAddressClaimPolicy.h>
|
||||
#include <helpers/sensors/NmeaSentenceProbe.h>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
|
||||
#define WB_IO2 21
|
||||
#define PIN_GPS_TX 15
|
||||
#define PIN_GPS_RX 16
|
||||
#define TELEM_RAK12500_ADDRESS 0x42
|
||||
#define RAK_UART_GPS_PROBE_TIMEOUT_MS 1200UL
|
||||
#define TELEM_WIRE (&Wire)
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define COM_TYPE_UBX 1
|
||||
#define SFE_UBLOX_GNSS_ID_GPS 0
|
||||
#define SFE_UBLOX_GNSS_ID_GALILEO 1
|
||||
#define SFE_UBLOX_GNSS_ID_GLONASS 2
|
||||
#define SFE_UBLOX_GNSS_ID_SBAS 3
|
||||
#define SFE_UBLOX_GNSS_ID_BEIDOU 4
|
||||
#define SFE_UBLOX_GNSS_ID_IMES 5
|
||||
#define SFE_UBLOX_GNSS_ID_QZSS 6
|
||||
#define VAL_CFG_SUBSEC_IOPORT 1
|
||||
|
||||
void LocationProvider::sendSentence(const char*) {}
|
||||
static uint32_t gpsResetPin = static_cast<uint32_t>(-1);
|
||||
static bool i2cGPSFlag = false, serialGPSFlag = false;
|
||||
static const char meshcore_capability_rak_wisblock_gps[] = "linked-provider";
|
||||
|
||||
struct SerialMock : Stream {
|
||||
unsigned begins=0, ends=0, polls=0;
|
||||
bool installed=false;
|
||||
std::deque<uint8_t> incoming;
|
||||
void setPins(int tx,int rx) { event("uartPins",tx,rx); }
|
||||
void begin(unsigned baud) { installed=true; ++begins; event("uartBegin",baud); }
|
||||
void end() { assert(installed&&"pinned UARTE end must not be called twice"); installed=false; ++ends; event("uartEnd"); }
|
||||
int available() override { ++polls; return installed ? int(incoming.size()) : 0; }
|
||||
int read() override { if(incoming.empty()) return -1; int c=incoming.front(); incoming.pop_front(); return c; }
|
||||
void nmea() {
|
||||
const char* body="GPRMC,valid";
|
||||
uint8_t checksum=0;
|
||||
for(const char* c=body;*c;++c) checksum^=uint8_t(*c);
|
||||
char sentence[64]; std::snprintf(sentence,sizeof(sentence),"$%s*%02X\r\n",body,checksum);
|
||||
for(const char* c=sentence;*c;++c) incoming.push_back(uint8_t(*c));
|
||||
}
|
||||
} Serial1;
|
||||
|
||||
namespace mesh { namespace ota {
|
||||
struct OtaStoreQspiNrf52 {
|
||||
inline static uint8_t selected=0;
|
||||
inline static uint32_t rak_id=0,w25_id=0;
|
||||
inline static bool header_present=false;
|
||||
inline static bool cached=false;
|
||||
inline static unsigned probes=0;
|
||||
static bool headerW25Detected() { autoDetect(); event("headerProbe"); return header_present; }
|
||||
static uint8_t autoDetect() { if(!cached) { cached=true; ++probes; event("norProbe"); assert(digitalRead(WB_IO2)==HIGH); } return selected; }
|
||||
static void autoProbeIds(uint32_t& rak,uint32_t& w25) { autoDetect(); rak=rak_id; w25=w25_id; }
|
||||
};
|
||||
}}
|
||||
|
||||
static bool bus_available=true;
|
||||
static bool ensureI2cBusReleased(TwoWire*) { event("i2cBus"); return bus_available; }
|
||||
struct GnssMock {
|
||||
bool present=false, begin_ok=true, output_ok=true, rate_ok=true, psm_ok=true, valid=false;
|
||||
unsigned begins=0, polls=0, flushes=0, output_calls=0, rate_calls=0, nv_writes=0;
|
||||
std::vector<bool> psm;
|
||||
bool begin(TwoWire&,uint8_t address) { ++begins; event("gnssBegin",address); return present&&begin_ok; }
|
||||
bool powerSaveMode(bool enabled) { psm.push_back(enabled); event("gnssPsm",enabled); return psm_ok; }
|
||||
bool setI2COutput(unsigned output) { ++output_calls; event("gnssOutput",output); return output_ok; }
|
||||
void enableGNSS(bool,unsigned) {}
|
||||
bool setMeasurementRate(unsigned rate) { ++rate_calls; event("gnssRate",rate); return rate_ok; }
|
||||
void saveConfigSelective(unsigned) { ++nv_writes; }
|
||||
void flushPVT() { ++flushes; }
|
||||
bool getPVT(unsigned) { ++polls; return valid; }
|
||||
bool getGnssFixOk(unsigned) { ++polls; return valid; }
|
||||
long getLatitude(unsigned) { return 476100000; }
|
||||
long getLongitude(unsigned) { return -1223300000; }
|
||||
long getAltitude(unsigned) { return 125000; }
|
||||
int getSIV(unsigned) { return valid?8:0; }
|
||||
bool getDateValid(unsigned) { return valid; }
|
||||
bool getTimeValid(unsigned) { return valid; }
|
||||
long getUnixEpoch(unsigned) { return 1790942400; }
|
||||
} ublox_GNSS;
|
||||
|
||||
@HELPERS@
|
||||
@PROVIDER@
|
||||
static RAK12500LocationProvider RAK12500_provider;
|
||||
|
||||
struct SensorDef {
|
||||
uint32_t address;
|
||||
const char* name;
|
||||
uint8_t (*init)(TwoWire*,uint8_t);
|
||||
void (*query)(uint8_t,uint8_t,CayenneLPP&);
|
||||
bool (*query_voltage)(uint8_t,float&);
|
||||
};
|
||||
static const SensorDef SENSOR_TABLE[1] = {};
|
||||
static const size_t SENSOR_TABLE_SIZE=0;
|
||||
bool EnvironmentSensorManager::i2c_probe(TwoWire&,uint8_t) { return false; }
|
||||
@METHODS@
|
||||
|
||||
struct TestLocation : LocationProvider {
|
||||
unsigned loops=0,resets=0,clears=0;
|
||||
int pin=-1;
|
||||
mesh::RTCClock clock;
|
||||
bool valid=false;
|
||||
void reset() override { ++resets; valid=false; }
|
||||
void begin() override {}
|
||||
void stop() override {}
|
||||
void loop() override { ++loops; }
|
||||
bool isEnabled() override { return pin<0 || digitalRead(pin)==HIGH; }
|
||||
bool clearPositionFix() override { valid=false; ++clears; return true; }
|
||||
void setPinEn(int value) override { pin=value; }
|
||||
int getPinEn() override { return pin; }
|
||||
mesh::RTCClock* getRTCClock() override { return &clock; }
|
||||
long getLatitude() override { return 47610000; }
|
||||
long getLongitude() override { return -122330000; }
|
||||
long getAltitude() override { return 125000; }
|
||||
long satellitesCount() override { return valid?8:0; }
|
||||
bool isValid() override { return valid; }
|
||||
long getTimestamp() override { return 1790942400; }
|
||||
void acceptedClock() { markTimeSyncApplied(); stopTimeSync(); }
|
||||
};
|
||||
|
||||
struct TestEnvironment : EnvironmentSensorManager {
|
||||
explicit TestEnvironment(TestLocation& location):EnvironmentSensorManager(location) {}
|
||||
bool active() const { return gps_active; }
|
||||
bool userEnabled() const { return isGpsTelemetryUserEnabled(); }
|
||||
bool transportAvailable() const { return isGpsTelemetryTransportAvailable(); }
|
||||
using EnvironmentSensorManager::start_gps;
|
||||
using EnvironmentSensorManager::stop_gps;
|
||||
using EnvironmentSensorManager::rakGPSInit;
|
||||
using EnvironmentSensorManager::_wisblock_power;
|
||||
};
|
||||
|
||||
static constexpr bool compile_held = mesh::gps::wisBlockCompileRailHolds()!=0;
|
||||
static void fresh() {
|
||||
now_ms=100; delay_count=0; hardware_events.clear();
|
||||
std::memset(pin_values,0,sizeof(pin_values)); pin_values[WB_IO2]=HIGH;
|
||||
Serial1=SerialMock{}; ublox_GNSS=GnssMock{}; RAK12500_provider=RAK12500LocationProvider{};
|
||||
mesh::ota::OtaStoreQspiNrf52::selected=0;
|
||||
mesh::ota::OtaStoreQspiNrf52::rak_id=mesh::ota::OtaStoreQspiNrf52::w25_id=0;
|
||||
mesh::ota::OtaStoreQspiNrf52::header_present=false;
|
||||
mesh::ota::OtaStoreQspiNrf52::cached=false;
|
||||
mesh::ota::OtaStoreQspiNrf52::probes=0;
|
||||
i2cGPSFlag=serialGPSFlag=false; gpsResetPin=uint32_t(-1); bus_available=true;
|
||||
}
|
||||
static size_t firstEvent(const char* name,int a=-999,int b=-999,size_t after=0) {
|
||||
for(size_t i=after;i<hardware_events.size();++i) {
|
||||
const auto& e=hardware_events[i];
|
||||
if(e.name==name&&(a==-999||e.a==a)&&(b==-999||e.b==b)) return i;
|
||||
}
|
||||
return hardware_events.size();
|
||||
}
|
||||
static void bootUart(TestEnvironment& sensors,TestLocation& location,bool shared=false) {
|
||||
Serial1.nmea(); assert(sensors.begin());
|
||||
assert(sensors.isGPSDetected()&&sensors.active()&&!sensors.userEnabled());
|
||||
assert(location.pin<0 && sensors.isGpsPowered());
|
||||
assert(digitalRead(WB_IO2)==HIGH);
|
||||
sensors.configureSharedPeripheralMode(shared);
|
||||
assert(sensors._wisblock_power.isConfigured()&&!sensors._wisblock_power.discoveryActive());
|
||||
assert(sensors.setSettingValue("gps","0"));
|
||||
assert(!sensors.active()&&!sensors.userEnabled()&&!Serial1.installed);
|
||||
}
|
||||
static void requireNoLow() {
|
||||
for(const auto& e:hardware_events) assert(!(e.name=="write"&&e.a==WB_IO2&&e.b==LOW));
|
||||
}
|
||||
static void requireUartReleasedBeforePowerOff(int pin=WB_IO2,int value=LOW) {
|
||||
const size_t end=firstEvent("uartEnd");
|
||||
const size_t tx=firstEvent("pinMode",PIN_GPS_TX,INPUT,end);
|
||||
const size_t rx=firstEvent("pinMode",PIN_GPS_RX,INPUT,end);
|
||||
const size_t off=firstEvent("write",pin,value,end);
|
||||
assert(off<hardware_events.size());
|
||||
assert(end<tx&&tx<off&&end<rx&&rx<off);
|
||||
}
|
||||
static void uart(bool shared) {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location,shared);
|
||||
const bool held=shared||compile_held;
|
||||
assert(sensors.isGpsPowered()==held && digitalRead(WB_IO2)==int(held));
|
||||
if(!held) requireUartReleasedBeforePowerOff();
|
||||
else requireNoLow();
|
||||
assert(sensors.gpsUsesSerialUart(1)&&sensors.gpsSerialTransportMayConflict(1));
|
||||
assert(!sensors.gpsSerialTransportCanYield(1));
|
||||
assert(!sensors.gpsUsesSerialUart(2)&&!sensors.gpsSerialTransportMayConflict(2));
|
||||
const unsigned begins=Serial1.begins,ends=Serial1.ends,loops=location.loops;
|
||||
assert(!sensors.setGpsSerialTransportBlocked(1,true));
|
||||
assert(!sensors.setGpsSerialTransportBlocked(2,true));
|
||||
assert(sensors.transportAvailable()&&Serial1.begins==begins&&Serial1.ends==ends);
|
||||
now_ms+=1000; sensors.loop(); assert(location.loops==loops&&!sensors.active());
|
||||
hardware_events.clear(); const uint32_t started=millis();
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
assert(sensors.active()&&sensors.userEnabled()&&Serial1.installed&&sensors.isGpsPowered());
|
||||
const size_t high=firstEvent("write",WB_IO2,HIGH),opened=firstEvent("uartBegin");
|
||||
assert(high<opened);
|
||||
assert(hardware_events[opened].time>=started+(held?0:500));
|
||||
assert(location.clears==1&&location.pin<0);
|
||||
sensors.setPowerSavingEnabled(false); now_ms+=600001; sensors.loop();
|
||||
assert(sensors.active()); // Manual on plus power-saving off is continuous.
|
||||
hardware_events.clear(); assert(sensors.setSettingValue("gps","0"));
|
||||
assert(!sensors.active()&&!Serial1.installed&&sensors.isGpsPowered()==held);
|
||||
if(!held) requireUartReleasedBeforePowerOff();
|
||||
else requireNoLow();
|
||||
sensors.setPowerSavingEnabled(true); now_ms+=1000; sensors.loop();
|
||||
sensors.setPowerSavingEnabled(false); assert(!sensors.active());
|
||||
const unsigned saved_begins=Serial1.begins;
|
||||
sensors.configureSharedPeripheralMode(!shared); // Boot-only mode cannot revoke/reconfigure owners.
|
||||
assert(sensors.isGpsPowered()==held&&Serial1.begins==saved_begins);
|
||||
}
|
||||
static void hourly() {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
|
||||
sensors.applyGpsTimeSyncInterval(1); sensors.setPowerSavingEnabled(true);
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
assert(sensors.active()&&location.getGPSPowerSaving());
|
||||
assert(location.getNextGPSOff()!=0);
|
||||
now_ms+=100; location.acceptedClock(); sensors.loop();
|
||||
assert(!sensors.active()&&!sensors.isGpsPowered());
|
||||
const uint32_t deadline=uint32_t(location.getNextGPSOn());
|
||||
assert(deadline==millis()+3600000U);
|
||||
now_ms=deadline-1; sensors.loop(); assert(!sensors.active());
|
||||
now_ms=deadline; hardware_events.clear(); sensors.loop();
|
||||
assert(sensors.active()&&Serial1.installed);
|
||||
assert(firstEvent("write",WB_IO2,HIGH)<firstEvent("uartBegin"));
|
||||
// Explicit off cancels both clock/duty deadlines and must not be undone by them.
|
||||
assert(sensors.setSettingValue("gps","0"));
|
||||
assert(!location.getGPSPowerSaving()&&location.getNextGPSOn()==0&&location.getNextGPSOff()==0);
|
||||
const unsigned begins=Serial1.begins; now_ms+=3600100; sensors.loop();
|
||||
assert(!sensors.active()&&!sensors.isGpsPowered()&&Serial1.begins==begins);
|
||||
}
|
||||
static void repeatedDiscovery() {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
|
||||
hardware_events.clear(); Serial1.nmea();
|
||||
assert(sensors.begin());
|
||||
assert(!sensors.active()&&!sensors.userEnabled()&&!sensors.isGpsPowered());
|
||||
assert(!Serial1.installed&&!sensors._wisblock_power.discoveryActive());
|
||||
assert(firstEvent("write",WB_IO2,HIGH)<firstEvent("uartBegin"));
|
||||
requireUartReleasedBeforePowerOff();
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
hardware_events.clear(); Serial1.nmea(); assert(sensors.begin());
|
||||
assert(sensors.active()&&sensors.userEnabled()&&sensors.isGpsPowered()&&Serial1.installed);
|
||||
requireNoLow();
|
||||
}
|
||||
static void preferenceBeforeDiscovery() {
|
||||
for(bool shared:{false,true}) {
|
||||
for(bool enabled:{false,true}) {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location);
|
||||
sensors.configureSharedPeripheralMode(shared);
|
||||
// Companion loads mode before sensors.begin(): saved GPS-only must not
|
||||
// cut supply until the first discovery pass has actually completed.
|
||||
assert(sensors._wisblock_power.isConfigured());
|
||||
assert(sensors._wisblock_power.discoveryActive());
|
||||
assert(sensors.isGpsPowered()&&digitalRead(WB_IO2)==HIGH); requireNoLow();
|
||||
Serial1.nmea(); assert(sensors.begin());
|
||||
assert(sensors.isGPSDetected()&&sensors.active()&&!sensors.userEnabled());
|
||||
assert(sensors.isGpsPowered()&&Serial1.installed); requireNoLow();
|
||||
assert(!sensors._wisblock_power.discoveryActive());
|
||||
assert(sensors.setSettingValue("gps",enabled?"1":"0"));
|
||||
assert(sensors.active()==enabled&&sensors.userEnabled()==enabled);
|
||||
const bool powered=enabled||shared||compile_held;
|
||||
assert(sensors.isGpsPowered()==powered&&digitalRead(WB_IO2)==int(powered));
|
||||
if(!powered) requireUartReleasedBeforePowerOff();
|
||||
else requireNoLow();
|
||||
}
|
||||
}
|
||||
}
|
||||
static void actualPoweredStatus() {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
|
||||
assert(!sensors.isGpsPowered());
|
||||
// Status must read the applied GPIO, not a provider whose absent enable pin
|
||||
// means 'always powered', and not merely the desired policy state.
|
||||
assert(location.isEnabled());
|
||||
pin_values[WB_IO2]=HIGH; assert(sensors.isGpsPowered());
|
||||
pin_values[WB_IO2]=LOW; assert(!sensors.isGpsPowered());
|
||||
}
|
||||
static void absent() {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location);
|
||||
assert(sensors.begin()); assert(!sensors.isGPSDetected()&&!sensors.active());
|
||||
assert(digitalRead(WB_IO2)==HIGH&&!sensors._wisblock_power.isConfigured());
|
||||
requireNoLow(); sensors.configureSharedPeripheralMode(false);
|
||||
assert(digitalRead(WB_IO2)==int(compile_held));
|
||||
const unsigned polls=Serial1.polls,begins=Serial1.begins;
|
||||
now_ms+=1000; sensors.loop(); assert(Serial1.polls==polls&&Serial1.begins==begins);
|
||||
assert(!sensors.setSettingValue("gps","1"));
|
||||
assert(!sensors.gpsSerialTransportCanYield(1)&&sensors.gpsSerialTransportMayConflict(1));
|
||||
}
|
||||
static void storage() {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location);
|
||||
mesh::ota::OtaStoreQspiNrf52::header_present=true;
|
||||
mesh::ota::OtaStoreQspiNrf52::selected=2;
|
||||
mesh::ota::OtaStoreQspiNrf52::w25_id=0xef4015;
|
||||
assert(sensors.begin()); assert(!sensors.isGPSDetected()&&Serial1.begins==0);
|
||||
sensors.configureSharedPeripheralMode(false);
|
||||
assert(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage);
|
||||
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
|
||||
sensors.configureSharedPeripheralMode(true);
|
||||
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
|
||||
}
|
||||
static void fixedStorage() {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); bootUart(sensors,location);
|
||||
assert(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage);
|
||||
assert(sensors.isGpsPowered()&&digitalRead(WB_IO2)==HIGH);
|
||||
assert(!sensors.active()&&!Serial1.installed); requireNoLow();
|
||||
assert(mesh::ota::OtaStoreQspiNrf52::probes==0);
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
assert(sensors.active()&&sensors.isGpsPowered());
|
||||
assert(sensors.setSettingValue("gps","0"));
|
||||
assert(!sensors.active()&&sensors.isGpsPowered()); requireNoLow();
|
||||
}
|
||||
static void storageIds() {
|
||||
for(uint8_t selected: {uint8_t(0),uint8_t(1),uint8_t(2),uint8_t(3)}) {
|
||||
for(uint32_t id: {0U,0xffffffU,0x123456U,0xef4015U}) {
|
||||
for(bool header: {false,true}) {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location);
|
||||
mesh::ota::OtaStoreQspiNrf52::selected=selected;
|
||||
if(header) mesh::ota::OtaStoreQspiNrf52::w25_id=id;
|
||||
else mesh::ota::OtaStoreQspiNrf52::rak_id=id;
|
||||
// Supply is protected while the read-only probe identifies a physical owner.
|
||||
assert(sensors.begin()); requireNoLow();
|
||||
sensors.configureSharedPeripheralMode(false);
|
||||
const bool storage_held=selected!=0||(id!=0&&id!=0xffffffU);
|
||||
assert(bool(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage)==storage_held);
|
||||
assert(digitalRead(WB_IO2)==int(storage_held||compile_held));
|
||||
assert(mesh::ota::OtaStoreQspiNrf52::probes>0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static void i2c(bool shared,unsigned wake_failure=0) {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); ublox_GNSS.present=true;
|
||||
assert(sensors.begin()); assert(sensors.isGPSDetected()&&sensors.active()&&!sensors.userEnabled());
|
||||
assert(i2cGPSFlag&&!serialGPSFlag&&sensors.getLocationProvider()==&RAK12500_provider);
|
||||
assert(RAK12500_provider.getPinEn()<0);
|
||||
sensors.configureSharedPeripheralMode(shared);
|
||||
assert(sensors.setSettingValue("gps","0"));
|
||||
const bool held=shared||compile_held;
|
||||
assert(!sensors.active()&&sensors.isGpsPowered()==held);
|
||||
if(held) { requireNoLow(); assert(ublox_GNSS.psm.back()); }
|
||||
else assert(ublox_GNSS.psm.empty());
|
||||
const unsigned begins=ublox_GNSS.begins,polls=ublox_GNSS.polls;
|
||||
now_ms+=1000; sensors.loop(); assert(ublox_GNSS.polls==polls);
|
||||
hardware_events.clear(); const uint32_t started=millis();
|
||||
const unsigned writes=ublox_GNSS.nv_writes;
|
||||
if(wake_failure==1) ublox_GNSS.begin_ok=false;
|
||||
if(wake_failure==2) ublox_GNSS.output_ok=false;
|
||||
if(wake_failure==3) ublox_GNSS.rate_ok=false;
|
||||
if(wake_failure==4) bus_available=false;
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
if(wake_failure) {
|
||||
assert(!held&&!sensors.active()&&!sensors.isGPSDetected()&&!sensors.isGpsPowered());
|
||||
assert(sensors.userEnabled()); // Persisted intent is not silently rewritten after failure.
|
||||
const unsigned failed_begins=ublox_GNSS.begins;
|
||||
for(unsigned i=0;i<10;++i) { now_ms+=1000; sensors.loop(); }
|
||||
assert(ublox_GNSS.begins==failed_begins&&ublox_GNSS.polls==polls);
|
||||
assert(!sensors.setSettingValue("gps","1"));
|
||||
return;
|
||||
}
|
||||
assert(ublox_GNSS.nv_writes==writes); // Cold duty wake must not write receiver flash/BBR again.
|
||||
assert(sensors.active()&&sensors.isGpsPowered());
|
||||
if(held) {
|
||||
assert(ublox_GNSS.begins==begins&&!ublox_GNSS.psm.back()); requireNoLow();
|
||||
} else {
|
||||
assert(ublox_GNSS.begins==begins+1);
|
||||
assert(firstEvent("write",WB_IO2,HIGH)<firstEvent("gnssBegin"));
|
||||
assert(hardware_events[firstEvent("gnssBegin")].time>=started+500);
|
||||
}
|
||||
assert(ublox_GNSS.flushes>0); // A cold wake never reuses a previous position as fresh.
|
||||
assert(sensors.setSettingValue("gps","0"));
|
||||
assert(!sensors.active()&&sensors.isGpsPowered()==held);
|
||||
}
|
||||
static void initialConfigFailure() {
|
||||
for(unsigned failed_step:{1U,2U}) {
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); ublox_GNSS.present=true;
|
||||
if(failed_step==1) ublox_GNSS.output_ok=false;
|
||||
else ublox_GNSS.rate_ok=false;
|
||||
assert(sensors.begin());
|
||||
assert(!sensors.isGPSDetected()&&!sensors.active()&&!i2cGPSFlag&&!serialGPSFlag);
|
||||
assert(sensors.getLocationProvider()==&location&&!Serial1.installed);
|
||||
assert(ublox_GNSS.output_calls==1&&ublox_GNSS.rate_calls==(failed_step==1?0U:1U));
|
||||
assert(ublox_GNSS.nv_writes==0&&!sensors.userEnabled());
|
||||
requireNoLow(); sensors.configureSharedPeripheralMode(false);
|
||||
assert(!sensors.isGpsPowered()&&digitalRead(WB_IO2)==LOW);
|
||||
const unsigned begins=ublox_GNSS.begins,polls=ublox_GNSS.polls;
|
||||
for(unsigned i=0;i<10;++i) { now_ms+=1000; sensors.loop(); }
|
||||
assert(ublox_GNSS.begins==begins&&ublox_GNSS.polls==polls);
|
||||
assert(!sensors.setSettingValue("gps","1"));
|
||||
}
|
||||
}
|
||||
static void dedicated() {
|
||||
#ifdef PIN_GPS_EN
|
||||
fresh(); TestLocation location; TestEnvironment sensors(location); Serial1.nmea();
|
||||
assert(sensors.begin()&&sensors.isGPSDetected()&&location.pin==PIN_GPS_EN);
|
||||
sensors.configureSharedPeripheralMode(true);
|
||||
assert(sensors.setSettingValue("gps","0"));
|
||||
requireUartReleasedBeforePowerOff(PIN_GPS_EN,!PIN_GPS_EN_ACTIVE);
|
||||
assert(!sensors.active()&&!sensors.isGpsPowered());
|
||||
assert(digitalRead(PIN_GPS_EN)==!PIN_GPS_EN_ACTIVE);
|
||||
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
|
||||
assert(sensors.gpsSerialTransportCanYield(1));
|
||||
const unsigned off_ends=Serial1.ends;
|
||||
assert(sensors.setGpsSerialTransportBlocked(1,true));
|
||||
assert(Serial1.ends==off_ends); // Already-off GPS must not close UARTE twice.
|
||||
assert(!sensors.transportAvailable()&&!sensors.isGpsPowered());
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
assert(sensors.userEnabled()&&!sensors.active()&&!sensors.isGpsPowered());
|
||||
hardware_events.clear(); assert(sensors.setGpsSerialTransportBlocked(1,false));
|
||||
assert(sensors.active()&&Serial1.installed&&sensors.isGpsPowered());
|
||||
assert(firstEvent("write",PIN_GPS_EN,PIN_GPS_EN_ACTIVE)<firstEvent("uartBegin"));
|
||||
assert(digitalRead(WB_IO2)==HIGH); requireNoLow();
|
||||
const unsigned on_ends=Serial1.ends;
|
||||
hardware_events.clear(); assert(sensors.setGpsSerialTransportBlocked(1,true));
|
||||
assert(!sensors.active()&&!Serial1.installed&&Serial1.ends==on_ends+1);
|
||||
requireUartReleasedBeforePowerOff(PIN_GPS_EN,!PIN_GPS_EN_ACTIVE);
|
||||
assert(!sensors.isGpsPowered()&&sensors.userEnabled()); requireNoLow();
|
||||
assert(sensors.setGpsSerialTransportBlocked(1,false));
|
||||
assert(sensors.active()&&sensors.isGpsPowered()&&Serial1.installed);
|
||||
#else
|
||||
assert(false&&"dedicated scenario requires a separate enable pin");
|
||||
#endif
|
||||
}
|
||||
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2);
|
||||
if(!std::strcmp(argv[1],"uart")) uart(false);
|
||||
else if(!std::strcmp(argv[1],"shared_uart")) uart(true);
|
||||
else if(!std::strcmp(argv[1],"held_uart")) uart(false);
|
||||
else if(!std::strcmp(argv[1],"hourly")) hourly();
|
||||
else if(!std::strcmp(argv[1],"repeat")) repeatedDiscovery();
|
||||
else if(!std::strcmp(argv[1],"prefs_before")) preferenceBeforeDiscovery();
|
||||
else if(!std::strcmp(argv[1],"powered_status")) actualPoweredStatus();
|
||||
else if(!std::strcmp(argv[1],"absent")) absent();
|
||||
else if(!std::strcmp(argv[1],"storage")) storage();
|
||||
else if(!std::strcmp(argv[1],"fixed_storage")) fixedStorage();
|
||||
else if(!std::strcmp(argv[1],"storage_ids")) storageIds();
|
||||
else if(!std::strcmp(argv[1],"i2c")) i2c(false);
|
||||
else if(!std::strcmp(argv[1],"shared_i2c")) i2c(true);
|
||||
else if(!std::strcmp(argv[1],"held_i2c")) i2c(false);
|
||||
else if(!std::strcmp(argv[1],"wake_failure")) i2c(false,1);
|
||||
else if(!std::strcmp(argv[1],"wake_output_failure")) i2c(false,2);
|
||||
else if(!std::strcmp(argv[1],"wake_rate_failure")) i2c(false,3);
|
||||
else if(!std::strcmp(argv[1],"wake_bus_failure")) i2c(false,4);
|
||||
else if(!std::strcmp(argv[1],"initial_config_failure")) initialConfigFailure();
|
||||
else if(!std::strcmp(argv[1],"dedicated")) dedicated();
|
||||
else assert(false&&"unknown scenario");
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#include <helpers/sensors/I2CAddressClaimPolicy.h>
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
|
||||
#define WB_IO2 21
|
||||
#define TELEM_WIRE (&Wire)
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
|
||||
void LocationProvider::sendSentence(const char*) {}
|
||||
|
||||
namespace mesh { namespace ota {
|
||||
struct OtaStoreQspiNrf52 {
|
||||
inline static unsigned probes=0;
|
||||
inline static uint32_t raw_id=0;
|
||||
static uint8_t autoDetect() { ++probes; assert(digitalRead(WB_IO2)==HIGH); return 0; }
|
||||
static void autoProbeIds(uint32_t& rak,uint32_t& w25) { rak=raw_id; w25=0; }
|
||||
};
|
||||
}}
|
||||
|
||||
static bool ensureI2cBusReleased(TwoWire*) { return true; }
|
||||
struct SensorDef {
|
||||
uint32_t address;
|
||||
const char* name;
|
||||
uint8_t (*init)(TwoWire*,uint8_t);
|
||||
void (*query)(uint8_t,uint8_t,CayenneLPP&);
|
||||
bool (*query_voltage)(uint8_t,float&);
|
||||
};
|
||||
static const SensorDef SENSOR_TABLE[1] = {};
|
||||
static const size_t SENSOR_TABLE_SIZE=0;
|
||||
bool EnvironmentSensorManager::i2c_probe(TwoWire&,uint8_t) { return false; }
|
||||
@METHODS@
|
||||
|
||||
struct TestEnvironment : EnvironmentSensorManager {
|
||||
using EnvironmentSensorManager::_wisblock_power;
|
||||
};
|
||||
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2&&!std::strcmp(argv[1],"no_gps"));
|
||||
for(bool prefs_first:{false,true}) {
|
||||
for(bool shared:{false,true}) {
|
||||
for(uint32_t id:{0U,0xffffffU,0x123456U}) {
|
||||
now_ms=100; hardware_events.clear();
|
||||
std::memset(pin_values,0,sizeof(pin_values)); pin_values[WB_IO2]=HIGH;
|
||||
mesh::ota::OtaStoreQspiNrf52::raw_id=id;
|
||||
mesh::ota::OtaStoreQspiNrf52::probes=0;
|
||||
TestEnvironment sensors;
|
||||
if(prefs_first) {
|
||||
sensors.configureSharedPeripheralMode(shared);
|
||||
assert(sensors._wisblock_power.discoveryActive());
|
||||
assert(digitalRead(WB_IO2)==HIGH);
|
||||
}
|
||||
assert(sensors.begin());
|
||||
if(!prefs_first) {
|
||||
// Even a no-GPS image must wait for validated mode after discovery.
|
||||
assert(digitalRead(WB_IO2)==HIGH);
|
||||
sensors.configureSharedPeripheralMode(shared);
|
||||
}
|
||||
assert(sensors._wisblock_power.isConfigured()&&!sensors._wisblock_power.discoveryActive());
|
||||
const bool storage=id!=0&&id!=0xffffffU;
|
||||
const bool held=shared||storage||mesh::gps::wisBlockCompileRailHolds()!=0;
|
||||
assert(digitalRead(WB_IO2)==int(held));
|
||||
assert(bool(sensors._wisblock_power.holds()&mesh::gps::ExternalStorage)==storage);
|
||||
assert(mesh::ota::OtaStoreQspiNrf52::probes>0);
|
||||
assert(!sensors.isGPSDetected()&&!sensors.isGpsPowered());
|
||||
assert(!sensors.setSettingValue("gps","1"));
|
||||
assert(!sensors.gpsUsesSerialUart(1)&&!sensors.gpsSerialTransportCanYield(1));
|
||||
assert(!sensors.setGpsSerialTransportBlocked(1,true));
|
||||
if(held) {
|
||||
for(const auto& event:hardware_events)
|
||||
assert(!(event.name=="write"&&event.a==WB_IO2&&event.b==LOW));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
#include <helpers/sensors/WisBlockSharedPowerPolicy.h>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
#include <initializer_list>
|
||||
|
||||
using mesh::gps::WisBlockPeripheralMode;
|
||||
using mesh::gps::WisBlockSharedPowerPolicy;
|
||||
using mesh::gps::RadioPower;
|
||||
using mesh::gps::EthernetPower;
|
||||
using mesh::gps::ExternalStorage;
|
||||
|
||||
static_assert(sizeof(WisBlockSharedPowerPolicy) == 2, "No allocation or added per-board RAM");
|
||||
static_assert(std::is_trivially_copyable<WisBlockSharedPowerPolicy>::value,
|
||||
"The rail policy must not acquire an owning allocation");
|
||||
static_assert(mesh::gps::wisBlockCompileRailHolds() == EXPECTED_COMPILE_HOLDS,
|
||||
"Compile-time radio/PoE dependencies are unconditional holds");
|
||||
|
||||
static void expectState(const WisBlockSharedPowerPolicy& policy, bool configured,
|
||||
bool discovery, bool gps, WisBlockPeripheralMode mode,
|
||||
uint8_t holds) {
|
||||
assert(policy.isConfigured() == configured);
|
||||
assert(policy.discoveryActive() == discovery);
|
||||
assert(policy.gpsActive() == gps);
|
||||
assert(policy.mode() == mode);
|
||||
assert(policy.holds() == holds);
|
||||
const bool independently_controllable = configured && !discovery
|
||||
&& mode == WisBlockPeripheralMode::GpsOnly && holds == 0;
|
||||
assert(policy.gpsPowerIndependentlyControllable() == independently_controllable);
|
||||
assert(policy.canPowerDown() == (independently_controllable && !gps));
|
||||
assert(policy.railEnabled() == !(independently_controllable && !gps));
|
||||
}
|
||||
|
||||
static void bootAndDiscovery() {
|
||||
WisBlockSharedPowerPolicy policy;
|
||||
expectState(policy, false, true, false, WisBlockPeripheralMode::GpsOnly,
|
||||
EXPECTED_COMPILE_HOLDS);
|
||||
// Even an early discovery completion cannot cut power before saved mode is known.
|
||||
policy.setDiscoveryActive(false);
|
||||
expectState(policy, false, false, false, WisBlockPeripheralMode::GpsOnly,
|
||||
EXPECTED_COMPILE_HOLDS);
|
||||
policy.setDiscoveryActive(true);
|
||||
policy.setGpsActive(true);
|
||||
assert(policy.configure(WisBlockPeripheralMode::GpsOnly));
|
||||
expectState(policy, true, true, true, WisBlockPeripheralMode::GpsOnly,
|
||||
EXPECTED_COMPILE_HOLDS);
|
||||
policy.setGpsActive(false);
|
||||
assert(policy.railEnabled()); // Saved disabled GPS cannot defeat discovery protection.
|
||||
policy.setDiscoveryActive(false);
|
||||
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
|
||||
EXPECTED_COMPILE_HOLDS);
|
||||
// Configuration is boot-only: changing a saved mode must not cause a live rail transition.
|
||||
assert(!policy.configure(WisBlockPeripheralMode::Shared, ExternalStorage));
|
||||
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
|
||||
EXPECTED_COMPILE_HOLDS);
|
||||
policy.setGpsActive(true);
|
||||
expectState(policy, true, false, true, WisBlockPeripheralMode::GpsOnly,
|
||||
EXPECTED_COMPILE_HOLDS);
|
||||
policy.setGpsActive(false);
|
||||
policy.setDiscoveryActive(true); // A subsequent safe probe reacquires its hold.
|
||||
assert(policy.railEnabled() && !policy.gpsPowerIndependentlyControllable());
|
||||
policy.setDiscoveryActive(false);
|
||||
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
|
||||
EXPECTED_COMPILE_HOLDS);
|
||||
}
|
||||
|
||||
static void everyOwnershipCombination() {
|
||||
// External NOR is a physical dependency, independent of user/service enablement.
|
||||
// All valid owner unions must survive GPS settings and repeated probes.
|
||||
for (uint8_t initial = 0; initial < 8; ++initial) {
|
||||
for (uint8_t detected = 0; detected < 8; ++detected) {
|
||||
for (uint8_t mode_value = 0; mode_value < 2; ++mode_value) {
|
||||
const auto mode = static_cast<WisBlockPeripheralMode>(mode_value);
|
||||
WisBlockSharedPowerPolicy policy(initial);
|
||||
assert(policy.configure(mode, detected));
|
||||
const uint8_t holds = static_cast<uint8_t>(initial | detected | EXPECTED_COMPILE_HOLDS);
|
||||
for (unsigned repeat = 0; repeat < 2; ++repeat) {
|
||||
for (bool discovery : {false, true}) {
|
||||
for (bool gps : {false, true}) {
|
||||
policy.setDiscoveryActive(discovery);
|
||||
policy.setGpsActive(gps);
|
||||
expectState(policy, true, discovery, gps, mode, holds);
|
||||
assert(!policy.configure(WisBlockPeripheralMode::GpsOnly, 0));
|
||||
expectState(policy, true, discovery, gps, mode, holds);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
WisBlockSharedPowerPolicy storage;
|
||||
assert(storage.configure(WisBlockPeripheralMode::GpsOnly, ExternalStorage));
|
||||
storage.setDiscoveryActive(false);
|
||||
assert(storage.railEnabled() && !storage.canPowerDown());
|
||||
assert(!storage.gpsPowerIndependentlyControllable());
|
||||
WisBlockSharedPowerPolicy shared;
|
||||
assert(shared.configure(WisBlockPeripheralMode::SharedPeripherals));
|
||||
shared.setDiscoveryActive(false);
|
||||
assert(shared.railEnabled() && !shared.gpsPowerIndependentlyControllable());
|
||||
}
|
||||
|
||||
static void rejectedConfigurationRetainsOwners() {
|
||||
WisBlockSharedPowerPolicy policy(RadioPower);
|
||||
policy.setGpsActive(true);
|
||||
assert(!policy.configure(static_cast<WisBlockPeripheralMode>(2), ExternalStorage));
|
||||
expectState(policy, false, true, true, WisBlockPeripheralMode::GpsOnly,
|
||||
RadioPower | EXPECTED_COMPILE_HOLDS);
|
||||
assert(!policy.configure(static_cast<WisBlockPeripheralMode>(255), 0));
|
||||
assert(!policy.configure(WisBlockPeripheralMode::GpsOnly, 0x80));
|
||||
expectState(policy, false, true, true, WisBlockPeripheralMode::GpsOnly,
|
||||
RadioPower | EXPECTED_COMPILE_HOLDS);
|
||||
assert(policy.configure(WisBlockPeripheralMode::GpsOnly, ExternalStorage));
|
||||
policy.setGpsActive(false);
|
||||
policy.setDiscoveryActive(false);
|
||||
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
|
||||
RadioPower | ExternalStorage | EXPECTED_COMPILE_HOLDS);
|
||||
assert(!policy.configure(WisBlockPeripheralMode::Shared, EthernetPower));
|
||||
expectState(policy, true, false, false, WisBlockPeripheralMode::GpsOnly,
|
||||
RadioPower | ExternalStorage | EXPECTED_COMPILE_HOLDS);
|
||||
// A future/unknown constructor owner stays fail-closed rather than being masked out.
|
||||
for (uint8_t unknown : {uint8_t(0x08), uint8_t(0x80), uint8_t(0xff)}) {
|
||||
WisBlockSharedPowerPolicy held(unknown);
|
||||
assert(held.configure(WisBlockPeripheralMode::GpsOnly));
|
||||
held.setDiscoveryActive(false);
|
||||
expectState(held, true, false, false, WisBlockPeripheralMode::GpsOnly,
|
||||
static_cast<uint8_t>(unknown | EXPECTED_COMPILE_HOLDS));
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
bootAndDiscovery();
|
||||
everyOwnershipCombination();
|
||||
rejectedConfigurationRetainsOwners();
|
||||
}
|
||||
@@ -429,7 +429,13 @@ for env_name in (
|
||||
|
||||
rak_usb = "RAK_3401_companion_radio_usb"
|
||||
require(rak_usb, "build_flags", "RAK_BOARD")
|
||||
require(rak_usb, "build_flags", "FORCE_GPS_ALIVE")
|
||||
require(rak_usb, "build_flags", "RAK_3401")
|
||||
require(rak_usb, "build_flags", "PIN_GPS_EN=-1")
|
||||
reject(rak_usb, "build_flags", "FORCE_GPS_ALIVE")
|
||||
for env_name in ("RAK_4631_companion_radio_usb", "RAK_4631_repeater"):
|
||||
require(env_name, "build_flags", "RAK_4631")
|
||||
require(env_name, "build_flags", "PIN_GPS_EN=-1")
|
||||
reject(env_name, "build_flags", "FORCE_GPS_ALIVE")
|
||||
'
|
||||
|
||||
# Every qualified ESP32 Companion layout publishes one Full image even when
|
||||
|
||||
@@ -185,11 +185,12 @@ class CompanionPrefsValuesTests(unittest.TestCase):
|
||||
r'^\s*(?:float|char|u?int(?:8|16|32)_t)\s+([A-Za-z_][A-Za-z_0-9]*)',
|
||||
prefix, re.M)
|
||||
self.assertEqual(declarations[0], 'airtime_factor')
|
||||
self.assertEqual(declarations[-1], 'lost_reply')
|
||||
self.assertEqual(declarations[-2:], ['lost_reply', 'peripherals_shared'])
|
||||
optional = {
|
||||
'fan_mode': 'TBEAM_1W', 'fan_lo': 'TBEAM_1W', 'fan_hi': 'TBEAM_1W',
|
||||
'wifi_ssid': 'defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)',
|
||||
'wifi_pwd': 'defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)',
|
||||
'peripherals_shared': 'RAK_4631',
|
||||
}
|
||||
|
||||
def fields(template):
|
||||
|
||||
@@ -122,10 +122,13 @@ class CompanionSettingsPersistenceContractTests(unittest.TestCase):
|
||||
"bool copyPersistedValuesFrom(const CompanionNodePrefs& source)",
|
||||
)
|
||||
self.assertIn("reinterpret_cast<uintptr_t>(&airtime_factor)", copy)
|
||||
# The appended lost-reply byte is the last persisted value. Both
|
||||
# snapshots must stop there before either runtime adapter begins.
|
||||
self.assertIn("reinterpret_cast<uintptr_t>(&lost_reply)", copy)
|
||||
self.assertIn("reinterpret_cast<uintptr_t>(&values.lost_reply)", copy)
|
||||
# RAK4631 appends its peripheral-mode byte; every other target stops
|
||||
# at lost-reply. Neither endpoint may include a runtime adapter.
|
||||
self.assertIn("const auto& last_value = peripherals_shared;", copy)
|
||||
self.assertIn("const auto& last_value = lost_reply;", copy)
|
||||
self.assertIn("reinterpret_cast<uintptr_t>(&last_value)", copy)
|
||||
self.assertIn("&values.peripherals_shared", copy)
|
||||
self.assertIn("&values.lost_reply", copy)
|
||||
self.assertIn("+ sizeof(lost_reply)", copy)
|
||||
self.assertIn("end - begin == size - sizeof(values.lost_reply)", copy)
|
||||
self.assertIn("reinterpret_cast<uintptr_t>(&values.radio)", copy)
|
||||
|
||||
@@ -399,7 +399,8 @@ class CooperativeGpsDiscoveryTests(unittest.TestCase):
|
||||
with self.subTest(defines=defines):
|
||||
result = subprocess.run([self.compiler, "-E", "-P", "-x", "c++",
|
||||
"-DENV_INCLUDE_GPS=1", *["-D" + value for value in defines],
|
||||
"-I" + str(work), str(cpp)], text=True, capture_output=True, timeout=10)
|
||||
"-I" + str(work), "-I" + str(ROOT / "src/helpers/sensors"),
|
||||
str(cpp)], text=True, capture_output=True, timeout=10)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual("gps_discovery_pending" in result.stdout, expected)
|
||||
|
||||
|
||||
@@ -41,7 +41,11 @@ struct LocationProvider {
|
||||
struct Sensors {
|
||||
LocationProvider* provider;
|
||||
const char* user = "0";
|
||||
bool physical_override = false, physical_power = false;
|
||||
LocationProvider* getLocationProvider() { return provider; }
|
||||
bool isGpsPowered() const {
|
||||
return physical_override ? physical_power : (provider && provider->powered);
|
||||
}
|
||||
const char* getSettingByKey(const char*) { return user; }
|
||||
};
|
||||
struct Prefs { bool powersaving_enabled = true; };
|
||||
@@ -58,6 +62,12 @@ int main() {
|
||||
gps.powered = true;
|
||||
status(&sensors, &prefs, reply);
|
||||
assert(!strcmp(reply, "off, powered"));
|
||||
// A provider without its own enable pin can report enabled despite a cut
|
||||
// shared rail. The manager's physical policy must win over that stale view.
|
||||
sensors.physical_override = true; sensors.physical_power = false;
|
||||
status(&sensors, &prefs, reply);
|
||||
assert(!strcmp(reply, "off, unpowered"));
|
||||
sensors.physical_override = false;
|
||||
sensors.user = "1"; gps.powered = false;
|
||||
status(&sensors, &prefs, reply);
|
||||
assert(!strcmp(reply, "on, unpowered (powersaving, wake in 0h 30m), last sync: none"));
|
||||
|
||||
@@ -259,18 +259,35 @@ class Nrf52VariantContractsTest(unittest.TestCase):
|
||||
self.assertIn("strap INA3221 A0 to SCL for 0x43", recipe)
|
||||
self.assertIn("-DTELEM_INA3221_ADDRESS=0x43", recipe)
|
||||
|
||||
def test_rak4631_shared_sensor_rail_stays_enabled(self) -> None:
|
||||
def test_rak4631_shared_sensor_rail_is_owned_by_boot_policy(self) -> None:
|
||||
recipe = (ROOT / "variants/rak4631/platformio.ini").read_text()
|
||||
rak3401_recipe = (ROOT / "variants/rak3401/platformio.ini").read_text()
|
||||
board = (ROOT / "variants/rak4631/RAK4631Board.cpp").read_text()
|
||||
sensors = (
|
||||
ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp"
|
||||
sensor_header = (
|
||||
ROOT / "src/helpers/sensors/EnvironmentSensorManager.h"
|
||||
).read_text()
|
||||
policy = (
|
||||
ROOT / "src/helpers/sensors/WisBlockSharedPowerPolicy.h"
|
||||
).read_text()
|
||||
prefs = (ROOT / "src/helpers/PeripheralModePrefs.h").read_text()
|
||||
|
||||
self.assertRegex(recipe, r"-D\s+FORCE_GPS_ALIVE\b")
|
||||
for current_recipe in (recipe, rak3401_recipe):
|
||||
self.assertNotRegex(current_recipe, r"-D\s+FORCE_GPS_ALIVE\b")
|
||||
self.assertIn("peripherals.mode defaults to gps-only", recipe)
|
||||
self.assertIn("pinMode(WB_IO2, OUTPUT)", board)
|
||||
self.assertIn("digitalWrite(WB_IO2, HIGH)", board)
|
||||
self.assertIn("setRakGpsControl(ioPin, shared_power_rail, true)", sensors)
|
||||
self.assertIn("setRakGpsControl(ioPin, shared_power_rail, false)", sensors)
|
||||
self.assertIn("WisBlockSharedPowerPolicy _wisblock_power", sensor_header)
|
||||
self.assertIn("_state(DiscoveryActive)", policy)
|
||||
self.assertIn("GpsOnly = 0", policy)
|
||||
self.assertIn("Shared = 1", policy)
|
||||
self.assertIn("#if defined(RAK_3401)", policy)
|
||||
self.assertIn("| RadioPower", policy)
|
||||
self.assertIn("defined(ETHERNET_ENABLED)", policy)
|
||||
self.assertIn("| EthernetPower", policy)
|
||||
self.assertIn("ExternalStorage =", policy)
|
||||
self.assertIn("get peripherals.mode", prefs)
|
||||
self.assertIn("set peripherals.mode", prefs)
|
||||
self.assertIn("saved; reboot to apply", prefs)
|
||||
|
||||
def test_rak4631_ethernet_does_not_suppress_shared_rail_gps(self) -> None:
|
||||
recipe = (ROOT / "variants/rak4631/platformio.ini").read_text()
|
||||
@@ -362,17 +379,17 @@ class Nrf52VariantContractsTest(unittest.TestCase):
|
||||
self.assertIn(
|
||||
"return uart == 1 && gps_detected && gps_serial_transport", sensors
|
||||
)
|
||||
self.assertIn(
|
||||
"#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)",
|
||||
can_yield,
|
||||
shared_rail_guard = (
|
||||
"#if defined(RAK_WISBLOCK_GPS) "
|
||||
"&& !(defined(PIN_GPS_EN) && PIN_GPS_EN >= 0)"
|
||||
)
|
||||
forced_rak = can_yield[
|
||||
can_yield.index(
|
||||
"#if defined(RAK_WISBLOCK_GPS) && defined(FORCE_GPS_ALIVE)"
|
||||
):
|
||||
can_yield.index("#elif defined(RAK_WISBLOCK_GPS)")
|
||||
self.assertIn(shared_rail_guard, can_yield)
|
||||
shared_rak = can_yield[
|
||||
can_yield.index(shared_rail_guard):
|
||||
can_yield.index("#endif", can_yield.index(shared_rail_guard))
|
||||
]
|
||||
self.assertIn("return false;", forced_rak)
|
||||
self.assertIn("return false;", shared_rak)
|
||||
self.assertNotIn("FORCE_GPS_ALIVE", can_yield)
|
||||
refusal = block.index(
|
||||
"if (blocked && !gpsSerialTransportCanYield(uart)) return false;"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
"""Execute RAK4631 append-only policy images, CLI rollback and boot ordering."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_common_usb_debug import HARNESS as COMMON_HARNESS
|
||||
from test_companion_preferences_transaction import HARNESS as COMPANION_HARNESS
|
||||
from test_radio_receive_contract import method
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def compile_run(code, macros=(), *, serializers=False):
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-peripheral-mode-") as directory:
|
||||
work = Path(directory)
|
||||
(work / "helpers").mkdir()
|
||||
fs = (ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h").read_text()
|
||||
fs = fs.replace(" size_t available() const", " bool seek(size_t p) { if (p > size()) return false; cursor_ = p; return true; }\n size_t available() const")
|
||||
(work / "helpers/IdentityStore.h").write_text(fs)
|
||||
(work / "Utils.h").write_text("""#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <cassert>
|
||||
namespace mesh { struct Utils {
|
||||
static void printHex(Stream&,const uint8_t*,size_t){assert(false);}
|
||||
static void fromHex(uint8_t*,size_t,const char*){assert(false);}
|
||||
}; }
|
||||
""")
|
||||
(work / "platform_shim.h").write_text("""#include <cstdlib>
|
||||
#include <cstdio>
|
||||
inline char* utoa(unsigned int v,char* p,int b){if(b!=10)abort();sprintf(p,"%u",v);return p;}
|
||||
""")
|
||||
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text()
|
||||
(work / "ContactFileTransaction.h").write_text(transaction.replace(
|
||||
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
|
||||
cpp, binary = work / "test.cpp", work / "test"
|
||||
cpp.write_text(code)
|
||||
flags = [] if os.name == "nt" else ["-fsanitize=address,undefined",
|
||||
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
command = ["g++", "-std=c++17", *flags, *["-D" + x + "=1" for x in macros],
|
||||
"-include", str(work / "platform_shim.h"), "-I" + str(work),
|
||||
"-I" + str(ROOT / "test/fixtures/radio_profiles/mocks"),
|
||||
"-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
|
||||
"-I" + str(ROOT / "src/helpers"), "-I" + str(ROOT), str(cpp)]
|
||||
if serializers:
|
||||
command += [str(ROOT / "src/helpers/" / name) for name in (
|
||||
"ConfigSerializer.cpp", "DynamicConfigSerializer.cpp", "CommonRadioPrefs.cpp",
|
||||
"TxtDataHelpers.cpp")]
|
||||
build = subprocess.run(command + ["-o", str(binary)], capture_output=True, text=True, timeout=90)
|
||||
if build.returncode:
|
||||
raise AssertionError(build.stdout + build.stderr)
|
||||
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=30)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stdout + result.stderr)
|
||||
|
||||
|
||||
def common_code():
|
||||
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
header = (ROOT / "src/helpers/CommonCLI.h").read_text()
|
||||
fields = header.split("class NodePrefs :", 1)[1].split("private:", 1)[0]
|
||||
fields = "\n".join(re.findall(r"^\s*(?:float|double|char|u?int(?:8|16|32)_t)\s+[^;]+;", fields, re.M))
|
||||
constants = "\n".join(re.findall(r"^\s*#define\s+\w+\s+(?:0x[\dA-Fa-f]+|\d+)\s*$", header, re.M))
|
||||
constants += "\n" + "\n".join(re.findall(r"^static const size_t LEGACY_\w+\s*=[^;]+;", source, re.M))
|
||||
constants += "\n" + re.search(r"^static constexpr uint8_t DEFAULT_ADVERT_LOC_POLICY\s*=[^;]+;", header, re.M).group()
|
||||
helpers = "\n".join(extract_braced(source, signature) for signature in (
|
||||
"static bool bwMatches(", "static bool isValidLoRaBandwidth(",
|
||||
"static float defaultLoRaBandwidth(", "static void markDirectRetryPrefsValid(",
|
||||
"static void applyFloodRetryPreset(", "static void applyDirectRetryPreset(",
|
||||
"static void setDefaultDirectRetryPrefs(", "static bool directRetryPrefsValid("))
|
||||
methods = "\n".join(extract_braced(source, signature) for signature in (
|
||||
"static const char* commonPrefsSaveResultName(", "bool CommonCLI::recoverCommonPrefsFiles(",
|
||||
"bool CommonCLI::saveCommonPrefsImageAtomically(", "void CommonCLI::savePrefs(FILESYSTEM*",
|
||||
"void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope)", "bool CommonCLI::trySavePrefs(",
|
||||
"void CommonCLI::loadPrefsInt("))
|
||||
code = COMMON_HARNESS.split("int main(")[0]
|
||||
for name, value in {
|
||||
"FIELDS": fields, "CONSTANTS": constants, "HELPERS": helpers,
|
||||
"LEGACY": extract_braced(header, "struct LegacyObserverTail") + ";",
|
||||
"SERIALIZER": "template<typename Writer>\n" + extract_braced(source, "static bool writeCommonPrefsImage("),
|
||||
"STORE": extract_braced(source, "class CommonPrefsFileStore"), "METHODS": methods,
|
||||
"DYNAMIC": extract_braced(header, "class BridgePrefs :"),
|
||||
"SET_DEBUG": "", "SET_MASTER": "", "GET_DEBUG": "", "GET_MASTER": "",
|
||||
}.items():
|
||||
code = code.replace("@" + name + "@", value)
|
||||
return "#include <helpers/PeripheralModePrefs.h>\n#include <helpers/bridges/RS232PowerSettings.h>\n" + code
|
||||
|
||||
|
||||
class PeripheralModePrefsTests(unittest.TestCase):
|
||||
def test_actual_cli_wrappers_use_saved_intent_without_hot_gpio(self):
|
||||
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
snippets = []
|
||||
for source, signature in (
|
||||
(common, "void CommonCLI::handleSetCmd("),
|
||||
(common, "void CommonCLI::handleGetCmd("),
|
||||
(companion, "bool MyMesh::handleLocalControlCommand(")):
|
||||
snippet = method(source, signature).split("#if defined(RAK_4631)", 1)[1].split("#else", 1)[0]
|
||||
self.assertIn("mesh::peripherals::handleCommand", snippet)
|
||||
snippets.append(snippet)
|
||||
code = r'''
|
||||
#include <helpers/PeripheralModePrefs.h>
|
||||
#include <cassert>
|
||||
struct Prefs{uint8_t peripherals_shared=0;};
|
||||
struct Sensors{bool supported=true;unsigned applications=0;
|
||||
bool supportsSharedPeripheralMode()const{return supported;}
|
||||
void configureSharedPeripheralMode(bool){++applications;}
|
||||
};
|
||||
struct Common{
|
||||
Prefs prefs,durable;Prefs* _prefs=&prefs;Sensors sensor;Sensors* _sensors=&sensor;
|
||||
bool ok=true;unsigned saves=0;
|
||||
bool trySavePrefs(){++saves;if(ok)durable=prefs;return ok;}
|
||||
void set(const char* command,char* reply){@SET@}
|
||||
void get(const char* command,char* reply){@GET@}
|
||||
};
|
||||
struct Companion{
|
||||
Prefs _prefs,durable;Sensors sensors;bool ok=true;unsigned saves=0;
|
||||
bool savePrefs(){++saves;if(ok)durable=_prefs;return ok;}
|
||||
bool command(const char* command,char* reply,size_t reply_size){@COMPANION@ return false;}
|
||||
};
|
||||
int main(){Common common;Companion companion;char reply[160];
|
||||
common.set("set peripherals.mode shared",reply);
|
||||
assert(common.prefs.peripherals_shared==1&&common.durable.peripherals_shared==1&&strstr(reply,"reboot"));
|
||||
assert(companion.command("set peripherals.mode shared",reply,sizeof(reply)));
|
||||
assert(companion._prefs.peripherals_shared==1&&companion.durable.peripherals_shared==1&&strstr(reply,"reboot"));
|
||||
assert(common.sensor.applications==0&&companion.sensors.applications==0);
|
||||
common.ok=companion.ok=false;
|
||||
common.set("set peripherals.mode gps-only",reply);assert(strstr(reply,"could not be saved"));
|
||||
assert(companion.command("set peripherals.mode gps-only",reply,sizeof(reply))&&strstr(reply,"could not be saved"));
|
||||
assert(common.prefs.peripherals_shared==1&&companion._prefs.peripherals_shared==1);
|
||||
common.get("get peripherals.mode",reply);assert(strstr(reply,"shared"));
|
||||
assert(companion.command("get peripherals.mode",reply,sizeof(reply))&&strstr(reply,"shared"));
|
||||
unsigned cs=common.saves,ps=companion.saves;
|
||||
common.sensor.supported=companion.sensors.supported=false;
|
||||
common.set("set peripherals.mode gps-only",reply);assert(strstr(reply,"unsupported"));
|
||||
assert(companion.command("set peripherals.mode gps-only",reply,sizeof(reply))&&strstr(reply,"unsupported"));
|
||||
assert(common.saves==cs&&companion.saves==ps&&common.sensor.applications==0&&companion.sensors.applications==0);
|
||||
}
|
||||
'''
|
||||
for key, snippet in zip(("SET", "GET", "COMPANION"), snippets):
|
||||
code = code.replace("@" + key + "@", snippet)
|
||||
compile_run(code)
|
||||
|
||||
def test_codec_and_strict_cli_transactions(self):
|
||||
compile_run(r'''
|
||||
#include <helpers/PeripheralModePrefs.h>
|
||||
#include <cassert>
|
||||
#include <initializer_list>
|
||||
int main(){
|
||||
uint8_t mode=0, durable=0;unsigned saves=0;bool ok=true;char reply[160];
|
||||
auto save=[&](){++saves;if(ok)durable=mode;return ok;};
|
||||
assert(mesh::peripherals::handleCommand(mode,"get peripherals.mode",reply,sizeof(reply),true,save));
|
||||
assert(strstr(reply,"gps-only")&&saves==0);
|
||||
for(const char* text:{"shared","gps-only"}){
|
||||
char cmd[80];snprintf(cmd,sizeof(cmd),"set peripherals.mode %s",text);
|
||||
assert(mesh::peripherals::handleCommand(mode,cmd,reply,sizeof(reply),true,save));
|
||||
assert(mode==(strcmp(text,"shared")==0)&&durable==mode&&strstr(reply,"reboot"));
|
||||
}
|
||||
for(const char* text:{"", "on", "1", "SHARED", "shared junk", "shared ", "gps-only\n"}){
|
||||
unsigned before=saves;char cmd[80];snprintf(cmd,sizeof(cmd),"set peripherals.mode %s",text);
|
||||
assert(mesh::peripherals::handleCommand(mode,cmd,reply,sizeof(reply),true,save));
|
||||
assert(strstr(reply,"Error")&&saves==before&&mode==durable);
|
||||
}
|
||||
ok=false;assert(mesh::peripherals::handleCommand(mode,"set peripherals.mode shared",reply,sizeof(reply),true,save));
|
||||
assert(mode==0&&durable==0&&strstr(reply,"could not be saved"));
|
||||
unsigned before=saves;
|
||||
assert(mesh::peripherals::handleCommand(mode,"set peripherals.mode shared",reply,sizeof(reply),false,save));
|
||||
assert(strstr(reply,"unsupported")&&saves==before&&mode==0);
|
||||
assert(!mesh::peripherals::handleCommand(mode,"set peripherals.modeX shared",reply,sizeof(reply),true,save));
|
||||
for(uint8_t value:{0,1}){
|
||||
uint8_t footer[8];mesh::peripherals::encodeFooter(footer,value);uint8_t decoded=9;
|
||||
assert(mesh::peripherals::decodeFooter(footer,sizeof(footer),decoded)&&decoded==value);
|
||||
for(unsigned i=0;i<8;++i){footer[i]^=1;decoded=9;
|
||||
assert(!mesh::peripherals::decodeFooter(footer,sizeof(footer),decoded)&&decoded==9);footer[i]^=1;}
|
||||
for(unsigned n=0;n<8;++n)assert(!mesh::peripherals::decodeFooter(footer,n,decoded));
|
||||
}
|
||||
}
|
||||
''')
|
||||
|
||||
def test_actual_common_images_and_footer_fail_closed(self):
|
||||
code = common_code() + r'''
|
||||
int main(){
|
||||
CommonCLI writer;writer.prefs.peripherals_shared=1;strcpy(writer.prefs.password,"keep secret");
|
||||
strcpy(writer.prefs.node_name,"keep node");writer.savePrefs(&writer.fs,PrefsSaveRouting::Scope::Common);
|
||||
assert(writer._common_save_succeeded);const auto disk=writer.fs.files["/com_prefs"];
|
||||
const size_t legacy=mesh::peripherals::CommonLegacySize+
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
mesh::peripherals::LegacyRs232TailSize;
|
||||
#else
|
||||
0;
|
||||
#endif
|
||||
assert(disk.size()==legacy+8);Capture capture;
|
||||
assert(writeCommonPrefsImage(capture,&writer.prefs)&&capture.bytes==disk);
|
||||
for(uint8_t mode:{0,1}){
|
||||
writer.prefs.peripherals_shared=mode;writer.savePrefs(&writer.fs,PrefsSaveRouting::Scope::Common);
|
||||
CommonCLI reader;reader.fs.files=writer.fs.files;reader.prefs.peripherals_shared=9;
|
||||
reader.loadPrefsInt(&reader.fs,"/com_prefs");assert(reader.prefs.peripherals_shared==mode);
|
||||
assert(!strcmp(reader.prefs.password,"keep secret")&&!strcmp(reader.prefs.node_name,"keep node"));
|
||||
}
|
||||
for(size_t keep=legacy;keep<disk.size();++keep){
|
||||
CommonCLI reader;reader.fs.files["/com_prefs"]={disk.begin(),disk.begin()+keep};
|
||||
reader.loadPrefsInt(&reader.fs,"/com_prefs");
|
||||
assert(reader.prefs.peripherals_shared==(keep==legacy?0:1));
|
||||
}
|
||||
for(size_t byte=legacy;byte<disk.size();++byte){
|
||||
CommonCLI reader;reader.fs.files["/com_prefs"]=disk;reader.fs.files["/com_prefs"][byte]^=1;
|
||||
reader.loadPrefsInt(&reader.fs,"/com_prefs");assert(reader.prefs.peripherals_shared==1);
|
||||
}
|
||||
// The optional UART tail belongs to the source profile, not the reader.
|
||||
// Both no-UART -> UART and UART -> no-UART upgrades retain the checked mode.
|
||||
for(bool uart:{false,true}){
|
||||
std::vector<uint8_t> foreign(disk.begin(),disk.begin()+mesh::peripherals::CommonLegacySize);
|
||||
if(uart){uint8_t tail[mesh::bridge::rs232power::PREFS_SIZE];
|
||||
mesh::bridge::rs232power::encodePrefs(tail,1,1,300);foreign.insert(foreign.end(),tail,tail+sizeof(tail));}
|
||||
CommonCLI old;old.fs.files["/com_prefs"]=foreign;old.loadPrefsInt(&old.fs,"/com_prefs");
|
||||
assert(old.prefs.peripherals_shared==0);
|
||||
uint8_t footer[8];mesh::peripherals::encodeFooter(footer,1);foreign.insert(foreign.end(),footer,footer+8);
|
||||
CommonCLI migrated;migrated.fs.files["/com_prefs"]=foreign;migrated.loadPrefsInt(&migrated.fs,"/com_prefs");
|
||||
assert(migrated.prefs.peripherals_shared==1);
|
||||
}
|
||||
CommonCLI unreadable;unreadable.fs.files["/com_prefs"]=disk;unreadable.fs.fail_read_open=true;
|
||||
unreadable.loadPrefsInt(&unreadable.fs,"/com_prefs");assert(unreadable.prefs.peripherals_shared==1);
|
||||
CommonCLI failed;failed.fs.files["/com_prefs"]=disk;failed.fs.fail_write_after=int(legacy+4);
|
||||
failed.prefs.peripherals_shared=0;failed.savePrefs(&failed.fs,PrefsSaveRouting::Scope::Common);
|
||||
assert(!failed._common_save_succeeded&&failed.fs.files["/com_prefs"]==disk);
|
||||
}
|
||||
'''
|
||||
for flags in (("NRF52_PLATFORM", "RAK_4631", "ENABLE_OTA"),
|
||||
("NRF52_PLATFORM", "RAK_4631", "ENABLE_OTA", "WITH_MQTT_BRIDGE"),
|
||||
("NRF52_PLATFORM", "RAK_4631", "ENABLE_OTA", "WITH_RS232_BRIDGE")):
|
||||
with self.subTest(flags=flags):
|
||||
# Only the profile's compile-time UART constant matters here.
|
||||
compile_run("#define WITH_RS232_BRIDGE_UART 2\n" + code, flags)
|
||||
|
||||
def test_actual_companion_images_and_adapter_snapshot(self):
|
||||
source = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
|
||||
methods = "\n".join(method(source, signature) for signature in (
|
||||
"bool DataStore::loadPrefs(", "bool DataStore::loadPrefsInt(", "bool DataStore::savePrefs("))
|
||||
code = "#include <helpers/PeripheralModePrefs.h>\n" + COMPANION_HARNESS.split("int main(")[0].replace("@METHODS@", methods)
|
||||
code += r'''
|
||||
int main(){
|
||||
DataStore store;CompanionNodePrefs prefs;prefs.peripherals_shared=1;
|
||||
strcpy(prefs.node_name,"retained name");prefs.ble_pin=654321;prefs.gps_interval=42;
|
||||
assert(prefs.getCustom()->setByKey("snapshot_tag","original"));
|
||||
assert(store.savePrefs(prefs,47.2,-122.1));const auto disk=store.fs.files["/new_prefs"];
|
||||
assert(disk.size()==244);double lat=0,lon=0;
|
||||
CompanionNodePrefs loaded;assert(loaded.getCustom()->setByKey("snapshot_tag","loaded"));
|
||||
assert(store.loadPrefsInt("/new_prefs",loaded,lat,lon));
|
||||
assert(loaded.peripherals_shared==1&&loaded.ble_pin==654321&&loaded.gps_interval==42);
|
||||
assert(!strcmp(loaded.node_name,"retained name")&&lat==47.2&&lon==-122.1);
|
||||
char tag[32];assert(loaded.getCustom()->getByKey("snapshot_tag",tag,sizeof(tag))&&!strcmp(tag,"loaded"));
|
||||
loaded.getRadioPrefs()->setFreq(919.5f);assert(loaded.freq==919.5f&&prefs.freq!=919.5f);
|
||||
for(size_t keep=236;keep<244;++keep){
|
||||
DataStore damaged;damaged.fs.files["/new_prefs"]={disk.begin(),disk.begin()+keep};
|
||||
CompanionNodePrefs candidate;candidate.peripherals_shared=1;candidate.ble_pin=123456;
|
||||
const bool ok=damaged.loadPrefsInt("/new_prefs",candidate,lat,lon);
|
||||
assert(ok==(keep==236));if(ok)assert(candidate.peripherals_shared==0);
|
||||
else assert(candidate.peripherals_shared==1&&candidate.ble_pin==123456);
|
||||
}
|
||||
for(size_t byte=236;byte<244;++byte){
|
||||
DataStore damaged;damaged.fs.files["/new_prefs"]=disk;damaged.fs.files["/new_prefs"][byte]^=1;
|
||||
CompanionNodePrefs candidate;candidate.peripherals_shared=1;candidate.ble_pin=123456;
|
||||
assert(!damaged.loadPrefsInt("/new_prefs",candidate,lat,lon));
|
||||
assert(candidate.peripherals_shared==1&&candidate.ble_pin==123456);
|
||||
// A separately chosen, fully validated image remains usable. This does
|
||||
// not change nRF52's existing primary-file authority/fallback policy.
|
||||
damaged.fs.files["/known_good"]=disk;
|
||||
assert(damaged.loadPrefsInt("/known_good",candidate,lat,lon)&&candidate.peripherals_shared==1);
|
||||
}
|
||||
DataStore interrupted;interrupted.fs.files["/new_prefs"]=disk;
|
||||
interrupted.fs.files["/new_prefs.tmp"]={disk.begin(),disk.begin()+240};
|
||||
CompanionNodePrefs rebooted;assert(interrupted.loadPrefs(rebooted,lat,lon)&&rebooted.peripherals_shared==1);
|
||||
DataStore fail;fail.fs.files["/new_prefs"]=disk;fail.fs.fail_write_after=240;
|
||||
prefs.peripherals_shared=0;assert(!fail.savePrefs(prefs,1,2)&&fail.fs.files["/new_prefs"]==disk);
|
||||
DataStore fresh;CompanionNodePrefs defaults;
|
||||
assert(fresh.loadPrefs(defaults,lat,lon)&&defaults.peripherals_shared==0);
|
||||
}
|
||||
'''
|
||||
compile_run(code, ("NRF52_PLATFORM", "RAK_4631"), serializers=True)
|
||||
|
||||
def test_production_wiring_is_boot_only_and_before_gps_intent(self):
|
||||
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
load = method(common, "void CommonCLI::loadPrefs(FILESYSTEM*")
|
||||
begin = method(companion, "void MyMesh::begin(")
|
||||
self.assertLess(load.index("loadPrefsInt("), load.index("configureSharedPeripheralMode("))
|
||||
self.assertLess(begin.index("_store->loadPrefs("), begin.index("configureSharedPeripheralMode("))
|
||||
self.assertIn("if (!prefs_ready) _prefs.peripherals_shared = 1;", begin)
|
||||
self.assertNotIn("configureSharedPeripheralMode", method(companion, "bool MyMesh::handleLocalControlCommand("))
|
||||
self.assertNotIn("configureSharedPeripheralMode", method(common, "void CommonCLI::handleSetCmd("))
|
||||
for path in ("examples/simple_repeater/MyMesh.cpp", "examples/simple_room_server/MyMesh.cpp",
|
||||
"examples/simple_sensor/SensorMesh.cpp"):
|
||||
source = (ROOT / path).read_text()
|
||||
self.assertLess(source.index("_cli.loadPrefs(_fs)"), source.index("applyGpsPrefs()"))
|
||||
main = (ROOT / "examples/companion_radio/main.cpp").read_text()
|
||||
self.assertLess(main.index("the_mesh.begin("), main.index("the_mesh.applyGpsPrefs()"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute WisBlock GPS/shared-rail ownership without touching hardware.
|
||||
|
||||
The production allocation-free policy is compiled unchanged. GPIO/UART/GNSS
|
||||
integration regressions live alongside these portable boot/hold cases.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from test_gps_upstream_adaptations import ARDUINO, CAYENNE, MESH
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/wisblock_shared_power_policy.cpp"
|
||||
ENVIRONMENT = ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp"
|
||||
|
||||
|
||||
def lifecycle_harness(gps=True, negative=None):
|
||||
source = ENVIRONMENT.read_text(encoding="utf-8")
|
||||
mutations = {
|
||||
"shared_provider_pin": (
|
||||
"_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);",
|
||||
"_location->setPinEn(gps_control_pin);"),
|
||||
"forced_user_on": (
|
||||
"_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);",
|
||||
"_location->setPinEn(shared_power_rail ? -1 : gps_control_pin);\n"
|
||||
" setGpsTelemetryUserEnabled(true);"),
|
||||
"no_wake_settle": ("if (!was_powered) delay(500);", "// Negative control: use UART before cold receiver settles."),
|
||||
"no_pad_release": ("pinMode(PIN_GPS_TX, INPUT);\n pinMode(PIN_GPS_RX, INPUT);",
|
||||
"// Negative control: retain driven serial pads after receiver power-off."),
|
||||
}
|
||||
if negative in mutations:
|
||||
before, after = mutations[negative]
|
||||
assert source.count(before) == 1, "negative-control source anchor changed"
|
||||
source = source.replace(before, after, 1)
|
||||
elif negative == "power_before_uart_end":
|
||||
before = extract_braced(source, "void EnvironmentSensorManager::stop_gps()")
|
||||
after = before.replace("if (serialGPSFlag) stopRakGpsUart();", "// Negative control: release UART too late.")
|
||||
assert after != before, "negative-control UART stop anchor changed"
|
||||
after = after.replace("applyWisBlockRailPower();", "applyWisBlockRailPower();\n if (serialGPSFlag) stopRakGpsUart();", 1)
|
||||
source = source.replace(before, after, 1)
|
||||
elif negative is not None:
|
||||
raise ValueError("unknown lifecycle negative control")
|
||||
fixture_name = "rak_gps_power.cpp" if gps else "rak_peripheral_power_no_gps.cpp"
|
||||
fixture = (ROOT / "test/fixtures" / fixture_name).read_text(encoding="utf-8")
|
||||
helpers = "\n".join(extract_braced(source, signature) for signature in (
|
||||
"static uint8_t rakGpsControlActiveLevel(",
|
||||
"static void setRakGpsControl(",
|
||||
"static void stopRakGpsUart(",
|
||||
"static bool serialHasValidGpsSentence(",
|
||||
"static bool configureRakUbloxReceiver(",
|
||||
)) if gps else ""
|
||||
provider = extract_braced(source, "class RAK12500LocationProvider") + ";\n" if gps else ""
|
||||
signatures = (
|
||||
"bool EnvironmentSensorManager::begin()",
|
||||
"bool EnvironmentSensorManager::querySensors(",
|
||||
"uint8_t EnvironmentSensorManager::getVoltageSensorChannels(",
|
||||
"uint8_t EnvironmentSensorManager::queryVoltageSensors(",
|
||||
"int EnvironmentSensorManager::getNumSettings()",
|
||||
"const char* EnvironmentSensorManager::getSettingName(",
|
||||
"const char* EnvironmentSensorManager::getSettingValue(",
|
||||
"bool EnvironmentSensorManager::setSettingValue(",
|
||||
"void EnvironmentSensorManager::setPowerSavingEnabled(",
|
||||
"bool EnvironmentSensorManager::supportsSharedPeripheralMode()",
|
||||
"void EnvironmentSensorManager::applyWisBlockRailPower()",
|
||||
"uint8_t EnvironmentSensorManager::detectWisBlockStorageHolds()",
|
||||
"void EnvironmentSensorManager::configureSharedPeripheralMode(",
|
||||
"bool EnvironmentSensorManager::isGpsPowered()",
|
||||
"void EnvironmentSensorManager::rakGPSInit()",
|
||||
"bool EnvironmentSensorManager::gpsIsAwake(",
|
||||
"void EnvironmentSensorManager::armGpsPowerSavingCycle()",
|
||||
"void EnvironmentSensorManager::start_gps()",
|
||||
"void EnvironmentSensorManager::stop_gps()",
|
||||
"bool EnvironmentSensorManager::gpsUsesSerialUart(",
|
||||
"bool EnvironmentSensorManager::gpsSerialTransportMayConflict(",
|
||||
"bool EnvironmentSensorManager::gpsSerialTransportCanYield(",
|
||||
"bool EnvironmentSensorManager::setGpsSerialTransportBlocked(",
|
||||
"void EnvironmentSensorManager::loop()",
|
||||
)
|
||||
if not gps:
|
||||
signatures = tuple(signature for signature in signatures if signature not in (
|
||||
"void EnvironmentSensorManager::rakGPSInit()",
|
||||
"bool EnvironmentSensorManager::gpsIsAwake(",
|
||||
"void EnvironmentSensorManager::armGpsPowerSavingCycle()",
|
||||
"void EnvironmentSensorManager::start_gps()",
|
||||
"void EnvironmentSensorManager::stop_gps()",
|
||||
"void EnvironmentSensorManager::loop()",
|
||||
))
|
||||
methods = "\n".join(extract_braced(source, signature) for signature in signatures)
|
||||
return fixture.replace("@HELPERS@", helpers).replace("@PROVIDER@", provider).replace("@METHODS@", methods)
|
||||
|
||||
|
||||
def compile_lifecycle(testcase, cases, defines=(), gps=True, negative=None, auto_store=True):
|
||||
with tempfile.TemporaryDirectory(prefix="rak-gps-lifecycle-") as folder:
|
||||
work = Path(folder)
|
||||
arduino = ARDUINO.replace("#include <deque>", "#include <deque>\n#include <vector>\n#include <string>\n#include <cassert>\n#define INPUT 0")
|
||||
arduino = arduino.replace("inline void pinMode(int,int){}\ninline void digitalWrite(int,int){}\ninline int digitalRead(int){return HIGH;}", r'''
|
||||
struct HardwareEvent { std::string name; int a; int b; uint32_t time; };
|
||||
inline std::vector<HardwareEvent> hardware_events;
|
||||
inline int pin_values[64]={};
|
||||
inline void event(const char* name,int a=0,int b=0){hardware_events.push_back({name,a,b,now_ms});}
|
||||
inline void pinMode(int pin,int mode){event("pinMode",pin,mode);}
|
||||
inline void digitalWrite(int pin,int value){assert(pin>=0&&pin<64);pin_values[pin]=value;event("write",pin,value);}
|
||||
inline int digitalRead(int pin){assert(pin>=0&&pin<64);return pin_values[pin];}
|
||||
''')
|
||||
for name, content in {"Arduino.h": arduino, "Mesh.h": MESH, "CayenneLPP.h": CAYENNE,
|
||||
"Wire.h": "#pragma once\nclass TwoWire {};\ninline TwoWire Wire;\n"}.items():
|
||||
(work / name).write_text(content, encoding="utf-8")
|
||||
cpp, binary = work / "lifecycle.cpp", work / "lifecycle"
|
||||
cpp.write_text(lifecycle_harness(gps=gps, negative=negative), encoding="utf-8")
|
||||
compiled = subprocess.run([
|
||||
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", "-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
*(["-Wno-unused-variable"] if not gps else []),
|
||||
"-fno-pie", "-no-pie", f"-DENV_INCLUDE_GPS={int(gps)}", "-DNRF52_PLATFORM",
|
||||
"-DRAK_BOARD", *(["-DRAK_WISBLOCK_GPS"] if gps else []),
|
||||
*(["-DOTA_RAK_AUTO_STORE"] if auto_store else []),
|
||||
"-DWITH_RS232_BRIDGE_GPS_CONFLICT_UART=1", *defines,
|
||||
"-I" + str(work), "-I" + str(ROOT / "src"), str(cpp),
|
||||
str(ROOT / "src/helpers/SensorManager.cpp"), "-o", str(binary),
|
||||
], text=True, capture_output=True, timeout=60)
|
||||
testcase.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
for case in cases:
|
||||
with testcase.subTest(scenario=case, defines=defines, negative=negative):
|
||||
result = subprocess.run([str(binary), case], text=True, capture_output=True, timeout=20)
|
||||
if negative:
|
||||
testcase.assertNotEqual(result.returncode, 0, "unsafe production variant escaped the regression")
|
||||
testcase.assertIn("Assertion", result.stderr)
|
||||
else:
|
||||
testcase.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
|
||||
|
||||
def compile_policy(testcase, flags=()):
|
||||
with tempfile.TemporaryDirectory(prefix="wisblock-gps-power-") as folder:
|
||||
binary = Path(folder) / "policy"
|
||||
compiled = subprocess.run([
|
||||
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*flags, "-I" + str(ROOT / "src"), str(FIXTURE), "-o", str(binary),
|
||||
], text=True, capture_output=True, timeout=60)
|
||||
testcase.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
|
||||
executed = subprocess.run([str(binary)], text=True, capture_output=True, timeout=20)
|
||||
testcase.assertEqual(executed.returncode, 0, executed.stdout + executed.stderr)
|
||||
|
||||
|
||||
class RakGpsPowerTests(unittest.TestCase):
|
||||
def test_actual_policy_boot_configuration_activity_and_fail_closed_holds(self):
|
||||
compile_policy(self, ("-DEXPECTED_COMPILE_HOLDS=0",))
|
||||
|
||||
def test_actual_compile_time_radio_and_poe_dependencies_are_not_service_toggles(self):
|
||||
variants = [
|
||||
(("-DRAK_4631",), 0),
|
||||
(("-DRAK_3401",), 1),
|
||||
(("-DETHERNET_ENABLED=1",), 2),
|
||||
(("-DETHERNET_ENABLED=0",), 2),
|
||||
(("-DETHERNET_USE_RAK13800=1",), 2),
|
||||
(("-DETHERNET_USE_RAK13800=0",), 2),
|
||||
(("-DRAK4631_COMBINED_ETHERNET=1",), 2),
|
||||
(("-DRAK4631_COMBINED_ETHERNET=0",), 2),
|
||||
(("-DRAK_3401", "-DETHERNET_ENABLED=0"), 3),
|
||||
(("-DETHERNET_ENABLED=0", "-DETHERNET_USE_RAK13800=0",
|
||||
"-DRAK4631_COMBINED_ETHERNET=0"), 2),
|
||||
]
|
||||
for flags, expected in variants:
|
||||
with self.subTest(flags=flags):
|
||||
compile_policy(self, (*flags, f"-DEXPECTED_COMPILE_HOLDS={expected}"))
|
||||
|
||||
def test_actual_rak4631_uart_boot_off_wake_schedule_and_bridge_ownership(self):
|
||||
compile_lifecycle(self, ("uart", "shared_uart", "repeat", "prefs_before", "powered_status", "hourly", "absent", "storage",
|
||||
"storage_ids", "i2c", "shared_i2c", "wake_failure", "initial_config_failure",
|
||||
"wake_output_failure", "wake_rate_failure", "wake_bus_failure"),
|
||||
("-DRAK_4631",))
|
||||
|
||||
def test_actual_radio_and_poe_holds_preserve_the_supply_when_gps_is_off(self):
|
||||
for defines in (("-DRAK_3401",), ("-DRAK_4631", "-DETHERNET_ENABLED=0")):
|
||||
with self.subTest(defines=defines):
|
||||
compile_lifecycle(self, ("held_uart", "held_i2c", "absent"), defines)
|
||||
|
||||
def test_actual_dedicated_gps_control_isolated_from_shared_slot_power(self):
|
||||
for active in (0, 1):
|
||||
with self.subTest(active_level=active):
|
||||
compile_lifecycle(self, ("dedicated",),
|
||||
("-DRAK_4631", "-DPIN_GPS_EN=14", f"-DPIN_GPS_EN_ACTIVE={active}"))
|
||||
|
||||
def test_actual_boot_policy_protects_storage_and_poe_even_when_gps_is_not_compiled(self):
|
||||
for defines in (("-DRAK_4631",), ("-DRAK_3401",), ("-DRAK_4631", "-DETHERNET_ENABLED=0")):
|
||||
with self.subTest(defines=defines):
|
||||
compile_lifecycle(self, ("no_gps",), defines, gps=False)
|
||||
|
||||
def test_independent_negative_controls_reject_forced_gps_and_unsafe_rail_ownership(self):
|
||||
for negative in ("shared_provider_pin", "forced_user_on", "no_wake_settle", "power_before_uart_end", "no_pad_release"):
|
||||
with self.subTest(negative=negative):
|
||||
compile_lifecycle(self, ("uart",), ("-DRAK_4631",), negative=negative)
|
||||
|
||||
def test_fixed_shared_spi_or_sd_storage_is_a_rail_owner_without_auto_probe(self):
|
||||
for store in ("OTA_QSPI_SHARED_WISBLOCK_SPI", "OTA_SD_STORE"):
|
||||
with self.subTest(store=store):
|
||||
compile_lifecycle(self, ("fixed_storage",), ("-DRAK_4631", "-D" + store), auto_store=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -22,7 +22,8 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D SX126X_REGISTER_PATCH=1 ; Patch register 0x8B5 for improved RX with SKY66122 FEM
|
||||
-D FORCE_GPS_ALIVE ; WB_IO2 controls the shared 3V3_S rail; stop_gps() must not drop it.
|
||||
; WB_IO2 also powers the radio frontend: its immutable rail hold stays HIGH.
|
||||
; GPS may stop parsing, but it must never remove this shared radio supply.
|
||||
-D PIN_GPS_EN=-1 ; -1 = no dedicated GPS enable pin (power is the shared 3V3_S rail), same as rak4631
|
||||
-UENV_INCLUDE_BME680
|
||||
-D ENV_INCLUDE_BME680_BSEC=1
|
||||
|
||||
@@ -15,7 +15,8 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-D MOTA_HW_ID='"RAK4631"' ; OTA hardware tag (apply refuses a .mota for different hw)
|
||||
-D PIN_BOARD_SCL=14
|
||||
-D PIN_BOARD_SDA=13
|
||||
-D FORCE_GPS_ALIVE ; WB_IO2 is shared 3V3_S; GPS sleep must not power off other slots
|
||||
; Saved peripherals.mode defaults to gps-only; shared keeps WB_IO2 alive.
|
||||
; The boot power policy automatically protects Ethernet/PoE and external NOR.
|
||||
-D PIN_GPS_EN=-1
|
||||
-D PIN_OLED_RESET=-1
|
||||
-D USE_SX1262
|
||||
|
||||
Reference in new issue
Block a user