Fix WisBlock GPS power policy and preserve shared peripherals

This commit is contained in:
mikecarper committed 2026-10-13 09:55:32 -07:00
1 parent 9079d6f47e
commit 368770656f
26 files changed
+1830 -107

No files matched your search

+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+24
View File
@@ -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)
+25
View File
@@ -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];
+17 -3
View File
@@ -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.
+50 -1
View File
@@ -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) {
+4
View File
@@ -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 {
+103
View File
@@ -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
+7
View File
@@ -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
+159 -74
View File
@@ -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
+444
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
+7 -1
View File
@@ -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
+2 -1
View File
@@ -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)
+2 -1
View File
@@ -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)
+10
View File
@@ -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"));
+32 -15
View File
@@ -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;"
)
+302
View File
@@ -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()
+204
View File
@@ -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()
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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