Reserve configured NOR pins from user GPIO control
This commit is contained in:
1 parent
9e4891989f
commit
4865162fb4
4 files changed
+211
No files matched your search
@@ -754,6 +754,7 @@ jobs:
|
||||
python3 -B test/test_ethernet_terminal_shutdown.py -v
|
||||
python3 -B test/test_rak_sd_receiver.py -v
|
||||
python3 -B test/test_rak15002_shared_spi.py -v
|
||||
python3 -B test/test_rak_nor_gpio_reservations.py -v
|
||||
python3 -B test/test_rak_companion_ethernet_worker.py -v
|
||||
python3 -B test/test_rak_sd_context_handoff.py -v
|
||||
python3 -B test/test_rak_sd_room_ethernet.py -v
|
||||
|
||||
@@ -540,6 +540,29 @@ inline bool isFirmwareReserved(uint8_t pin) {
|
||||
#ifdef SDCARD_CS
|
||||
SDCARD_CS,
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
|
||||
// The raw NOR backend owns these configured pads even while its bus is
|
||||
// released or no matching chip was found. User GPIO/timers must not
|
||||
// defeat a later OTA storage claim or drive an installed flash's CS.
|
||||
#ifdef OTA_QSPI_SCK_ARDUINO_PIN
|
||||
OTA_QSPI_SCK_ARDUINO_PIN,
|
||||
#endif
|
||||
#ifdef OTA_QSPI_CS_ARDUINO_PIN
|
||||
OTA_QSPI_CS_ARDUINO_PIN,
|
||||
#endif
|
||||
#ifdef OTA_QSPI_IO0_ARDUINO_PIN
|
||||
OTA_QSPI_IO0_ARDUINO_PIN,
|
||||
#endif
|
||||
#ifdef OTA_QSPI_IO1_ARDUINO_PIN
|
||||
OTA_QSPI_IO1_ARDUINO_PIN,
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO2_ARDUINO_PIN) && !defined(OTA_QSPI_IO2_NOT_CONNECTED)
|
||||
OTA_QSPI_IO2_ARDUINO_PIN,
|
||||
#endif
|
||||
#if defined(OTA_QSPI_IO3_ARDUINO_PIN) && !defined(OTA_QSPI_IO3_NOT_CONNECTED)
|
||||
OTA_QSPI_IO3_ARDUINO_PIN,
|
||||
#endif
|
||||
#endif
|
||||
#ifdef SENSOR_POWER_CTRL_PIN
|
||||
SENSOR_POWER_CTRL_PIN,
|
||||
#endif
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#include <Arduino.h>
|
||||
#include <variant.h>
|
||||
#include <helpers/UserGpio.h>
|
||||
#include <UserGpioPinPolicy.h>
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
|
||||
// Actual RAK4631 variant.cpp maps Arduino pins 0..47 directly to P0/P1.
|
||||
static uint8_t digitalPinToPinName(uint8_t pin) { return pin; }
|
||||
class NRF52Board : public mesh::MainBoard {
|
||||
public:
|
||||
uint16_t getBattMilliVolts() override { return 0; }
|
||||
const char* getManufacturerName() const override { return "RAK test"; }
|
||||
void reboot() override {}
|
||||
uint8_t getStartupReason() const override { return BD_STARTUP_NORMAL; }
|
||||
bool isUserGpioAvailable(uint8_t pin) const override;
|
||||
};
|
||||
@EXPOSED@
|
||||
@APPROVAL@
|
||||
|
||||
static unsigned scenarios = 0;
|
||||
static void refused(NRF52Board& board, UserGpio& gpio, uint8_t pin) {
|
||||
assert(!board.isUserGpioAvailable(pin));
|
||||
for (const char* action : {"on", "off", "reset", "on 30 off", "off 1 reset"}) {
|
||||
uint8_t levels[64], modes[64];
|
||||
memcpy(levels, g_mock_pin_levels, sizeof(levels));
|
||||
memcpy(modes, g_mock_pin_modes, sizeof(modes));
|
||||
const unsigned writes = gpio_writes, directions = gpio_modes;
|
||||
char args[64], reply[160] = {};
|
||||
snprintf(args, sizeof(args), "%u %s", unsigned(pin), action);
|
||||
assert(gpio.handleSet(args, reply, sizeof(reply)).outcome == UserGpio::SetResult::ERROR);
|
||||
assert(!strncmp(reply, "Error:", 6));
|
||||
assert(gpio_writes == writes && gpio_modes == directions);
|
||||
assert(!memcmp(levels, g_mock_pin_levels, sizeof(levels)));
|
||||
assert(!memcmp(modes, g_mock_pin_modes, sizeof(modes)));
|
||||
assert(!gpio.hasActiveTimer());
|
||||
UserGpio::Completion completion;
|
||||
assert(!gpio.takeCompletion(completion));
|
||||
++scenarios;
|
||||
}
|
||||
}
|
||||
|
||||
static void available(NRF52Board& board, UserGpio& gpio, uint8_t pin) {
|
||||
assert(board.isUserGpioAvailable(pin));
|
||||
char args[64], reply[160] = {};
|
||||
for (const char* action : {"on", "off", "reset"}) {
|
||||
snprintf(args, sizeof(args), "%u %s", unsigned(pin), action);
|
||||
assert(gpio.handleSet(args, reply, sizeof(reply)).outcome == UserGpio::SetResult::APPLIED);
|
||||
assert(!strncmp(reply, "OK", 2));
|
||||
assert(g_mock_pin_modes[pin] == (!strcmp(action, "reset") ? INPUT : OUTPUT));
|
||||
if (strcmp(action, "reset"))
|
||||
assert(g_mock_pin_levels[pin] == (!strcmp(action, "on") ? HIGH : LOW));
|
||||
++scenarios;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
NRF52Board board;
|
||||
UserGpio gpio(board);
|
||||
// Shared supply and existing SD bus claims remain protected independently
|
||||
// of the new NOR policy. Refusal must perform no pin writes or timer work.
|
||||
for (uint8_t pin : {WB_IO2, WB_IO4, WB_IO5}) refused(board, gpio, pin);
|
||||
#if defined(OTA_RAK15002_SD_STORE)
|
||||
for (uint8_t pin : {3, 10, 26, 29, 30}) refused(board, gpio, pin);
|
||||
#endif
|
||||
#if defined(TEST_ARBITRARY)
|
||||
for (uint8_t pin : {OTA_QSPI_SCK_ARDUINO_PIN, OTA_QSPI_CS_ARDUINO_PIN,
|
||||
OTA_QSPI_IO0_ARDUINO_PIN, OTA_QSPI_IO1_ARDUINO_PIN,
|
||||
OTA_QSPI_IO2_ARDUINO_PIN, OTA_QSPI_IO3_ARDUINO_PIN}) {
|
||||
#if defined(TEST_DISCONNECTED_OVERRIDES)
|
||||
if (pin == OTA_QSPI_IO2_ARDUINO_PIN || pin == OTA_QSPI_IO3_ARDUINO_PIN) {
|
||||
available(board, gpio, pin);
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
#if TEST_EXPECT_NOR
|
||||
refused(board, gpio, pin);
|
||||
#else
|
||||
available(board, gpio, pin);
|
||||
#endif
|
||||
}
|
||||
// No blanket GPIO31 ban: this override does not use it as a NOR pad.
|
||||
available(board, gpio, 31);
|
||||
#elif TEST_EXPECT_NOR
|
||||
digitalWrite(31, HIGH); pinMode(31, OUTPUT); // Live NOR initially deselected.
|
||||
refused(board, gpio, 31);
|
||||
assert(g_mock_pin_levels[31] == HIGH && g_mock_pin_modes[31] == OUTPUT);
|
||||
#else
|
||||
// The same valid board GPIO remains usable when no NOR backend is compiled.
|
||||
available(board, gpio, 31);
|
||||
#endif
|
||||
available(board, gpio, 33); // Unrelated exposed GPIO is never removed.
|
||||
printf("%u RAK NOR GPIO ownership scenarios passed\n", scenarios);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Run actual RAK board GPIO approval and commands with NOR pin ownership."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / 'test/fixtures/rak_nor_gpio_reservations.cpp'
|
||||
POLICY = ROOT / 'src/helpers/UserGpioPinPolicy.h'
|
||||
ARDUINO = r'''
|
||||
#pragma once
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
constexpr uint8_t LOW=0,HIGH=1,INPUT=0,OUTPUT=1,INPUT_PULLUP=2,INPUT_PULLDOWN=3;
|
||||
inline uint8_t g_mock_pin_modes[64]={},g_mock_pin_levels[64]={};
|
||||
inline unsigned gpio_writes=0,gpio_modes=0;
|
||||
inline uint32_t g_mock_millis=0;
|
||||
inline uint32_t millis(){return g_mock_millis;}
|
||||
inline void pinMode(uint8_t p,uint8_t m){assert(p<64);++gpio_modes;g_mock_pin_modes[p]=m;}
|
||||
inline void digitalWrite(uint8_t p,uint8_t v){assert(p<64);++gpio_writes;g_mock_pin_levels[p]=v;}
|
||||
inline int digitalRead(uint8_t p){assert(p<64);return g_mock_pin_levels[p];}
|
||||
'''
|
||||
|
||||
|
||||
class RakNorGpioReservationsTest(unittest.TestCase):
|
||||
def test_real_board_and_gpio_preserve_nor_pin_ownership(self):
|
||||
compiler = os.environ.get('CXX') or shutil.which('g++') or shutil.which('clang++')
|
||||
self.assertIsNotNone(compiler, 'host C++17 compiler required')
|
||||
board = (ROOT / 'src/helpers/NRF52Board.cpp').read_text(encoding='ascii')
|
||||
fixture = FIXTURE.read_text(encoding='ascii')
|
||||
fixture = fixture.replace('@EXPOSED@', extract_braced(board, 'bool isExposedNrf52UserGpio('))
|
||||
fixture = fixture.replace('@APPROVAL@', extract_braced(board, 'bool NRF52Board::isUserGpioAvailable('))
|
||||
shared = ('RAK_4631=1', 'PIN_BOARD_SDA=13', 'PIN_BOARD_SCL=14',
|
||||
'PIN_GPS_EN=-1', 'PIN_VBAT_READ=5', 'USER_GPIO_CONTROL=1')
|
||||
normal = ('OTA_RAK15002_SD_STORE=1', 'OTA_SD_STORE=1',
|
||||
'OTA_QSPI_SCK_ARDUINO_PIN=3', 'OTA_QSPI_CS_ARDUINO_PIN=31',
|
||||
'OTA_QSPI_IO0_ARDUINO_PIN=30', 'OTA_QSPI_IO1_ARDUINO_PIN=29',
|
||||
'OTA_QSPI_IO2_NOT_CONNECTED=1', 'OTA_QSPI_IO3_NOT_CONNECTED=1')
|
||||
arbitrary = ('TEST_ARBITRARY=1', 'OTA_QSPI_SCK_ARDUINO_PIN=19',
|
||||
'OTA_QSPI_CS_ARDUINO_PIN=2', 'OTA_QSPI_IO0_ARDUINO_PIN=20',
|
||||
'OTA_QSPI_IO1_ARDUINO_PIN=21', 'OTA_QSPI_IO2_ARDUINO_PIN=24',
|
||||
'OTA_QSPI_IO3_ARDUINO_PIN=25')
|
||||
variants = (
|
||||
('normal_sensor_nor', normal + ('NRF52_PLATFORM=1', 'OTA_QSPI_STORE=1',
|
||||
'OTA_RAK_AUTO_STORE=1', 'TEST_EXPECT_NOR=1'), 48),
|
||||
('normal_sensor_without_nor', normal + ('NRF52_PLATFORM=1', 'TEST_EXPECT_NOR=0'), 46),
|
||||
('wrong_backend_does_not_claim', normal + ('OTA_QSPI_STORE=1', 'TEST_EXPECT_NOR=0'), 46),
|
||||
('arbitrary_six_nor_pads', arbitrary + ('NRF52_PLATFORM=1', 'OTA_QSPI_STORE=1',
|
||||
'TEST_EXPECT_NOR=1'), 51),
|
||||
('arbitrary_without_backend', arbitrary + ('NRF52_PLATFORM=1', 'TEST_EXPECT_NOR=0'), 39),
|
||||
# Policy-only robustness control: ignored explicit pads must not
|
||||
# become user reservations. The storage backend independently
|
||||
# rejects contradictory connected/disconnected configuration.
|
||||
('disconnected_explicit_io2_io3', arbitrary + ('NRF52_PLATFORM=1',
|
||||
'OTA_QSPI_STORE=1', 'TEST_EXPECT_NOR=1', 'TEST_DISCONNECTED_OVERRIDES=1',
|
||||
'OTA_QSPI_IO2_NOT_CONNECTED=1', 'OTA_QSPI_IO3_NOT_CONNECTED=1'), 47),
|
||||
)
|
||||
sanitizer = (['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
||||
'-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else [])
|
||||
with tempfile.TemporaryDirectory(prefix='rak-nor-gpio-reservations-') as folder:
|
||||
work = Path(folder)
|
||||
(work / 'WVariant.h').write_text('#pragma once\n#include <cstdint>\n', encoding='ascii')
|
||||
(work / 'Arduino.h').write_text(ARDUINO, encoding='ascii')
|
||||
# Preserve the header unchanged; its quoted sensor include resolves
|
||||
# against the real helper directory after this private directory.
|
||||
(work / 'UserGpioPinPolicy.h').write_bytes(POLICY.read_bytes())
|
||||
(work / 'test.cpp').write_text(fixture, encoding='ascii')
|
||||
for name, macros, scenarios in variants:
|
||||
with self.subTest(variant=name):
|
||||
binary = work / (name + ('.exe' if os.name == 'nt' else ''))
|
||||
command = [compiler, '-std=c++17', '-Wall', '-Wextra', '-Werror',
|
||||
'-Wno-unused-parameter', *sanitizer,
|
||||
*['-D' + x for x in shared + macros], '-I' + str(work),
|
||||
'-I' + str(ROOT / 'src/helpers'), '-I' + str(ROOT / 'src'),
|
||||
'-I' + str(ROOT / 'variants/rak4631'),
|
||||
str(work / 'test.cpp'), str(ROOT / 'src/helpers/UserGpio.cpp'),
|
||||
'-o', str(binary)]
|
||||
built = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
||||
tested = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
|
||||
self.assertEqual(tested.returncode, 0, tested.stdout + tested.stderr)
|
||||
self.assertIn(f'{scenarios} RAK NOR GPIO ownership scenarios passed', tested.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user