mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 14:11:18 +00:00
Control Wio Tracker L2 GPS power and reserve display runtime RAM
This commit is contained in:
1 parent
958bc7a632
commit
aabbaf7eb7
9 files changed
+516
-4
No files matched your search
@@ -14,6 +14,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
@@ -97,6 +98,34 @@ def mqtt_requirements(defines):
|
||||
}
|
||||
|
||||
|
||||
def wio_tracker_l2_display_heap(defines):
|
||||
# Both LGFXDisplay::begin()'s internal canvas and Wio's concrete begin()
|
||||
# must fit. The latter requests PSRAM, but LovyanGFX 1.2.30 explicitly
|
||||
# falls back to internal DMA memory when that allocation fails.
|
||||
zoom_text = str(defines.get("UI_ZOOM", 1)).strip().strip("()")
|
||||
zoom_text = re.sub(r"[fFlL]$", "", zoom_text)
|
||||
try:
|
||||
zoom = float(zoom_text)
|
||||
except ValueError as error:
|
||||
raise ValueError(f"memory budget needs a numeric UI_ZOOM, got {zoom_text!r}") from error
|
||||
if not math.isfinite(zoom) or zoom <= 0:
|
||||
raise ValueError("Wio Tracker L2 UI_ZOOM must be finite and positive")
|
||||
if integer(defines, "UI_COORD_SCALE", 1) == 0:
|
||||
raise ValueError("Wio Tracker L2 UI_COORD_SCALE must be positive")
|
||||
depth = integer(defines, "UI_BUFFER_COLOR_DEPTH", 8)
|
||||
if depth not in (1, 2, 4, 8, 16, 24, 32):
|
||||
raise ValueError("unsupported Wio Tracker L2 UI_BUFFER_COLOR_DEPTH")
|
||||
# The base constructor divides physical dimensions by zoom*coordinate
|
||||
# scale; its render path multiplies by coordinate scale again. Rounding
|
||||
# upward bounds both that canvas and Wio's fixed-8bpp logical sprite.
|
||||
width = math.ceil(320 / zoom)
|
||||
height = math.ceil(240 / zoom)
|
||||
row_bytes = (width * max(8, depth) + 7) // 8
|
||||
# Include the sprite's extra trailing pixel, up to a 256-entry RGB888
|
||||
# palette, alignment and allocation metadata, even with working PSRAM.
|
||||
return row_bytes * height + 1024
|
||||
|
||||
|
||||
def requirements(platform, defines, target):
|
||||
if platform not in SUPPORTED:
|
||||
raise ValueError(f"no memory policy for {platform}")
|
||||
@@ -111,11 +140,22 @@ def requirements(platform, defines, target):
|
||||
depth = integer(defines, "UI_BUFFER_COLOR_DEPTH", 8)
|
||||
pixels = 480 * 480 if "INDICATOR_TRANSPORT_RENDER_PROFILE" in defines else 320 * 320
|
||||
display_heap = (pixels * depth + 7) // 8 + 1024
|
||||
elif display == "WioTrackerL2Display":
|
||||
if platform != "ESP32_PLATFORM":
|
||||
raise ValueError("Wio Tracker L2 display budget requires ESP32")
|
||||
display_heap = wio_tracker_l2_display_heap(defines)
|
||||
elif display in DISPLAY_HEAP:
|
||||
display_heap = DISPLAY_HEAP[display]
|
||||
else:
|
||||
raise ValueError(f"add a runtime allocation budget for display {display!r}")
|
||||
parts = {}
|
||||
if display == "WioTrackerL2Display":
|
||||
# The pinned LGFX QSPI bus has two DMA flip buffers. RGB565 conversion
|
||||
# requests at most 2048 bytes each (2304 for RGB888); shrinking can
|
||||
# briefly retain a third. Reserve 16 KiB for those, queued descriptors
|
||||
# (up to 320 rows, including overlap while growing) and SDK bus/touch
|
||||
# driver metadata. These allocations coexist with the UI sprite.
|
||||
parts["display_dma_and_bus"] = 16384
|
||||
mqtt = None
|
||||
combined_rak_ethernet = "RAK4631_COMBINED_ETHERNET" in defines
|
||||
if combined_rak_ethernet and (platform != "NRF52_PLATFORM"
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <WioTrackerL2LocationProvider.h>
|
||||
#include "GpsTap.h"
|
||||
#include <helpers/SensorManager.h>
|
||||
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define POWERSAVING_DEBUG_PRINTLN(...) ((void)0)
|
||||
|
||||
void LocationProvider::sendSentence(const char*) {}
|
||||
|
||||
@EXPANDER@
|
||||
|
||||
// Keep only ESP32 startup hardware out of this host harness. The expander
|
||||
// initialization, output shadows and GNSS power/reset methods are production.
|
||||
void WioTrackerL2Board::begin() { expander_ok = initExpander(); }
|
||||
uint16_t WioTrackerL2Board::getBattMilliVolts() { return 0; }
|
||||
bool WioTrackerL2Board::isExternalPowered() { return false; }
|
||||
void WioTrackerL2Board::setLed(bool) {}
|
||||
|
||||
class EnvironmentSensorManager : public SensorManager {
|
||||
public:
|
||||
LocationProvider* _location;
|
||||
bool gps_detected=false, gps_active=false, gps_serial_transport=false;
|
||||
bool gps_serial_transport_blocked=false, gps_discovery_pending=false;
|
||||
bool gps_discovery_preference_known=false;
|
||||
uint32_t gps_discovery_started_at=0;
|
||||
explicit EnvironmentSensorManager(LocationProvider& location) : _location(&location) {}
|
||||
LocationProvider* getLocationProvider() override { return _location; }
|
||||
bool telemetryGpsDetected() const override { return gps_detected; }
|
||||
bool telemetryGpsActive() const override { return gps_active; }
|
||||
void telemetryGpsStart() override { start_gps(); }
|
||||
void telemetryGpsStop() override { stop_gps(); }
|
||||
bool setSettingValue(const char*,const char*) override;
|
||||
void setPowerSavingEnabled(bool) override;
|
||||
void finishBasicGpsDiscovery(bool found);
|
||||
void serviceBasicGpsDiscovery();
|
||||
void initBasicGPS();
|
||||
void armGpsPowerSavingCycle();
|
||||
void start_gps();
|
||||
void stop_gps();
|
||||
void loop() override;
|
||||
};
|
||||
|
||||
@LIFECYCLE@
|
||||
|
||||
WioTrackerL2Board board;
|
||||
GpsTapStream gps_tap(Serial1);
|
||||
mesh::RTCClock clock_source;
|
||||
WioTrackerL2LocationProvider gps(board, gps_tap, &clock_source);
|
||||
EnvironmentSensorManager sensors(gps);
|
||||
struct RadioMock { void powerOff() { event("radio-off"); } } radio_driver;
|
||||
|
||||
@SHUTDOWN@
|
||||
|
||||
size_t first(const char* name) {
|
||||
for (size_t i=0;i<events.size();++i) if(events[i].name==name)return i;
|
||||
return events.size();
|
||||
}
|
||||
void assertOff() {
|
||||
assert(!Wire.gnssOn()&&!gps.isEnabled()&&!Serial1.installed);
|
||||
assert(modes[PIN_GPS_TX]==INPUT&&modes[PIN_GPS_RX]==INPUT);
|
||||
}
|
||||
void start() {
|
||||
// Match EnvironmentSensorManager's UART setup before provider begin().
|
||||
Serial1.setPins(PIN_GPS_TX,PIN_GPS_RX);Serial1.begin(9600);
|
||||
events.clear();gps.begin();
|
||||
assert(gps.isEnabled()&&Wire.gnssOn()&&Serial1.installed);
|
||||
assert(first("uart-end")<first("gnss-on"));
|
||||
assert(first("gnss-on")<first("uart-begin"));
|
||||
}
|
||||
void stop() {
|
||||
events.clear();gps.stop();assertOff();
|
||||
assert(first("uart-end")<first("gnss-off"));
|
||||
}
|
||||
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2&&events.empty()&&Wire.transactions==0);
|
||||
const std::string mode=argv[1];
|
||||
if(mode=="failed") {
|
||||
Wire.present=false;board.begin();assert(!board.expanderOK());
|
||||
Serial1.setPins(PIN_GPS_TX,PIN_GPS_RX);Serial1.begin(9600);
|
||||
unsigned probes=Wire.transactions;gps.begin();
|
||||
assertOff();assert(Wire.transactions==probes);
|
||||
gps.stop();assertOff();assert(Wire.transactions==probes);
|
||||
return 0;
|
||||
}
|
||||
board.begin();
|
||||
assert(board.expanderOK()&&!Wire.gnssOn()&&!gps.isEnabled());
|
||||
if(mode=="boot")return 0;
|
||||
if(mode=="cycles") {
|
||||
for(unsigned cycle=0;cycle<4;++cycle) {
|
||||
start();assert(!gps.isValid());
|
||||
unsigned probes=Wire.transactions,begins=Serial1.begins,ends=Serial1.ends;
|
||||
gps.begin();assert(Wire.transactions==probes);
|
||||
assert(Serial1.begins==begins&&Serial1.ends==ends);
|
||||
stop();probes=Wire.transactions;ends=Serial1.ends;
|
||||
gps.stop();assert(Wire.transactions==probes&&Serial1.ends==ends);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if(mode=="manager") {
|
||||
sensors.initBasicGPS();
|
||||
assert(sensors.gps_detected&&!sensors.gps_active);assertOff();
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
assert(sensors.gps_active&&gps.isEnabled()&&Wire.gnssOn());
|
||||
assert(sensors.setSettingValue("gps","0"));assertOff();
|
||||
sensors.setPowerSavingEnabled(true);
|
||||
assert(sensors.setSettingValue("gps","1"));
|
||||
assert(sensors.gps_active&&gps.getGPSPowerSaving()&&Wire.gnssOn());
|
||||
gps.setNextGPSOff(millis()+1);gps.stopTimeSync();now_ms+=2;
|
||||
sensors.loop();assert(!sensors.gps_active);assertOff();
|
||||
uint32_t wake=gps.getNextGPSOn();assert(wake!=0);
|
||||
now_ms=wake;sensors.loop();
|
||||
assert(sensors.gps_active&&gps.isEnabled()&&Wire.gnssOn());
|
||||
assert(sensors.setSettingValue("gps","0"));assertOff();
|
||||
return 0;
|
||||
}
|
||||
assert(mode=="tapshutdown");
|
||||
start();
|
||||
Serial1.feed("$GPGGA,123456,4760.000,N,12200.000,W,1,08,1.0,1.0,M,0.0,M,,*00\r\n");
|
||||
gps.loop();assert(gps.isValid());
|
||||
assert(gps_tap.ggaFix()==1&&gps_tap.ggaUsed()==8);
|
||||
events.clear();board.shutdownPeripherals();assertOff();
|
||||
assert(first("radio-off")<first("gnss-off"));
|
||||
assert(first("uart-end")<first("gnss-off"));
|
||||
start();assert(!gps.isValid());stop();
|
||||
}
|
||||
@@ -121,6 +121,58 @@ class FirmwareRamTest(unittest.TestCase):
|
||||
}, "muzi_base_uno_superIO_companion_radio_ble")
|
||||
self.assertEqual(policy["components"]["display_pixels_and_driver"], 4096)
|
||||
|
||||
def test_wio_l2_counts_internal_sprite_fallback_and_dma_beside_psram(self):
|
||||
flags = {"DISPLAY_CLASS": "WioTrackerL2Display", "UI_ZOOM": 2}
|
||||
for target in ("Wio_Tracker_L2_repeater", "Wio_Tracker_L2_companion_radio_ble"):
|
||||
with self.subTest(target=target):
|
||||
policy = ram.requirements("ESP32_PLATFORM", flags, target)
|
||||
self.assertEqual(policy["components"]["display_pixels_and_driver"],
|
||||
160 * 120 + 1024)
|
||||
self.assertEqual(policy["components"]["display_dma_and_bus"], 16384)
|
||||
self.assertGreaterEqual(policy["required_contiguous_bytes"], 160 * 120 + 1024)
|
||||
psram = ram.requirements("ESP32_PLATFORM", {**flags, "BOARD_HAS_PSRAM": 1}, target)
|
||||
self.assertEqual(psram, policy)
|
||||
without = ram.requirements("ESP32_PLATFORM", {"UI_ZOOM": 2}, target)
|
||||
display_components = (policy["components"]["display_pixels_and_driver"]
|
||||
+ policy["components"]["display_dma_and_bus"]
|
||||
+ policy["components"]["screen_objects_and_history"])
|
||||
self.assertEqual(policy["required_heap_bytes"] - without["required_heap_bytes"],
|
||||
display_components)
|
||||
|
||||
def test_wio_l2_zoom_and_base_color_depth_cannot_erase_actual_sprite_cost(self):
|
||||
flags = {"DISPLAY_CLASS": "WioTrackerL2Display"}
|
||||
cases = (({}, 320 * 240), ({"UI_ZOOM": 2, "UI_BUFFER_COLOR_DEPTH": 4}, 160 * 120),
|
||||
({"UI_ZOOM": 2, "UI_COORD_SCALE": 2}, 160 * 120),
|
||||
({"UI_ZOOM": 2, "UI_BUFFER_COLOR_DEPTH": 16}, 160 * 120 * 2),
|
||||
({"UI_ZOOM": "1.5f", "UI_BUFFER_COLOR_DEPTH": 24}, 214 * 160 * 3),
|
||||
({"UI_ZOOM": "0.5", "UI_BUFFER_COLOR_DEPTH": 32}, 640 * 480 * 4))
|
||||
for overrides, pixels in cases:
|
||||
with self.subTest(overrides=overrides):
|
||||
policy = ram.requirements("ESP32_PLATFORM", {**flags, **overrides}, "Wio_Tracker_L2_repeater")
|
||||
self.assertEqual(policy["components"]["display_pixels_and_driver"], pixels + 1024)
|
||||
self.assertGreaterEqual(policy["required_contiguous_bytes"], pixels + 1024)
|
||||
for override in ({"UI_ZOOM": 0}, {"UI_ZOOM": -1}, {"UI_ZOOM": "inf"},
|
||||
{"UI_ZOOM": "nan"}, {"UI_ZOOM": "auto"},
|
||||
{"UI_COORD_SCALE": 0}, {"UI_BUFFER_COLOR_DEPTH": 7}):
|
||||
with self.subTest(invalid=override), self.assertRaises(ValueError):
|
||||
ram.requirements("ESP32_PLATFORM", {**flags, **override}, "Wio_Tracker_L2_repeater")
|
||||
with self.assertRaisesRegex(ValueError, "requires ESP32"):
|
||||
ram.requirements("NRF52_PLATFORM", flags, "unsupported_display")
|
||||
|
||||
def test_wio_l2_budget_tracks_real_sprite_and_qspi_geometry(self):
|
||||
display = (ROOT / "variants/wio-tracker-l2/WioTrackerL2Display.h").read_text()
|
||||
base = (ROOT / "src/helpers/ui/LGFXDisplay.cpp").read_text()
|
||||
self.assertIn("WioTrackerL2Display() : LGFXDisplay(320, 240, disp)", display)
|
||||
self.assertIn("cfg.panel_width = 240;", display)
|
||||
self.assertIn("cfg.panel_height = 320;", display)
|
||||
self.assertIn("buffer.setColorDepth(8);", display)
|
||||
self.assertIn("buffer.setPsram(true);", display)
|
||||
self.assertIn("buffer.createSprite(width(), height());", display)
|
||||
self.assertIn("lgfx::Bus_SPI _bus_instance;", display)
|
||||
self.assertIn("buffer.setPsram(false);", base)
|
||||
self.assertIn("buffer.setColorDepth(UI_BUFFER_COLOR_DEPTH);", base)
|
||||
self.assertIn("buffer.createSprite(width() * _coordinateScale,", base)
|
||||
|
||||
def test_heap_tables_and_ota_remain_in_runtime_budget(self):
|
||||
policy = ram.requirements("ESP32_PLATFORM", {
|
||||
"ENABLE_OTA": 1, "OTA_HEAP_CONTEXT": 1,
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Host-test Wio L2's real GNSS provider, expander operations and GPS lifecycle.
|
||||
|
||||
Only the UART/I2C/ESP32 hardware boundaries and NMEA library are mocked. No
|
||||
physical power/current claims are made by these tests.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_gps_upstream_adaptations import CAYENNE, MESH, NMEA, RTC
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
VARIANT = ROOT / "variants/wio-tracker-l2"
|
||||
|
||||
ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#define HIGH 1
|
||||
#define LOW 0
|
||||
#define OUTPUT 1
|
||||
#define INPUT 0
|
||||
struct Event { std::string name; int a; int b; };
|
||||
inline std::vector<Event> events;
|
||||
inline int modes[64] = {};
|
||||
inline uint32_t now_ms = 1;
|
||||
inline void event(const char* name, int a=0, int b=0) { events.push_back({name,a,b}); }
|
||||
inline uint32_t millis() { return now_ms; }
|
||||
inline void delay(uint32_t ms) { now_ms += ms; }
|
||||
inline void yield() { ++now_ms; }
|
||||
inline void pinMode(int pin,int mode) { assert(pin>=0&&pin<64);modes[pin]=mode;event("pin",pin,mode); }
|
||||
inline void digitalWrite(int,int) {}
|
||||
inline int digitalRead(int) { return HIGH; }
|
||||
class Stream {
|
||||
public:
|
||||
virtual ~Stream() = default;
|
||||
virtual int available() { return 0; }
|
||||
virtual int peek() { return -1; }
|
||||
virtual int read() { return -1; }
|
||||
virtual void flush() {}
|
||||
virtual size_t write(uint8_t) { return 1; }
|
||||
virtual size_t write(const uint8_t*,size_t size) { return size; }
|
||||
};
|
||||
class HardwareSerial : public Stream {
|
||||
public:
|
||||
bool installed=false;
|
||||
unsigned begins=0, ends=0;
|
||||
int rx=-1,tx=-1;
|
||||
std::deque<char> incoming;
|
||||
void setPins(int r,int t) { rx=r;tx=t;event("uart-pins",r,t); }
|
||||
void begin(unsigned baud) {
|
||||
assert(rx>=0&&tx>=0);installed=true;++begins;
|
||||
pinMode(rx,INPUT);pinMode(tx,OUTPUT);event("uart-begin",baud);
|
||||
}
|
||||
void end() { installed=false;++ends;incoming.clear();event("uart-end"); }
|
||||
int available() override { return installed ? int(incoming.size()) : 0; }
|
||||
int peek() override { return available() ? incoming.front() : -1; }
|
||||
int read() override { if(!available())return -1;int c=incoming.front();incoming.pop_front();return c; }
|
||||
void feed(const char* text) { while(*text)incoming.push_back(*text++); }
|
||||
};
|
||||
inline HardwareSerial Serial1, Serial;
|
||||
using std::isnan;
|
||||
'''
|
||||
|
||||
WIRE = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
class TwoWire {
|
||||
std::vector<uint8_t> sent;
|
||||
uint8_t address=0;
|
||||
public:
|
||||
bool present=true;
|
||||
unsigned transactions=0;
|
||||
uint8_t regs[256];
|
||||
TwoWire() { memset(regs,0xff,sizeof(regs)); }
|
||||
bool gnssOn() const { return !(regs[7]&(1<<5)) && (regs[3]&(1<<5)); }
|
||||
void beginTransmission(uint8_t a) { address=a;sent.clear();++transactions; }
|
||||
size_t write(uint8_t byte) { sent.push_back(byte);return 1; }
|
||||
uint8_t endTransmission() {
|
||||
if(!present)return 4;
|
||||
if(address==0x21&&sent.size()==2) {
|
||||
const bool before=gnssOn();regs[sent[0]]=sent[1];
|
||||
if(before!=gnssOn()) {
|
||||
// Both enabling and disabling the receiver happen with UART pads
|
||||
// released, never an idle-high MCU TX into an unpowered GNSS RX.
|
||||
assert(!Serial1.installed&&modes[17]==INPUT&&modes[18]==INPUT);
|
||||
event(gnssOn()?"gnss-on":"gnss-off");
|
||||
}
|
||||
event("i2c-write",sent[0],sent[1]);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int requestFrom(int,int size) { return size; }
|
||||
int read() { return 0; }
|
||||
};
|
||||
inline TwoWire Wire;
|
||||
'''
|
||||
|
||||
ESP_BOARD = r'''
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
class ESP32Board {
|
||||
public:
|
||||
virtual void begin() {}
|
||||
virtual uint16_t getBattMilliVolts() { return 0; }
|
||||
virtual const char* getManufacturerName() const { return "host"; }
|
||||
virtual void onBeforeTransmit() {}
|
||||
virtual void onAfterTransmit() {}
|
||||
virtual bool isExternalPowered() { return false; }
|
||||
virtual void shutdownPeripherals();
|
||||
};
|
||||
'''
|
||||
|
||||
|
||||
def harness(mutation=None):
|
||||
board = (VARIANT / "WioTrackerL2Board.cpp").read_text()
|
||||
if mutation == "boot-rail-on":
|
||||
board = board.replace("expSetOutput(EXP_PIN_GNSS_EN, LOW);",
|
||||
"expSetOutput(EXP_PIN_GNSS_EN, HIGH);", 1)
|
||||
constants = "\n".join(line for line in board.splitlines()
|
||||
if re.match(r"static const uint8_t TCA_REG_", line))
|
||||
expander = "\n".join(extract_braced(board, signature) for signature in (
|
||||
"bool WioTrackerL2Board::expWriteReg(",
|
||||
"void WioTrackerL2Board::expSetOutput(",
|
||||
"void WioTrackerL2Board::expSetInput(",
|
||||
"void WioTrackerL2Board::expWritePin(",
|
||||
"bool WioTrackerL2Board::initExpander()",
|
||||
"int WioTrackerL2Board::expReadInputs()",
|
||||
))
|
||||
manager = (ROOT / "src/helpers/sensors/EnvironmentSensorManager.cpp").read_text()
|
||||
lifecycle = "\n".join(extract_braced(manager, signature) for signature in (
|
||||
"bool EnvironmentSensorManager::setSettingValue(",
|
||||
"void EnvironmentSensorManager::setPowerSavingEnabled(",
|
||||
"void EnvironmentSensorManager::finishBasicGpsDiscovery(",
|
||||
"void EnvironmentSensorManager::serviceBasicGpsDiscovery()",
|
||||
"void EnvironmentSensorManager::initBasicGPS()",
|
||||
"void EnvironmentSensorManager::armGpsPowerSavingCycle()",
|
||||
"void EnvironmentSensorManager::start_gps()",
|
||||
"void EnvironmentSensorManager::stop_gps()",
|
||||
"void EnvironmentSensorManager::loop()",
|
||||
))
|
||||
shutdown = extract_braced((ROOT / "src/helpers/ESP32Board.cpp").read_text(),
|
||||
"void ESP32Board::shutdownPeripherals()")
|
||||
fixture = (ROOT / "test/fixtures/wio_tracker_l2_gps_power.cpp").read_text()
|
||||
return (fixture.replace("@EXPANDER@", constants + "\n" + expander)
|
||||
.replace("@LIFECYCLE@", lifecycle).replace("@SHUTDOWN@", shutdown))
|
||||
|
||||
|
||||
class WioTrackerL2GpsPowerTests(unittest.TestCase):
|
||||
def compile_run(self, mode, mutation=None, fail=False):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++17 compiler is required")
|
||||
with tempfile.TemporaryDirectory(prefix="wio-l2-gps-power-") as directory:
|
||||
work = Path(directory)
|
||||
files = {
|
||||
"Arduino.h": ARDUINO, "Wire.h": WIRE, "Mesh.h": MESH,
|
||||
"MicroNMEA.h": NMEA, "RTClib.h": RTC, "CayenneLPP.h": CAYENNE,
|
||||
"helpers/ESP32Board.h": ESP_BOARD,
|
||||
}
|
||||
provider = (VARIANT / "WioTrackerL2LocationProvider.h").read_text()
|
||||
if mutation == "never-power-off":
|
||||
provider = provider.replace("_board.setGnssPower(false);", "// negative control")
|
||||
# Copy the production adaptation unchanged except explicit negative
|
||||
# controls; quoted includes still resolve the real board header.
|
||||
files["WioTrackerL2LocationProvider.h"] = provider
|
||||
for name, contents in files.items():
|
||||
path = work / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(contents)
|
||||
source = work / "case.cpp"
|
||||
source.write_text(harness(mutation))
|
||||
binary = work / "case"
|
||||
command = [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", "-DENV_INCLUDE_GPS=1",
|
||||
"-DESP32_PLATFORM=1", "-DENV_SKIP_GPS_DETECT=1",
|
||||
"-DPIN_GPS_TX=18", "-DPIN_GPS_RX=17", "-DGPS_BAUD_RATE=9600",
|
||||
"-I", str(work), "-I", str(VARIANT),
|
||||
"-I", str(ROOT / "src"), str(source),
|
||||
str(ROOT / "src/helpers/SensorManager.cpp"), "-o", str(binary)]
|
||||
if sys.platform.startswith("linux"):
|
||||
command[1:1] = ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
|
||||
"-fno-pie", "-no-pie"]
|
||||
built = subprocess.run(command, text=True, capture_output=True)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
ran = subprocess.run([str(binary), mode], text=True, capture_output=True)
|
||||
if fail:
|
||||
self.assertNotEqual(ran.returncode, 0, "negative control did not fail")
|
||||
else:
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
def test_boot_gnss_is_off_without_global_constructor_io(self):
|
||||
self.compile_run("boot")
|
||||
|
||||
def test_begin_stop_are_idempotent_and_order_uart_before_rail(self):
|
||||
self.compile_run("cycles")
|
||||
|
||||
def test_failed_expander_leaves_uart_released_and_provider_disabled(self):
|
||||
self.compile_run("failed")
|
||||
|
||||
def test_actual_manager_saved_gps_off_and_scheduled_sleep_wake(self):
|
||||
self.compile_run("manager")
|
||||
|
||||
def test_gps_tap_parser_and_actual_esp32_shutdown_are_preserved(self):
|
||||
self.compile_run("tapshutdown")
|
||||
|
||||
def test_negative_controls_detect_boot_on_and_missing_rail_stop(self):
|
||||
self.compile_run("boot", "boot-rail-on", fail=True)
|
||||
self.compile_run("cycles", "never-power-off", fail=True)
|
||||
|
||||
def test_target_uses_board_provider_without_fake_enable_gpio(self):
|
||||
target = (VARIANT / "target.cpp").read_text()
|
||||
self.assertIn("GpsTapStream gps_tap(Serial1);", target)
|
||||
self.assertIn("WioTrackerL2LocationProvider gps(board, gps_tap, &rtc_clock);", target)
|
||||
self.assertIn("EnvironmentSensorManager sensors(gps);", target)
|
||||
provider = (VARIANT / "WioTrackerL2LocationProvider.h").read_text()
|
||||
self.assertIn("MicroNMEALocationProvider(stream, clock, -1, -1)", provider)
|
||||
self.assertNotIn("getPinEn()", provider)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -55,6 +55,9 @@ PIN shown on the device screen.
|
||||
- **GNSS**: GPS+BeiDou+GLONASS are enabled at start (`$PCAS04,7`). A watchdog
|
||||
resets the module if it tracks 4+ strong satellites for minutes without
|
||||
producing a fix. Raw NMEA can be logged to SD for diagnosis.
|
||||
The GNSS rail starts off. The board-local provider powers it only while
|
||||
GPS is enabled, including scheduled wake periods, and releases the UART
|
||||
pads before cutting power so they cannot back-power the receiver.
|
||||
- **Power**: CPU drops to 80 MHz while the screen sleeps; the speaker amp and
|
||||
I2S clocks run only while a tone plays; the GNSS and Grove rails are gated;
|
||||
idle Bluetooth switches to slow advertising.
|
||||
|
||||
@@ -66,9 +66,12 @@ bool WioTrackerL2Board::initExpander() {
|
||||
delay(10);
|
||||
expSetOutput(EXP_PIN_BAT_ADC_EN, HIGH);
|
||||
delay(10);
|
||||
expSetOutput(EXP_PIN_GNSS_EN, HIGH);
|
||||
// The location provider owns this rail, including saved GPS-off settings.
|
||||
// Keep it off until its explicit power/reset sequence starts acquisition.
|
||||
expSetOutput(EXP_PIN_GNSS_EN, LOW);
|
||||
delay(10);
|
||||
// GNSS reset is active HIGH; hold 10ms then release LOW so the L76K runs
|
||||
// Configure active-HIGH reset while unpowered. The provider pulses it again
|
||||
// after enabling the GNSS rail, before it starts receiving NMEA.
|
||||
expSetOutput(EXP_PIN_GNSS_RST, HIGH);
|
||||
delay(10);
|
||||
expWritePin(EXP_PIN_GNSS_RST, LOW);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "WioTrackerL2Board.h"
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
|
||||
// The GNSS enable/reset pins are TCA9535 channels, not ESP32 GPIOs. Keep this
|
||||
// adaptation local to the board while retaining the normal NMEA/GpsTap stream.
|
||||
class WioTrackerL2LocationProvider : public MicroNMEALocationProvider {
|
||||
WioTrackerL2Board& _board;
|
||||
bool _powered = false;
|
||||
|
||||
static void releaseUart() {
|
||||
Serial1.end();
|
||||
// The MCU's TX idle level must not feed an unpowered GNSS through its RX.
|
||||
pinMode(PIN_GPS_TX, INPUT);
|
||||
pinMode(PIN_GPS_RX, INPUT);
|
||||
}
|
||||
|
||||
public:
|
||||
WioTrackerL2LocationProvider(WioTrackerL2Board& board, Stream& stream,
|
||||
mesh::RTCClock* clock)
|
||||
: MicroNMEALocationProvider(stream, clock, -1, -1), _board(board) {}
|
||||
|
||||
void begin() override {
|
||||
if (_powered) return;
|
||||
// The manager starts Serial1 before provider begin(). Release its pads
|
||||
// until the board-specific power/reset sequence has made GNSS ready.
|
||||
releaseUart();
|
||||
if (!_board.expanderOK()) return;
|
||||
_board.setGnssPower(true);
|
||||
Serial1.setPins(PIN_GPS_TX, PIN_GPS_RX);
|
||||
#ifdef GPS_BAUD_RATE
|
||||
Serial1.begin(GPS_BAUD_RATE);
|
||||
#else
|
||||
Serial1.begin(9600);
|
||||
#endif
|
||||
MicroNMEALocationProvider::begin();
|
||||
clearPositionFix();
|
||||
_powered = true;
|
||||
}
|
||||
|
||||
void stop() override {
|
||||
if (!_powered) return;
|
||||
MicroNMEALocationProvider::stop();
|
||||
releaseUart();
|
||||
_board.setGnssPower(false);
|
||||
_powered = false;
|
||||
}
|
||||
|
||||
bool isEnabled() override { return _powered; }
|
||||
};
|
||||
@@ -11,7 +11,7 @@ WRAPPER_CLASS radio_driver(radio, board);
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
GpsTapStream gps_tap(Serial1); // GSV sats-in-view / SNR watcher
|
||||
MicroNMEALocationProvider gps(gps_tap, &rtc_clock);
|
||||
WioTrackerL2LocationProvider gps(board, gps_tap, &rtc_clock);
|
||||
EnvironmentSensorManager sensors(gps);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
#include "WioTrackerL2LocationProvider.h"
|
||||
#include "GpsTap.h"
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include "WioTrackerL2Display.h"
|
||||
|
||||
Reference in new issue
Block a user