Add Simpson board support

This commit is contained in:
sim1988son
2026-10-15 20:39:48 +02:00
parent 1f47fc56cd
commit 539271f293
13 changed files with 466 additions and 12 deletions
@@ -670,6 +670,16 @@ public:
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_prev_page(0), _shutdown_init(false), sensors_lpp(200) { }
void showBluetoothPage() {
_page = BLUETOOTH;
_quick_sel = 0;
}
void showGpsPage() {
_page = STATUS;
_quick_sel = 0;
}
// Flips home screen between LOCK page and the page that was showing before locking
void setLocked(bool locked) {
if (locked) {
@@ -291,6 +291,9 @@ struct KeyboardWidget {
// next letter, then reverts) — Hold-Enter on Shift toggles caps_lock, which
// keeps it on for a whole run of capitals instead.
bool caps_lock = false;
bool _physical_t9_input = false;
const char* input_mode_hint = nullptr;
bool hide_keyboard_grid = false;
// Set by render() every time it's actually called; UITask clears it before
// curr->render() each frame (beginFrame()) so it reflects only "was the
// keyboard the thing on screen this frame" -- lets the alert overlay (new
@@ -355,6 +358,16 @@ struct KeyboardWidget {
// the #@/abc key's existing cycle (case 4 below) reaches it for free.
int totalPages() const { return hasAltAlphabet() ? 3 : 2; }
bool pageIsSymbols(int pg) const { return pg == totalPages() - 1; }
void openSymbolsPage() {
if (!_visible || isPin() || _ph_menu.active || cursor_mode || accent_active) return;
page = totalPages() - 1;
t9_cell = -1;
}
void backspaceTyped() {
if (_visible) handleInput(0x08);
}
// Which script (Latin/Cyrillic/Greek) the given non-symbols page shows.
uint8_t scriptAt(int pg) const { return pg == 0 ? mainScript() : altScript(); }
@@ -492,6 +505,29 @@ struct KeyboardWidget {
insertGlyph(one, false);
}
#if defined(SIMPSON)
void typePhysicalT9Cell(int cell, bool uppercase) {
if (!_visible || isPin() || cell < 0 || cell >= KB_T9_ROWS * KB_T9_COLS ||
_ph_menu.active || cursor_mode || accent_active) return;
const int saved_row = row;
const int saved_col = col;
const int saved_page = page;
const bool saved_caps = caps;
row = cell / KB_T9_COLS;
col = cell % KB_T9_COLS;
page = 0;
caps = uppercase;
_physical_t9_input = true;
handleInput(KEY_ENTER);
_physical_t9_input = false;
row = saved_row;
col = saved_col;
page = saved_page;
caps = saved_caps;
}
#endif
void clearPlaceholders() { _ph_count = 0; }
void addPlaceholder(const char* ph) {
@@ -604,7 +640,8 @@ struct KeyboardWidget {
// lines, so the preview keeps the same height whichever mode is on.
// A tall screen (the 4.2" panel: 20 lines) gives the keys half a line more each.
const int key_lh = display.height() >= 18 * lh ? lh * 3 / 2 : lh;
const int kb_h = compact_ui ? (KB_T9_ROWS + 1) * lh : (rows + 1) * key_lh;
const bool hide_grid = hide_keyboard_grid && !isPin();
const int kb_h = hide_grid ? lh : (compact_ui ? (KB_T9_ROWS + 1) * lh : (rows + 1) * key_lh);
const int preview_h = display.height() - kb_h - display.sepH();
const int prev_lines = (preview_h / lh) > 1 ? (preview_h / lh) : 1;
const int sep_y = prev_lines * lh;
@@ -794,13 +831,21 @@ struct KeyboardWidget {
// submit -- so they get no hint of their own). Physical buttons (if used
// instead of/alongside CardKB) still drive row/col/page as normal; it
// just won't be visible on this screen which cell is selected.
if (compact_ui) {
display.setColor(UIColor::primary_txt);
if (cursor_mode) {
display.drawTextCentered(display.width() / 2, chars_y, "<> char ^v word");
} else {
display.drawTextCentered(display.width() / 2, chars_y, "Tab: placeholders");
display.drawTextCentered(display.width() / 2, chars_y + lh, "Fn+letter: accent");
if (compact_ui || hide_grid) {
if (!hide_grid) {
display.setColor(UIColor::primary_txt);
if (cursor_mode) {
display.drawTextCentered(display.width() / 2, chars_y, "<> char ^v word");
} else {
display.drawTextCentered(display.width() / 2, chars_y, "Tab: placeholders");
display.drawTextCentered(display.width() / 2, chars_y + lh, "Fn+letter: accent");
}
} else if (input_mode_hint) {
char hint[40];
snprintf(hint, sizeof(hint), "FN: %s", input_mode_hint);
display.setColor(UIColor::primary_txt);
display.setCursor(display.width() - display.getTextWidth(hint) - 2, display.height() - lh);
display.print(hint);
}
} else {
// character grid
@@ -1228,7 +1273,11 @@ struct KeyboardWidget {
// the special row's DONE cell -- same as a physical button. CardKB's
// own Enter is only ever KEY_ENTER when that's true (see pollCardKB()'s
// KEY_KB_ENTER cases just above), so there's no ambiguity here.
if (row < rows && isT9()) {
if (row < rows && isT9()
#if defined(SIMPSON)
&& !_physical_t9_input
#endif
) {
int cell = row * cols + col;
const char* group = t9GroupStr(cell);
int glen = kbUtf8Len(group); // codepoint count, not byte length
+135
View File
@@ -11,6 +11,9 @@
#include "target.h"
#include "CardKB.h"
#if defined(SIMPSON)
#include <string.h>
#endif
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
@@ -120,6 +123,13 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
_sensors = sensors;
_node_prefs = node_prefs;
_kb.prefs = node_prefs;
#if defined(SIMPSON)
if (!_simpson_keypad.begin()) {
MESH_DEBUG_PRINTLN("Simpson keypad initialization failed");
}
_kb.input_mode_hint = "Ctl";
_kb.hide_keyboard_grid = true;
#endif
if (node_prefs) battery::useCurve(node_prefs->batt_curve_mv); // Settings > Battery curve
uint32_t aoff = autoOffMillis();
_auto_off = millis() + (aoff > 0 ? aoff : AUTO_OFF_MILLIS);
@@ -402,6 +412,20 @@ void UITask::gotoMessagesScreen() {
setCurrScreen(messages_screen);
}
#if defined(SIMPSON)
void UITask::gotoBluetoothHomePage() {
setCurrScreen(home);
static_cast<HomeScreen*>(home)->showBluetoothPage();
_next_refresh = 0;
}
void UITask::gotoGpsHomePage() {
setCurrScreen(home);
static_cast<HomeScreen*>(home)->showGpsPage();
_next_refresh = 0;
}
#endif
#ifdef SIM_PLATFORM
void UITask::simMessage(const char* text) {
UiEvent ev = {};
@@ -1066,6 +1090,114 @@ void UITask::pollHallSensor() {
#endif
}
#if defined(SIMPSON)
void UITask::pollSimpsonKeypad() {
const uint8_t key = _simpson_keypad.poll();
if (key != SIMPSON_KEY_NONE) handleSimpsonKeypadKey(key);
}
void UITask::handleSimpsonKeypadKey(uint8_t key) {
if (checkDisplayOn(KEY_ENTER) == 0 || (_locked && !_unlock_kb)) return;
const uint32_t auto_off = autoOffMillis();
if (auto_off > 0) _auto_off = millis() + auto_off;
if (key == SIMPSON_KEY_MODE_CHANGED) {
static const char* const modes[] = { "Ctl", "abc", "ABC", "123" };
_kb.input_mode_hint = modes[_simpson_keypad.mode()];
_kb.hide_keyboard_grid = true;
showAlert(modes[_simpson_keypad.mode()], 1200);
_next_refresh = 0;
return;
}
switch (key) {
case SIMPSON_KEY_CANCEL:
enqueueKey(KEY_CANCEL);
break;
case SIMPSON_KEY_UP:
enqueueKey(KEY_UP);
break;
case SIMPSON_KEY_DOWN:
enqueueKey(KEY_DOWN);
break;
case SIMPSON_KEY_LEFT:
enqueueKey(KEY_LEFT);
break;
case SIMPSON_KEY_RIGHT:
enqueueKey(KEY_RIGHT);
break;
case SIMPSON_KEY_ENTER:
enqueueKey(_unlock_kb ? KEY_KB_ENTER : KEY_ENTER);
break;
case SIMPSON_KEY_BLUETOOTH:
if (!_unlock_kb) gotoBluetoothHomePage();
break;
case SIMPSON_KEY_GPS:
if (!_unlock_kb) gotoGpsHomePage();
break;
case SIMPSON_KEY_MESSAGES:
if (!_unlock_kb) gotoMessagesScreen();
break;
case SIMPSON_KEY_BRIGHTNESS_UP:
case SIMPSON_KEY_BRIGHTNESS_DOWN:
if (!_unlock_kb && _node_prefs) {
uint8_t& brightness = _node_prefs->display_brightness;
if (key == SIMPSON_KEY_BRIGHTNESS_UP && brightness < 4) ++brightness;
if (key == SIMPSON_KEY_BRIGHTNESS_DOWN && brightness > 0) --brightness;
applyBrightness();
bool dirty = true;
savePrefsIfDirty(dirty);
}
break;
case SIMPSON_KEY_SYMBOLS:
if (_unlock_kb) break;
if (_kb.isVisible()) _kb.openSymbolsPage();
else showAlert("Open a text field first", 1200);
break;
case SIMPSON_KEY_BACKSPACE:
if (_kb.isVisible()) {
if (_unlock_kb) handleUnlockKey(0x08);
else _kb.backspaceTyped();
} else {
enqueueKey(KEY_CANCEL);
}
break;
default:
if (_kb.isVisible()) {
const auto typeInput = [this](char c) {
if (_unlock_kb) handleUnlockKey(c);
else _kb.handleInput(c);
};
switch (_simpson_keypad.mode()) {
case SIMPSON_KEYPAD_ABC:
case SIMPSON_KEYPAD_ABC_UPPER:
if (key == ',') {
_kb.typePhysicalT9Cell(0, _simpson_keypad.mode() == SIMPSON_KEYPAD_ABC_UPPER);
} else {
static const char t9_keys[] = "ADGJMPTW";
const char* found = strchr(t9_keys, (char)key);
if (found) {
_kb.typePhysicalT9Cell((int)(found - t9_keys) + 1,
_simpson_keypad.mode() == SIMPSON_KEYPAD_ABC_UPPER);
} else if (key == ' ') {
typeInput(' ');
}
}
break;
case SIMPSON_KEYPAD_NUMERIC:
if (key >= '0' && key <= '9') typeInput((char)key);
break;
case SIMPSON_KEYPAD_CONTROL:
break;
}
}
break;
}
_next_refresh = 0;
}
#endif
#include "PerfL1.h"
void UITask::loop() {
@@ -1293,6 +1425,9 @@ void UITask::loop() {
}
#endif
pollCardKB();
#if defined(SIMPSON)
pollSimpsonKeypad();
#endif
pollHallSensor();
#ifdef ENV_USE_TCA8418
{
+14
View File
@@ -8,6 +8,9 @@
#include <helpers/ContactInfo.h>
#include <Arduino.h>
#include <helpers/sensors/LPPDataHelpers.h>
#if defined(SIMPSON)
#include <SimpsonKeypad.h>
#endif
#ifndef LED_STATE_ON
#define LED_STATE_ON 1
@@ -80,6 +83,9 @@ class UITask : public UITaskBase, public UiCoreHost {
// each) + NUL = 372B, rounded up with margin.
char _bot_buzz_buf[400];
KeyboardWidget _kb; // shared across all screens — only one active at a time
#if defined(SIMPSON)
SimpsonKeypad _simpson_keypad;
#endif
unsigned long _alert_expiry;
unsigned long _alert_t0 = 0; // when the banner began to come down
// Settings > Message alert Compact: the alert is an envelope and a count
@@ -231,6 +237,10 @@ private:
#endif
#endif
void pollCardKB();
#if defined(SIMPSON)
void pollSimpsonKeypad();
void handleSimpsonKeypadKey(uint8_t key);
#endif
void handleCardKBByte(uint8_t raw);
void setCardKBBackground(bool on); // no-op without CardKB / the poll task
void startCardKBCapture(); // end of begin(): start the poll task
@@ -306,6 +316,10 @@ public:
void gotoHomeScreen() { setCurrScreen(home); }
void gotoSettingsScreen();
void gotoMessagesScreen();
#if defined(SIMPSON)
void gotoBluetoothHomePage();
void gotoGpsHomePage();
#endif
#ifdef SIM_PLATFORM
void simCompose(const char* text); // sim tests: a channel post's keyboard holding `text`
void simMessage(const char* text); // sim tests: a channel message arrives
+27
View File
@@ -0,0 +1,27 @@
[env:Simpson_companion_solo]
extends = Simpson
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
board_upload.maximum_size = 712704
build_unflags = -Ofast -D OFFLINE_QUEUE_SIZE=256
build_flags = ${Simpson.build_flags}
-I examples/companion_radio/ui-new
-D MAX_CONTACTS=350
-D MAX_GROUP_CHANNELS=40
-D BLE_PIN_CODE=123456
-D DUAL_SERIAL=1
-D OFFLINE_QUEUE_SIZE=128
-D DISPLAY_CLASS=SH1106Display
-D FIRMWARE_SOLO_BUILD=1
-D UI_SENSORS_PAGE=1
-Os
build_src_filter = ${Simpson.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/SH1106Display.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/ui/GenericVibration.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-new/*.cpp>
lib_deps = ${Simpson.lib_deps}
adafruit/RTClib @ ^2.1.3
densaugeo/base64 @ ~1.4.0
+3
View File
@@ -40,6 +40,9 @@ bool SH1106Display::begin()
// spi_dev NULL, and UITask::begin() calls turnOn() regardless of our
// return value, which then dereferences the null spi_dev.
bool ok = display.begin(addr ? addr : DISPLAY_ADDRESS, true);
#ifdef DISPLAY_ROTATION
display.setRotation(DISPLAY_ROTATION);
#endif
return addr != 0 && ok;
}
+3 -1
View File
@@ -5,7 +5,9 @@
#include <helpers/NRF52Board.h>
#define PIN_VBAT_READ 17
#define ADC_MULTIPLIER (1.815f) // dependent on voltage divider resistors. TODO: more accurate battery tracking
#ifndef ADC_MULTIPLIER
#define ADC_MULTIPLIER (1.815f) // dependent on voltage divider resistors. TODO: more accurate battery tracking
#endif
class PromicroBoard : public NRF52BoardDCDC {
protected:
+5 -2
View File
@@ -7,7 +7,11 @@
#include <helpers/radiolib/CustomSX1262Wrapper.h>
#include <helpers/AutoDiscoverRTCClock.h>
#ifdef DISPLAY_CLASS
#include <helpers/ui/SSD1306Display.h>
#if defined(SIMPSON)
#include <helpers/ui/SH1106Display.h>
#else
#include <helpers/ui/SSD1306Display.h>
#endif
#include <helpers/ui/MomentaryButton.h>
#endif
@@ -25,4 +29,3 @@ extern EnvironmentSensorManager sensors;
bool radio_init();
mesh::LocalIdentity radio_new_identity();
+70
View File
@@ -0,0 +1,70 @@
#include "SimpsonKeypad.h"
#include <Wire.h>
namespace {
char control_keymap[19] = {
SIMPSON_KEY_CANCEL, SIMPSON_KEY_UP, 0, SIMPSON_KEY_BLUETOOTH,
SIMPSON_KEY_LEFT, SIMPSON_KEY_ENTER, SIMPSON_KEY_RIGHT, SIMPSON_KEY_BRIGHTNESS_UP,
SIMPSON_KEY_SYMBOLS, SIMPSON_KEY_DOWN, 0, SIMPSON_KEY_BRIGHTNESS_DOWN,
SIMPSON_KEY_MESSAGES, 0, SIMPSON_KEY_FN, SIMPSON_KEY_GPS, 0, 0, 0
};
char alphabet_keymap[19] = {
',', 'A', 'D', SIMPSON_KEY_BLUETOOTH,
'G', 'J', 'M', SIMPSON_KEY_BRIGHTNESS_UP,
'P', 'T', 'W', SIMPSON_KEY_BRIGHTNESS_DOWN,
SIMPSON_KEY_BACKSPACE, ' ', SIMPSON_KEY_FN, SIMPSON_KEY_GPS, 0, 0, 0
};
char numeric_keymap[19] = {
'1', '2', '3', SIMPSON_KEY_BLUETOOTH,
'4', '5', '6', SIMPSON_KEY_BRIGHTNESS_UP,
'7', '8', '9', SIMPSON_KEY_BRIGHTNESS_DOWN,
SIMPSON_KEY_BACKSPACE, '0', SIMPSON_KEY_FN, SIMPSON_KEY_GPS, 0, 0, 0
};
}
bool SimpsonKeypad::begin() {
if (!_keypad.begin()) return false;
_keypad.setKeyPadMode(I2C_KEYPAD_4x4);
_keypad.setDebounceThreshold(50);
loadModeMap();
_ready = true;
return true;
}
void SimpsonKeypad::loadModeMap() {
switch (_mode) {
case SIMPSON_KEYPAD_CONTROL:
_keypad.loadKeyMap(control_keymap);
break;
case SIMPSON_KEYPAD_ABC:
case SIMPSON_KEYPAD_ABC_UPPER:
_keypad.loadKeyMap(alphabet_keymap);
break;
case SIMPSON_KEYPAD_NUMERIC:
_keypad.loadKeyMap(numeric_keymap);
break;
}
}
uint8_t SimpsonKeypad::poll() {
if (!_ready) return SIMPSON_KEY_NONE;
uint8_t key = _keypad.getChar();
if (key == I2C_KEYPAD_THRESHOLD) return SIMPSON_KEY_NONE;
if (key == SIMPSON_KEY_NONE) {
_last_key = SIMPSON_KEY_NONE;
return SIMPSON_KEY_NONE;
}
if (key == _last_key) return SIMPSON_KEY_NONE;
_last_key = key;
if (key == SIMPSON_KEY_FN) {
_mode = static_cast<SimpsonKeypadMode>((_mode + 1) % 4);
loadModeMap();
return SIMPSON_KEY_MODE_CHANGED;
}
return key;
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <Arduino.h>
#include <I2CKeyPad.h>
#ifndef SIMPSON_KEYPAD_ADDR
#define SIMPSON_KEYPAD_ADDR 0x20
#endif
enum SimpsonKeypadMode : uint8_t {
SIMPSON_KEYPAD_CONTROL,
SIMPSON_KEYPAD_ABC,
SIMPSON_KEYPAD_ABC_UPPER,
SIMPSON_KEYPAD_NUMERIC
};
enum SimpsonKeypadKey : uint8_t {
SIMPSON_KEY_NONE = 0,
SIMPSON_KEY_CANCEL = 1,
SIMPSON_KEY_UP,
SIMPSON_KEY_BLUETOOTH,
SIMPSON_KEY_LEFT,
SIMPSON_KEY_ENTER,
SIMPSON_KEY_RIGHT,
SIMPSON_KEY_BRIGHTNESS_UP,
SIMPSON_KEY_SYMBOLS,
SIMPSON_KEY_DOWN,
SIMPSON_KEY_BRIGHTNESS_DOWN,
SIMPSON_KEY_MESSAGES,
SIMPSON_KEY_FN,
SIMPSON_KEY_GPS,
SIMPSON_KEY_BACKSPACE,
SIMPSON_KEY_MODE_CHANGED
};
class SimpsonKeypad {
I2CKeyPad _keypad;
SimpsonKeypadMode _mode = SIMPSON_KEYPAD_CONTROL;
uint8_t _last_key = SIMPSON_KEY_NONE;
bool _ready = false;
void loadModeMap();
public:
SimpsonKeypad() : _keypad(SIMPSON_KEYPAD_ADDR, &Wire) {}
bool begin();
uint8_t poll();
SimpsonKeypadMode mode() const { return _mode; }
};
+21
View File
@@ -0,0 +1,21 @@
[Simpson]
extends = Promicro
build_unflags = -D PIN_GPS_RX=3 -D PIN_GPS_TX=4 -D ENV_INCLUDE_AHTX0=1 -D ENV_INCLUDE_BME280=1 -D ENV_INCLUDE_BMP280=1 -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 -D ENV_INCLUDE_INA226=1
build_flags = ${Promicro.build_flags}
-I variants/simpson
-D SIMPSON
-D PIN_GPS_TX=3
-D PIN_GPS_RX=4
-D PIN_VIBRATION=1
-D ADC_MULTIPLIER=1.71
-D SIMPSON_KEYPAD_ADDR=0x20
-D DISPLAY_ROTATION=2
-D ENV_SKIP_GPS_DETECT
build_src_filter = ${nrf52_base.build_src_filter}
+<helpers/sensors>
+<../variants/simpson>
+<../variants/promicro/target.cpp>
+<../variants/promicro/PromicroBoard.cpp>
lib_deps = ${Promicro.lib_deps}
adafruit/Adafruit SH110X @ ^2.1.13
RobTillaart/I2CKeyPad @ ^0.5.2
+9
View File
@@ -0,0 +1,9 @@
#include "variant.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
const uint32_t g_ADigitalPinMap[] = {
8, 6, 17, 20, 22, 24, 32, 11, 36, 38, 9, 10, 43, 45, 47, 2, 29, 31, 33, 34, 37, 13, 15
};
void initVariant() {}
+62
View File
@@ -0,0 +1,62 @@
/*
* Simpson variant for the Pro Micro nRF52840 carrier used by this device.
* Arduino pin indices map to physical nRF52840 GPIOs in variant.cpp.
*/
#pragma once
#include "WVariant.h"
#define VARIANT_MCK (64000000ul)
#define USE_LFRC
#define PIN_EXT_VCC (21)
#define EXT_VCC PIN_EXT_VCC
#define BATTERY_PIN (17)
#define ADC_RESOLUTION 12
#define PINS_COUNT (23)
#define NUM_DIGITAL_PINS (23)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
#define PIN_SERIAL1_TX (4)
#define PIN_SERIAL1_RX (3)
#define WIRE_INTERFACES_COUNT 1
#define PIN_WIRE_SDA (8)
#define PIN_WIRE_SCL (7)
#define SPI_INTERFACES_COUNT 1
#define PIN_SPI_SCK (12)
#define PIN_SPI_MISO (15)
#define PIN_SPI_MOSI (14)
#define PIN_SPI_NSS (13)
#define PIN_LED (22)
#define LED_PIN PIN_LED
#define LED_BLUE PIN_LED
#define LED_BUILTIN PIN_LED
#define LED_STATE_ON 1
#define PIN_BUTTON1 (6)
#define BUTTON_PIN PIN_BUTTON1
#define PIN_BUZZER (0)
#ifndef PIN_VIBRATION
#define PIN_VIBRATION (1)
#endif
#define PIN_GPS_EN (5)
#define P_LORA_NSS (13)
#define P_LORA_DIO_1 (11)
#define P_LORA_RESET (10)
#define P_LORA_BUSY (16)
#define P_LORA_MISO (15)
#define P_LORA_SCLK (12)
#define P_LORA_MOSI (14)
#define SX126X_POWER_EN (21)
#define SX126X_RXEN (2)
#define SX126X_TXEN (-1)
#define SX126X_DIO2_AS_RF_SWITCH true
#define SX126X_DIO3_TCXO_VOLTAGE (1.8f)