mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-10 14:01:00 +10:00
Compare commits
12
Commits
develop
...
Temp_Offset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89d0221174 | ||
|
|
471017697f | ||
|
|
4f222ce1e8 | ||
|
|
7bf905d86d | ||
|
|
400d2d604d | ||
|
|
eaf15f78de | ||
|
|
4bb2c5634f | ||
|
|
9bd16cf474 | ||
|
|
1961c59a60 | ||
|
|
5fc914cb87 | ||
|
|
5fdbdecd41 | ||
|
|
466408e195 |
@@ -276,6 +276,42 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
|||||||
ui->update();
|
ui->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Screen::showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
|
||||||
|
int maxValueTenths, std::function<void(int)> bannerCallback)
|
||||||
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||||
|
#endif
|
||||||
|
if (minValueTenths > maxValueTenths) {
|
||||||
|
int temp = minValueTenths;
|
||||||
|
minValueTenths = maxValueTenths;
|
||||||
|
maxValueTenths = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initialValueTenths < minValueTenths) {
|
||||||
|
initialValueTenths = minValueTenths;
|
||||||
|
} else if (initialValueTenths > maxValueTenths) {
|
||||||
|
initialValueTenths = maxValueTenths;
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||||
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||||
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||||
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||||
|
NotificationRenderer::pauseBanner = false;
|
||||||
|
NotificationRenderer::curSelected = 0;
|
||||||
|
NotificationRenderer::current_notification_type = notificationTypeEnum::signed_decimal_picker;
|
||||||
|
NotificationRenderer::signedDecimalValueTenths = static_cast<int16_t>(initialValueTenths);
|
||||||
|
NotificationRenderer::signedDecimalMinTenths = static_cast<int16_t>(minValueTenths);
|
||||||
|
NotificationRenderer::signedDecimalMaxTenths = static_cast<int16_t>(maxValueTenths);
|
||||||
|
NotificationRenderer::signedDecimalIsNegative = (initialValueTenths < 0);
|
||||||
|
|
||||||
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||||
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
ui->setTargetFPS(60);
|
||||||
|
ui->update();
|
||||||
|
}
|
||||||
|
|
||||||
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
std::function<void(const std::string &)> textCallback)
|
std::function<void(const std::string &)> textCallback)
|
||||||
{
|
{
|
||||||
@@ -1269,6 +1305,8 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
fsi.positions.focusedModule = numframes;
|
fsi.positions.focusedModule = numframes;
|
||||||
if (m && m == waypointModule)
|
if (m && m == waypointModule)
|
||||||
fsi.positions.waypoint = numframes;
|
fsi.positions.waypoint = numframes;
|
||||||
|
if (m && strcmp(m->getName(), "EnvironmentTelemetry") == 0)
|
||||||
|
fsi.positions.environment = numframes;
|
||||||
|
|
||||||
indicatorIcons.push_back(icon_module);
|
indicatorIcons.push_back(icon_module);
|
||||||
numframes++;
|
numframes++;
|
||||||
@@ -1982,6 +2020,8 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
menuHandler::textMessageBaseMenu();
|
menuHandler::textMessageBaseMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.environment) {
|
||||||
|
menuHandler::environmentTelemetryBaseMenu();
|
||||||
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
||||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2)
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input };
|
enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, signed_decimal_picker, text_input };
|
||||||
|
|
||||||
struct BannerOverlayOptions {
|
struct BannerOverlayOptions {
|
||||||
const char *message;
|
const char *message;
|
||||||
@@ -312,6 +312,8 @@ class Screen : public concurrency::OSThread
|
|||||||
|
|
||||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||||
|
void showSignedDecimalPicker(const char *message, uint32_t durationMs, int initialValueTenths, int minValueTenths,
|
||||||
|
int maxValueTenths, std::function<void(int)> bannerCallback);
|
||||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||||
std::function<void(const std::string &)> textCallback);
|
std::function<void(const std::string &)> textCallback);
|
||||||
|
|
||||||
@@ -707,6 +709,7 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t firstFavorite = 255;
|
uint8_t firstFavorite = 255;
|
||||||
uint8_t lastFavorite = 255;
|
uint8_t lastFavorite = 255;
|
||||||
uint8_t lora = 255;
|
uint8_t lora = 255;
|
||||||
|
uint8_t environment = 255;
|
||||||
} positions;
|
} positions;
|
||||||
|
|
||||||
uint8_t frameCount = 0;
|
uint8_t frameCount = 0;
|
||||||
|
|||||||
@@ -58,6 +58,32 @@ BannerOverlayOptions createStaticBannerOptions(const char *message, const MenuOp
|
|||||||
return bannerOptions;
|
return bannerOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr float kTemperatureOffsetMinC = -20.0f;
|
||||||
|
constexpr float kTemperatureOffsetMaxC = 20.0f;
|
||||||
|
constexpr float kTemperatureOffsetDeltaFPerC = 1.8f;
|
||||||
|
|
||||||
|
bool useImperialTemperatureOffsetUnits()
|
||||||
|
{
|
||||||
|
return config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL ||
|
||||||
|
moduleConfig.telemetry.environment_display_fahrenheit;
|
||||||
|
}
|
||||||
|
|
||||||
|
float clampTemperatureOffsetC(float offsetC)
|
||||||
|
{
|
||||||
|
if (offsetC < kTemperatureOffsetMinC) {
|
||||||
|
return kTemperatureOffsetMinC;
|
||||||
|
}
|
||||||
|
if (offsetC > kTemperatureOffsetMaxC) {
|
||||||
|
return kTemperatureOffsetMaxC;
|
||||||
|
}
|
||||||
|
return offsetC;
|
||||||
|
}
|
||||||
|
|
||||||
|
int toTenthsRounded(float value)
|
||||||
|
{
|
||||||
|
return (value >= 0.0f) ? static_cast<int>(value * 10.0f + 0.5f) : static_cast<int>(value * 10.0f - 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||||
@@ -1009,6 +1035,52 @@ void menuHandler::textMessageMenu()
|
|||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void menuHandler::environmentTelemetryBaseMenu()
|
||||||
|
{
|
||||||
|
enum optionsNumbers { Back, SetTempOffset };
|
||||||
|
|
||||||
|
static const char *optionsArray[] = {"Back", "Set Temp Offset"};
|
||||||
|
static int optionsEnumArray[] = {Back, SetTempOffset};
|
||||||
|
|
||||||
|
BannerOverlayOptions bannerOptions;
|
||||||
|
bannerOptions.message = "Env Actions";
|
||||||
|
bannerOptions.optionsArrayPtr = optionsArray;
|
||||||
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
|
bannerOptions.optionsCount = 2;
|
||||||
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
|
if (selected == SetTempOffset) {
|
||||||
|
menuQueue = EnvironmentTempOffsetPicker;
|
||||||
|
screen->runNow();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
screen->showOverlayBanner(bannerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::environmentTemperatureOffsetPicker()
|
||||||
|
{
|
||||||
|
static char pickerTitle[40];
|
||||||
|
|
||||||
|
const bool useImperial = useImperialTemperatureOffsetUnits();
|
||||||
|
const char displayUnit = useImperial ? 'F' : 'C';
|
||||||
|
const float currentOffsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
|
||||||
|
const float displayOffset = useImperial ? (currentOffsetC * kTemperatureOffsetDeltaFPerC) : currentOffsetC;
|
||||||
|
const float minDisplay = useImperial ? (kTemperatureOffsetMinC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMinC;
|
||||||
|
const float maxDisplay = useImperial ? (kTemperatureOffsetMaxC * kTemperatureOffsetDeltaFPerC) : kTemperatureOffsetMaxC;
|
||||||
|
|
||||||
|
snprintf(pickerTitle, sizeof(pickerTitle), "Set Temp Offset (%c)", displayUnit);
|
||||||
|
screen->showSignedDecimalPicker(pickerTitle, 60000, toTenthsRounded(displayOffset), toTenthsRounded(minDisplay),
|
||||||
|
toTenthsRounded(maxDisplay), [useImperial](int pickedTenths) -> void {
|
||||||
|
float selectedOffset = static_cast<float>(pickedTenths) / 10.0f;
|
||||||
|
if (useImperial) {
|
||||||
|
selectedOffset /= kTemperatureOffsetDeltaFPerC;
|
||||||
|
}
|
||||||
|
|
||||||
|
moduleConfig.telemetry.environment_temperature_offset_c =
|
||||||
|
clampTemperatureOffsetC(selectedOffset);
|
||||||
|
nodeDB->saveToDisk(SEGMENT_MODULECONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void menuHandler::textMessageBaseMenu()
|
void menuHandler::textMessageBaseMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
|
enum optionsNumbers { Back, Preset, Freetext, enumEnd };
|
||||||
@@ -2218,9 +2290,6 @@ void menuHandler::testMenu()
|
|||||||
static int optionsEnumArray[5] = {Back};
|
static int optionsEnumArray[5] = {Back};
|
||||||
int options = 1;
|
int options = 1;
|
||||||
|
|
||||||
optionsArray[options] = "Number Picker";
|
|
||||||
optionsEnumArray[options++] = NumberPicker;
|
|
||||||
|
|
||||||
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy";
|
||||||
optionsEnumArray[options++] = ShowChirpy;
|
optionsEnumArray[options++] = ShowChirpy;
|
||||||
#ifdef HAS_I2S
|
#ifdef HAS_I2S
|
||||||
@@ -2234,10 +2303,7 @@ void menuHandler::testMenu()
|
|||||||
bannerOptions.optionsCount = options;
|
bannerOptions.optionsCount = options;
|
||||||
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
bannerOptions.optionsEnumPtr = optionsEnumArray;
|
||||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||||
if (selected == NumberPicker) {
|
if (selected == ShowChirpy) {
|
||||||
menuQueue = NumberTest;
|
|
||||||
screen->runNow();
|
|
||||||
} else if (selected == ShowChirpy) {
|
|
||||||
screen->toggleFrameVisibility("chirpy");
|
screen->toggleFrameVisibility("chirpy");
|
||||||
screen->setFrames(Screen::FOCUS_SYSTEM);
|
screen->setFrames(Screen::FOCUS_SYSTEM);
|
||||||
|
|
||||||
@@ -2253,12 +2319,6 @@ void menuHandler::testMenu()
|
|||||||
screen->showOverlayBanner(bannerOptions);
|
screen->showOverlayBanner(bannerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void menuHandler::numberTest()
|
|
||||||
{
|
|
||||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
|
||||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
|
||||||
}
|
|
||||||
|
|
||||||
void menuHandler::wifiBaseMenu()
|
void menuHandler::wifiBaseMenu()
|
||||||
{
|
{
|
||||||
enum optionsNumbers { Back, Wifi_toggle };
|
enum optionsNumbers { Back, Wifi_toggle };
|
||||||
@@ -2754,8 +2814,8 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
|||||||
case TestMenu:
|
case TestMenu:
|
||||||
testMenu();
|
testMenu();
|
||||||
break;
|
break;
|
||||||
case NumberTest:
|
case EnvironmentTempOffsetPicker:
|
||||||
numberTest();
|
environmentTemperatureOffsetPicker();
|
||||||
break;
|
break;
|
||||||
case WifiToggleMenu:
|
case WifiToggleMenu:
|
||||||
wifiToggleMenu();
|
wifiToggleMenu();
|
||||||
@@ -2810,4 +2870,4 @@ void menuHandler::saveUIConfig()
|
|||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class menuHandler
|
|||||||
ManageNodeMenu,
|
ManageNodeMenu,
|
||||||
RemoveFavorite,
|
RemoveFavorite,
|
||||||
TestMenu,
|
TestMenu,
|
||||||
NumberTest,
|
EnvironmentTempOffsetPicker,
|
||||||
WifiToggleMenu,
|
WifiToggleMenu,
|
||||||
BluetoothToggleMenu,
|
BluetoothToggleMenu,
|
||||||
ScreenOptionsMenu,
|
ScreenOptionsMenu,
|
||||||
@@ -78,6 +78,7 @@ class menuHandler
|
|||||||
static void deleteMessagesMenu();
|
static void deleteMessagesMenu();
|
||||||
static void homeBaseMenu();
|
static void homeBaseMenu();
|
||||||
static void textMessageBaseMenu();
|
static void textMessageBaseMenu();
|
||||||
|
static void environmentTelemetryBaseMenu();
|
||||||
static void systemBaseMenu();
|
static void systemBaseMenu();
|
||||||
static void favoriteBaseMenu();
|
static void favoriteBaseMenu();
|
||||||
static void positionBaseMenu();
|
static void positionBaseMenu();
|
||||||
@@ -101,7 +102,7 @@ class menuHandler
|
|||||||
static void removeFavoriteMenu();
|
static void removeFavoriteMenu();
|
||||||
static void traceRouteMenu();
|
static void traceRouteMenu();
|
||||||
static void testMenu();
|
static void testMenu();
|
||||||
static void numberTest();
|
static void environmentTemperatureOffsetPicker();
|
||||||
static void wifiBaseMenu();
|
static void wifiBaseMenu();
|
||||||
static void wifiToggleMenu();
|
static void wifiToggleMenu();
|
||||||
static void screenOptionsMenu();
|
static void screenOptionsMenu();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#if HAS_TRACKBALL
|
#if HAS_TRACKBALL
|
||||||
@@ -52,16 +53,292 @@ bool NotificationRenderer::pauseBanner = false;
|
|||||||
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none;
|
||||||
uint32_t NotificationRenderer::numDigits = 0;
|
uint32_t NotificationRenderer::numDigits = 0;
|
||||||
uint32_t NotificationRenderer::currentNumber = 0;
|
uint32_t NotificationRenderer::currentNumber = 0;
|
||||||
|
int16_t NotificationRenderer::signedDecimalValueTenths = 0;
|
||||||
|
int16_t NotificationRenderer::signedDecimalMinTenths = -999;
|
||||||
|
int16_t NotificationRenderer::signedDecimalMaxTenths = 999;
|
||||||
|
bool NotificationRenderer::signedDecimalIsNegative = false;
|
||||||
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr;
|
||||||
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
std::function<void(const std::string &)> NotificationRenderer::textInputCallback = nullptr;
|
||||||
|
|
||||||
uint32_t pow_of_10(uint32_t n)
|
struct NumericSlotPickerState {
|
||||||
|
std::vector<uint8_t> digits;
|
||||||
|
bool hasSign = false;
|
||||||
|
bool isNegative = false;
|
||||||
|
uint8_t decimalDigits = 0;
|
||||||
|
int32_t minValue = 0;
|
||||||
|
int32_t maxValue = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
int32_t maxValueForDigits(uint8_t digitCount)
|
||||||
{
|
{
|
||||||
uint32_t ret = 1;
|
int64_t value = 0;
|
||||||
for (uint32_t i = 0; i < n; i++) {
|
for (uint8_t i = 0; i < digitCount; i++) {
|
||||||
ret *= 10;
|
value = (value * 10) + 9;
|
||||||
|
if (value > std::numeric_limits<int32_t>::max()) {
|
||||||
|
return std::numeric_limits<int32_t>::max();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ret;
|
return static_cast<int32_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t pickerSlotCount(const NumericSlotPickerState &state)
|
||||||
|
{
|
||||||
|
return static_cast<uint8_t>(state.digits.size() + (state.hasSign ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t clampPickerValue(int32_t value, int32_t minValue, int32_t maxValue)
|
||||||
|
{
|
||||||
|
if (value < minValue) {
|
||||||
|
return minValue;
|
||||||
|
}
|
||||||
|
if (value > maxValue) {
|
||||||
|
return maxValue;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t combinePickerDigits(const std::vector<uint8_t> &digits)
|
||||||
|
{
|
||||||
|
uint32_t value = 0;
|
||||||
|
for (const uint8_t digit : digits) {
|
||||||
|
value = (value * 10) + digit;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void splitPickerDigits(uint32_t value, std::vector<uint8_t> &digits)
|
||||||
|
{
|
||||||
|
if (digits.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = static_cast<int>(digits.size()) - 1; i >= 0; i--) {
|
||||||
|
digits[static_cast<size_t>(i)] = static_cast<uint8_t>(value % 10);
|
||||||
|
value /= 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t composePickerValue(const NumericSlotPickerState &state)
|
||||||
|
{
|
||||||
|
int64_t value = static_cast<int64_t>(combinePickerDigits(state.digits));
|
||||||
|
if (state.hasSign && state.isNegative && value != 0) {
|
||||||
|
value = -value;
|
||||||
|
}
|
||||||
|
return clampPickerValue(static_cast<int32_t>(value), state.minValue, state.maxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
void normalizePickerState(NumericSlotPickerState &state)
|
||||||
|
{
|
||||||
|
const bool keepNegativeZero = state.isNegative;
|
||||||
|
const int32_t clampedValue = composePickerValue(state);
|
||||||
|
const uint32_t absValue =
|
||||||
|
(clampedValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(clampedValue)) : static_cast<uint32_t>(clampedValue);
|
||||||
|
splitPickerDigits(absValue, state.digits);
|
||||||
|
|
||||||
|
if (!state.hasSign) {
|
||||||
|
state.isNegative = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clampedValue < 0) {
|
||||||
|
state.isNegative = true;
|
||||||
|
} else if (clampedValue > 0) {
|
||||||
|
state.isNegative = false;
|
||||||
|
} else {
|
||||||
|
// Keep selected sign for zero so users can pick +/- before entering digits.
|
||||||
|
state.isNegative = keepNegativeZero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPickerIncrementEvent(uint8_t eventType)
|
||||||
|
{
|
||||||
|
return eventType == INPUT_BROKER_UP || eventType == INPUT_BROKER_ALT_PRESS || eventType == INPUT_BROKER_UP_LONG;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPickerDecrementEvent(uint8_t eventType)
|
||||||
|
{
|
||||||
|
return eventType == INPUT_BROKER_DOWN || eventType == INPUT_BROKER_USER_PRESS || eventType == INPUT_BROKER_DOWN_LONG;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyPickerDelta(NumericSlotPickerState &state, int8_t selectedSlot, int8_t delta)
|
||||||
|
{
|
||||||
|
const uint8_t slotCount = pickerSlotCount(state);
|
||||||
|
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.hasSign && selectedSlot == 0) {
|
||||||
|
state.isNegative = !state.isNegative;
|
||||||
|
normalizePickerState(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int8_t digitOffset = state.hasSign ? 1 : 0;
|
||||||
|
const int8_t digitIndex = selectedSlot - digitOffset;
|
||||||
|
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t &digit = state.digits[static_cast<size_t>(digitIndex)];
|
||||||
|
if (delta > 0) {
|
||||||
|
digit = static_cast<uint8_t>((digit + 1) % 10);
|
||||||
|
} else {
|
||||||
|
digit = static_cast<uint8_t>((digit == 0) ? 9 : (digit - 1));
|
||||||
|
}
|
||||||
|
normalizePickerState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applyPickerKeypress(NumericSlotPickerState &state, int8_t selectedSlot, char key)
|
||||||
|
{
|
||||||
|
const uint8_t slotCount = pickerSlotCount(state);
|
||||||
|
if (selectedSlot < 0 || selectedSlot >= static_cast<int8_t>(slotCount)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.hasSign && selectedSlot == 0 && (key == '+' || key == '-')) {
|
||||||
|
state.isNegative = (key == '-');
|
||||||
|
normalizePickerState(state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key < '0' || key > '9') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int8_t digitOffset = state.hasSign ? 1 : 0;
|
||||||
|
const int8_t digitIndex = selectedSlot - digitOffset;
|
||||||
|
if (digitIndex < 0 || digitIndex >= static_cast<int8_t>(state.digits.size())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.digits[static_cast<size_t>(digitIndex)] = static_cast<uint8_t>(key - '0');
|
||||||
|
normalizePickerState(state);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseBannerLines(const char *lineStarts[MAX_LINES + 1], uint16_t &lineCount)
|
||||||
|
{
|
||||||
|
lineCount = 0;
|
||||||
|
char *alertEnd = NotificationRenderer::alertBannerMessage +
|
||||||
|
strnlen(NotificationRenderer::alertBannerMessage, sizeof(NotificationRenderer::alertBannerMessage));
|
||||||
|
lineStarts[lineCount] = NotificationRenderer::alertBannerMessage;
|
||||||
|
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||||
|
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||||
|
if (lineStarts[lineCount + 1][0] == '\n') {
|
||||||
|
lineStarts[lineCount + 1] += 1;
|
||||||
|
}
|
||||||
|
lineCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void buildNumericPickerDisplay(const NumericSlotPickerState &state, std::string &formattedValue,
|
||||||
|
std::vector<int16_t> &slotCharIndexBySlot)
|
||||||
|
{
|
||||||
|
const uint8_t slotCount = pickerSlotCount(state);
|
||||||
|
slotCharIndexBySlot.assign(slotCount, -1);
|
||||||
|
|
||||||
|
formattedValue = " ";
|
||||||
|
formattedValue.reserve(24);
|
||||||
|
int8_t slot = 0;
|
||||||
|
|
||||||
|
if (state.hasSign) {
|
||||||
|
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
|
||||||
|
formattedValue += state.isNegative ? '-' : '+';
|
||||||
|
formattedValue += ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t digitCount = state.digits.size();
|
||||||
|
const size_t decimalBreak =
|
||||||
|
(state.decimalDigits > 0 && state.decimalDigits < digitCount) ? (digitCount - state.decimalDigits) : digitCount;
|
||||||
|
for (size_t i = 0; i < digitCount; i++) {
|
||||||
|
slotCharIndexBySlot[slot++] = static_cast<int16_t>(formattedValue.size());
|
||||||
|
formattedValue += static_cast<char>('0' + state.digits[i]);
|
||||||
|
formattedValue += ' ';
|
||||||
|
|
||||||
|
if (state.decimalDigits > 0 && i + 1 == decimalBreak) {
|
||||||
|
formattedValue += '.';
|
||||||
|
formattedValue += ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number picker renderer
|
||||||
|
// Reuse by building `formattedValue` as displayed, and mapping each editable slot
|
||||||
|
// to its character index in that string via `slotCharIndexBySlot`.
|
||||||
|
void NumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lineStarts[MAX_LINES + 1], uint16_t lineCount,
|
||||||
|
const std::string &formattedValue, const int16_t *slotCharIndexBySlot, uint8_t slotCount, int8_t selectedSlot)
|
||||||
|
{
|
||||||
|
if (formattedValue.empty() || slotCharIndexBySlot == nullptr || slotCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string spacer(formattedValue.size(), ' ');
|
||||||
|
uint16_t totalLines = lineCount + 3;
|
||||||
|
const char *linePointers[totalLines + 1] = {0};
|
||||||
|
for (uint16_t i = 0; i < lineCount; i++) {
|
||||||
|
linePointers[i] = lineStarts[i];
|
||||||
|
}
|
||||||
|
const uint16_t topGuideLineIndex = lineCount;
|
||||||
|
linePointers[lineCount++] = spacer.c_str();
|
||||||
|
const uint16_t valueLineIndex = lineCount;
|
||||||
|
linePointers[lineCount++] = formattedValue.c_str();
|
||||||
|
const uint16_t bottomGuideLineIndex = lineCount;
|
||||||
|
linePointers[lineCount++] = spacer.c_str();
|
||||||
|
|
||||||
|
NotificationRenderer::drawNotificationBox(display, state, linePointers, totalLines, 0);
|
||||||
|
|
||||||
|
constexpr uint16_t hPadding = 5;
|
||||||
|
constexpr uint16_t vPadding = 2;
|
||||||
|
uint16_t maxWidth = 0;
|
||||||
|
uint16_t lineWidths[MAX_LINES + 3] = {0};
|
||||||
|
for (uint16_t i = 0; i < totalLines; i++) {
|
||||||
|
const uint16_t lineLength = static_cast<uint16_t>(strlen(linePointers[i]));
|
||||||
|
lineWidths[i] = display->getStringWidth(linePointers[i], lineLength, true);
|
||||||
|
if (lineWidths[i] > maxWidth) {
|
||||||
|
maxWidth = lineWidths[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||||
|
uint16_t screenHeight = display->height();
|
||||||
|
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||||
|
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||||
|
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||||
|
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||||
|
if (visibleTotalLines == 1) {
|
||||||
|
boxHeight += (currentResolution == ScreenResolution::High) ? 4 : 3;
|
||||||
|
}
|
||||||
|
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||||
|
if (totalLines > visibleTotalLines) {
|
||||||
|
boxWidth += (currentResolution == ScreenResolution::High) ? 4 : 2;
|
||||||
|
}
|
||||||
|
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||||
|
|
||||||
|
const int selectedSlotClamped = std::max<int>(0, std::min<int>(selectedSlot, static_cast<int>(slotCount) - 1));
|
||||||
|
const int selectedCharIndex = slotCharIndexBySlot[selectedSlotClamped];
|
||||||
|
if (selectedCharIndex < 0 || selectedCharIndex >= static_cast<int>(formattedValue.size())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t valueTextX = boxLeft + (boxWidth - lineWidths[valueLineIndex]) / 2;
|
||||||
|
const uint16_t prefixWidth = display->getStringWidth(formattedValue.c_str(), selectedCharIndex, true);
|
||||||
|
const uint16_t slotCharWidth = display->getStringWidth(formattedValue.c_str() + selectedCharIndex, 1, true);
|
||||||
|
const int16_t slotCenterX = valueTextX + static_cast<int16_t>(prefixWidth + (slotCharWidth / 2));
|
||||||
|
|
||||||
|
int16_t topGuideY = boxTop + vPadding + (topGuideLineIndex * effectiveLineHeight);
|
||||||
|
int16_t bottomGuideY = boxTop + vPadding + (bottomGuideLineIndex * effectiveLineHeight);
|
||||||
|
const int16_t triHalfWidth = (currentResolution == ScreenResolution::High) ? 3 : 2;
|
||||||
|
const int16_t guideInsetY = 1;
|
||||||
|
const int16_t triHeight = std::max<int16_t>(2, static_cast<int16_t>((effectiveLineHeight - (guideInsetY * 2) - 1) / 2));
|
||||||
|
const int16_t topBaseY = topGuideY + effectiveLineHeight - guideInsetY - 1;
|
||||||
|
const int16_t topApexY = topBaseY - triHeight;
|
||||||
|
const int16_t bottomBaseY = bottomGuideY + guideInsetY;
|
||||||
|
const int16_t bottomApexY = bottomBaseY + triHeight;
|
||||||
|
|
||||||
|
display->setColor(WHITE);
|
||||||
|
display->fillTriangle(slotCenterX, topApexY, slotCenterX - triHalfWidth, topBaseY, slotCenterX + triHalfWidth, topBaseY);
|
||||||
|
display->fillTriangle(slotCenterX - triHalfWidth, bottomBaseY, slotCenterX + triHalfWidth, bottomBaseY, slotCenterX,
|
||||||
|
bottomApexY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used on boot when a certificate is being created
|
// Used on boot when a certificate is being created
|
||||||
@@ -103,6 +380,10 @@ void NotificationRenderer::resetBanner()
|
|||||||
pauseBanner = false;
|
pauseBanner = false;
|
||||||
numDigits = 0;
|
numDigits = 0;
|
||||||
currentNumber = 0;
|
currentNumber = 0;
|
||||||
|
signedDecimalValueTenths = 0;
|
||||||
|
signedDecimalMinTenths = -999;
|
||||||
|
signedDecimalMaxTenths = 999;
|
||||||
|
signedDecimalIsNegative = false;
|
||||||
|
|
||||||
nodeDB->pause_sort(false);
|
nodeDB->pause_sort(false);
|
||||||
|
|
||||||
@@ -153,6 +434,9 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
|||||||
case notificationTypeEnum::number_picker:
|
case notificationTypeEnum::number_picker:
|
||||||
drawNumberPicker(display, state);
|
drawNumberPicker(display, state);
|
||||||
break;
|
break;
|
||||||
|
case notificationTypeEnum::signed_decimal_picker:
|
||||||
|
drawSignedDecimalPicker(display, state);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,83 +444,132 @@ void NotificationRenderer::drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiS
|
|||||||
{
|
{
|
||||||
const char *lineStarts[MAX_LINES + 1] = {0};
|
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||||
uint16_t lineCount = 0;
|
uint16_t lineCount = 0;
|
||||||
|
parseBannerLines(lineStarts, lineCount);
|
||||||
|
|
||||||
// Parse lines
|
NumericSlotPickerState pickerState;
|
||||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
pickerState.hasSign = false;
|
||||||
lineStarts[lineCount] = alertBannerMessage;
|
pickerState.decimalDigits = 0;
|
||||||
|
pickerState.minValue = 0;
|
||||||
|
pickerState.maxValue = maxValueForDigits(static_cast<uint8_t>(numDigits));
|
||||||
|
pickerState.digits.assign(numDigits, 0);
|
||||||
|
splitPickerDigits(currentNumber, pickerState.digits);
|
||||||
|
normalizePickerState(pickerState);
|
||||||
|
|
||||||
// Find lines
|
const uint8_t slotCount = pickerSlotCount(pickerState);
|
||||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
if (curSelected < 0) {
|
||||||
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
curSelected = 0;
|
||||||
if (lineStarts[lineCount + 1][0] == '\n')
|
} else if (curSelected > static_cast<int8_t>(slotCount)) {
|
||||||
lineStarts[lineCount + 1] += 1;
|
curSelected = static_cast<int8_t>(slotCount);
|
||||||
lineCount++;
|
|
||||||
}
|
}
|
||||||
// modulo to extract
|
|
||||||
uint8_t this_digit = (currentNumber % (pow_of_10(numDigits - curSelected))) / (pow_of_10(numDigits - curSelected - 1));
|
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
||||||
// Handle input
|
resetBanner();
|
||||||
if (inEvent.inputEvent == INPUT_BROKER_UP || inEvent.inputEvent == INPUT_BROKER_ALT_PRESS ||
|
return;
|
||||||
inEvent.inputEvent == INPUT_BROKER_UP_LONG) {
|
}
|
||||||
if (this_digit == 9) {
|
|
||||||
currentNumber -= 9 * (pow_of_10(numDigits - curSelected - 1));
|
if (isPickerIncrementEvent(inEvent.inputEvent)) {
|
||||||
} else {
|
applyPickerDelta(pickerState, curSelected, 1);
|
||||||
currentNumber += (pow_of_10(numDigits - curSelected - 1));
|
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
|
||||||
}
|
applyPickerDelta(pickerState, curSelected, -1);
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_DOWN || inEvent.inputEvent == INPUT_BROKER_USER_PRESS ||
|
|
||||||
inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) {
|
|
||||||
if (this_digit == 0) {
|
|
||||||
currentNumber += 9 * (pow_of_10(numDigits - curSelected - 1));
|
|
||||||
} else {
|
|
||||||
currentNumber -= (pow_of_10(numDigits - curSelected - 1));
|
|
||||||
}
|
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||||
if (inEvent.kbchar > 47 && inEvent.kbchar < 58) { // have a digit
|
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
|
||||||
currentNumber -= this_digit * (pow_of_10(numDigits - curSelected - 1));
|
|
||||||
currentNumber += (inEvent.kbchar - 48) * (pow_of_10(numDigits - curSelected - 1));
|
|
||||||
curSelected++;
|
curSelected++;
|
||||||
}
|
}
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||||
curSelected++;
|
curSelected++;
|
||||||
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||||
curSelected--;
|
curSelected = std::max<int8_t>(0, curSelected - 1);
|
||||||
} else if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) &&
|
|
||||||
alertBannerUntil != 0) {
|
|
||||||
resetBanner();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (curSelected == static_cast<int8_t>(numDigits)) {
|
|
||||||
|
currentNumber = static_cast<uint32_t>(std::max<int32_t>(0, composePickerValue(pickerState)));
|
||||||
|
if (curSelected == static_cast<int8_t>(slotCount)) {
|
||||||
alertBannerCallback(currentNumber);
|
alertBannerCallback(currentNumber);
|
||||||
resetBanner();
|
resetBanner();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
inEvent.inputEvent = INPUT_BROKER_NONE;
|
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||||
if (alertBannerMessage[0] == '\0')
|
if (alertBannerMessage[0] == '\0') {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
uint16_t totalLines = lineCount + 2;
|
|
||||||
const char *linePointers[totalLines + 1] = {0}; // this is sort of a dynamic allocation
|
|
||||||
|
|
||||||
// copy the linestarts to display to the linePointers holder
|
|
||||||
for (uint16_t i = 0; i < lineCount; i++) {
|
|
||||||
linePointers[i] = lineStarts[i];
|
|
||||||
}
|
}
|
||||||
std::string digits = " ";
|
|
||||||
std::string arrowPointer = " ";
|
std::string formattedValue;
|
||||||
for (uint16_t i = 0; i < numDigits; i++) {
|
std::vector<int16_t> slotCharIndexBySlot;
|
||||||
// Modulo minus modulo to return just the current number
|
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
|
||||||
digits += std::to_string((currentNumber % (pow_of_10(numDigits - i))) / (pow_of_10(numDigits - i - 1))) + " ";
|
|
||||||
if (curSelected == i) {
|
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
|
||||||
arrowPointer += "^ ";
|
}
|
||||||
} else {
|
|
||||||
arrowPointer += "_ ";
|
void NotificationRenderer::drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
|
{
|
||||||
|
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||||
|
uint16_t lineCount = 0;
|
||||||
|
parseBannerLines(lineStarts, lineCount);
|
||||||
|
|
||||||
|
NumericSlotPickerState pickerState;
|
||||||
|
pickerState.hasSign = true;
|
||||||
|
pickerState.decimalDigits = 1;
|
||||||
|
pickerState.minValue = signedDecimalMinTenths;
|
||||||
|
pickerState.maxValue = signedDecimalMaxTenths;
|
||||||
|
pickerState.digits.assign(3, 0); // XX.X format
|
||||||
|
|
||||||
|
const int32_t currentValue = static_cast<int32_t>(signedDecimalValueTenths);
|
||||||
|
if (currentValue < 0) {
|
||||||
|
pickerState.isNegative = true;
|
||||||
|
} else if (currentValue > 0) {
|
||||||
|
pickerState.isNegative = false;
|
||||||
|
} else {
|
||||||
|
pickerState.isNegative = signedDecimalIsNegative;
|
||||||
|
}
|
||||||
|
const uint32_t absValue =
|
||||||
|
(currentValue < 0) ? static_cast<uint32_t>(-static_cast<int64_t>(currentValue)) : static_cast<uint32_t>(currentValue);
|
||||||
|
splitPickerDigits(absValue, pickerState.digits);
|
||||||
|
normalizePickerState(pickerState);
|
||||||
|
|
||||||
|
const uint8_t slotCount = pickerSlotCount(pickerState);
|
||||||
|
if (curSelected < 0) {
|
||||||
|
curSelected = 0;
|
||||||
|
} else if (curSelected > static_cast<int8_t>(slotCount)) {
|
||||||
|
curSelected = static_cast<int8_t>(slotCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((inEvent.inputEvent == INPUT_BROKER_CANCEL || inEvent.inputEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
||||||
|
resetBanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPickerIncrementEvent(inEvent.inputEvent)) {
|
||||||
|
applyPickerDelta(pickerState, curSelected, 1);
|
||||||
|
} else if (isPickerDecrementEvent(inEvent.inputEvent)) {
|
||||||
|
applyPickerDelta(pickerState, curSelected, -1);
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_ANYKEY) {
|
||||||
|
if (applyPickerKeypress(pickerState, curSelected, inEvent.kbchar)) {
|
||||||
|
curSelected++;
|
||||||
}
|
}
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_SELECT || inEvent.inputEvent == INPUT_BROKER_RIGHT) {
|
||||||
|
curSelected++;
|
||||||
|
} else if (inEvent.inputEvent == INPUT_BROKER_LEFT) {
|
||||||
|
curSelected = std::max<int8_t>(0, curSelected - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
linePointers[lineCount++] = digits.c_str();
|
signedDecimalValueTenths = static_cast<int16_t>(composePickerValue(pickerState));
|
||||||
linePointers[lineCount++] = arrowPointer.c_str();
|
signedDecimalIsNegative = pickerState.isNegative;
|
||||||
|
if (curSelected == static_cast<int8_t>(slotCount)) {
|
||||||
|
alertBannerCallback(signedDecimalValueTenths);
|
||||||
|
resetBanner();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
drawNotificationBox(display, state, linePointers, totalLines, 0);
|
inEvent.inputEvent = INPUT_BROKER_NONE;
|
||||||
|
if (alertBannerMessage[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string formattedValue;
|
||||||
|
std::vector<int16_t> slotCharIndexBySlot;
|
||||||
|
buildNumericPickerDisplay(pickerState, formattedValue, slotCharIndexBySlot);
|
||||||
|
|
||||||
|
NumberPicker(display, state, lineStarts, lineCount, formattedValue, slotCharIndexBySlot.data(), slotCount, curSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
@@ -622,7 +955,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
|||||||
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
strncpy(lineBuffer, lines[i], lineLengths[i]);
|
||||||
lineBuffer[lineLengths[i]] = '\0';
|
lineBuffer[lineLengths[i]] = '\0';
|
||||||
// Determine if this is a pop-up or a pick list
|
// Determine if this is a pop-up or a pick list
|
||||||
if (alertBannerOptions > 0 && i == 0) {
|
const bool highlightTitleRow =
|
||||||
|
(i == 0) && (alertBannerOptions > 0 || current_notification_type == notificationTypeEnum::signed_decimal_picker);
|
||||||
|
if (highlightTitleRow) {
|
||||||
// Pick List
|
// Pick List
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
int background_yOffset = 1;
|
int background_yOffset = 1;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "graphics/Screen.h"
|
#include "graphics/Screen.h"
|
||||||
#include "graphics/VirtualKeyboard.h"
|
#include "graphics/VirtualKeyboard.h"
|
||||||
#include "modules/OnScreenKeyboardModule.h"
|
#include "modules/OnScreenKeyboardModule.h"
|
||||||
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#define MAX_LINES 5
|
#define MAX_LINES 5
|
||||||
@@ -26,6 +27,10 @@ class NotificationRenderer
|
|||||||
static std::function<void(int)> alertBannerCallback;
|
static std::function<void(int)> alertBannerCallback;
|
||||||
static uint32_t numDigits;
|
static uint32_t numDigits;
|
||||||
static uint32_t currentNumber;
|
static uint32_t currentNumber;
|
||||||
|
static int16_t signedDecimalValueTenths;
|
||||||
|
static int16_t signedDecimalMinTenths;
|
||||||
|
static int16_t signedDecimalMaxTenths;
|
||||||
|
static bool signedDecimalIsNegative;
|
||||||
static VirtualKeyboard *virtualKeyboard;
|
static VirtualKeyboard *virtualKeyboard;
|
||||||
static std::function<void(const std::string &)> textInputCallback;
|
static std::function<void(const std::string &)> textInputCallback;
|
||||||
|
|
||||||
@@ -36,6 +41,7 @@ class NotificationRenderer
|
|||||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
static void drawSignedDecimalPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ class MeshModule
|
|||||||
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
||||||
meshtastic_AdminMessage *request,
|
meshtastic_AdminMessage *request,
|
||||||
meshtastic_AdminMessage *response);
|
meshtastic_AdminMessage *response);
|
||||||
|
const char *getName() const { return name; }
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; }
|
virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; }
|
||||||
virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset
|
virtual bool isRequestingFocus(); // Checked by screen, when regenerating frameset
|
||||||
|
|||||||
@@ -398,6 +398,8 @@ typedef struct _meshtastic_ModuleConfig_TelemetryConfig {
|
|||||||
bool device_telemetry_enabled;
|
bool device_telemetry_enabled;
|
||||||
/* Enable/Disable the air quality telemetry measurement module on-device display */
|
/* Enable/Disable the air quality telemetry measurement module on-device display */
|
||||||
bool air_quality_screen_enabled;
|
bool air_quality_screen_enabled;
|
||||||
|
/* Temperature offset in Celsius applied to local environment telemetry before it is sent. */
|
||||||
|
float environment_temperature_offset_c;
|
||||||
} meshtastic_ModuleConfig_TelemetryConfig;
|
} meshtastic_ModuleConfig_TelemetryConfig;
|
||||||
|
|
||||||
/* Canned Messages Module Config */
|
/* Canned Messages Module Config */
|
||||||
@@ -593,7 +595,7 @@ extern "C" {
|
|||||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_StoreForwardConfig_init_default {0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
#define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
||||||
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""}
|
#define meshtastic_ModuleConfig_StatusMessageConfig_init_default {""}
|
||||||
@@ -612,7 +614,7 @@ extern "C" {
|
|||||||
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_ExternalNotificationConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_StoreForwardConfig_init_zero {0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
#define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0}
|
||||||
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0}
|
#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0}
|
||||||
#define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""}
|
#define meshtastic_ModuleConfig_StatusMessageConfig_init_zero {""}
|
||||||
@@ -718,6 +720,7 @@ extern "C" {
|
|||||||
#define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13
|
#define meshtastic_ModuleConfig_TelemetryConfig_health_screen_enabled_tag 13
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14
|
#define meshtastic_ModuleConfig_TelemetryConfig_device_telemetry_enabled_tag 14
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15
|
#define meshtastic_ModuleConfig_TelemetryConfig_air_quality_screen_enabled_tag 15
|
||||||
|
#define meshtastic_ModuleConfig_TelemetryConfig_environment_temperature_offset_c_tag 16
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1
|
#define meshtastic_ModuleConfig_CannedMessageConfig_rotary1_enabled_tag 1
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2
|
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_a_tag 2
|
||||||
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3
|
#define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_pin_b_tag 3
|
||||||
@@ -948,7 +951,8 @@ X(a, STATIC, SINGULAR, BOOL, health_measurement_enabled, 11) \
|
|||||||
X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \
|
X(a, STATIC, SINGULAR, UINT32, health_update_interval, 12) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \
|
X(a, STATIC, SINGULAR, BOOL, health_screen_enabled, 13) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \
|
X(a, STATIC, SINGULAR, BOOL, device_telemetry_enabled, 14) \
|
||||||
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15)
|
X(a, STATIC, SINGULAR, BOOL, air_quality_screen_enabled, 15) \
|
||||||
|
X(a, STATIC, SINGULAR, FLOAT, environment_temperature_offset_c, 16)
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL
|
#define meshtastic_ModuleConfig_TelemetryConfig_CALLBACK NULL
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL
|
#define meshtastic_ModuleConfig_TelemetryConfig_DEFAULT NULL
|
||||||
|
|
||||||
@@ -1051,8 +1055,8 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg;
|
|||||||
#define meshtastic_ModuleConfig_SerialConfig_size 28
|
#define meshtastic_ModuleConfig_SerialConfig_size 28
|
||||||
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
|
#define meshtastic_ModuleConfig_StatusMessageConfig_size 81
|
||||||
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
|
#define meshtastic_ModuleConfig_StoreForwardConfig_size 24
|
||||||
|
#define meshtastic_ModuleConfig_TelemetryConfig_size 55
|
||||||
#define meshtastic_ModuleConfig_TAKConfig_size 4
|
#define meshtastic_ModuleConfig_TAKConfig_size 4
|
||||||
#define meshtastic_ModuleConfig_TelemetryConfig_size 50
|
|
||||||
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
|
#define meshtastic_ModuleConfig_TrafficManagementConfig_size 52
|
||||||
#define meshtastic_ModuleConfig_size 227
|
#define meshtastic_ModuleConfig_size 227
|
||||||
#define meshtastic_RemoteHardwarePin_size 21
|
#define meshtastic_RemoteHardwarePin_size 21
|
||||||
|
|||||||
@@ -138,6 +138,32 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
|
|||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr float TEMPERATURE_OFFSET_MIN_C = -20.0f;
|
||||||
|
constexpr float TEMPERATURE_OFFSET_MAX_C = 20.0f;
|
||||||
|
|
||||||
|
float clampTemperatureOffsetC(float offsetC)
|
||||||
|
{
|
||||||
|
if (offsetC < TEMPERATURE_OFFSET_MIN_C)
|
||||||
|
return TEMPERATURE_OFFSET_MIN_C;
|
||||||
|
if (offsetC > TEMPERATURE_OFFSET_MAX_C)
|
||||||
|
return TEMPERATURE_OFFSET_MAX_C;
|
||||||
|
return offsetC;
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyTemperatureOffset(meshtastic_EnvironmentMetrics *metrics)
|
||||||
|
{
|
||||||
|
const float offsetC = clampTemperatureOffsetC(moduleConfig.telemetry.environment_temperature_offset_c);
|
||||||
|
if (offsetC == 0.0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (metrics->has_temperature)
|
||||||
|
metrics->temperature += offsetC;
|
||||||
|
if (metrics->has_soil_temperature)
|
||||||
|
metrics->soil_temperature += offsetC;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
|
static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002;
|
||||||
|
|
||||||
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||||
@@ -261,6 +287,29 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
return disable();
|
return disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto refreshLocalMeasurementPacket = [this]() {
|
||||||
|
// Keep displaying remote telemetry once we have it.
|
||||||
|
if (lastMeasurementPacket != nullptr && lastMeasurementPacket->from != nodeDB->getNodeNum()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_Telemetry local = meshtastic_Telemetry_init_zero;
|
||||||
|
if (!getEnvironmentTelemetry(&local)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
meshtastic_MeshPacket *localPacket = allocDataProtobuf(local);
|
||||||
|
if (localPacket == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastMeasurementPacket != nullptr) {
|
||||||
|
packetPool.release(lastMeasurementPacket);
|
||||||
|
}
|
||||||
|
lastMeasurementPacket = packetPool.allocCopy(*localPacket);
|
||||||
|
packetPool.release(localPacket);
|
||||||
|
};
|
||||||
|
|
||||||
if (firstTime) {
|
if (firstTime) {
|
||||||
// This is the first time the OSThread library has called this function, so do some setup
|
// This is the first time the OSThread library has called this function, so do some setup
|
||||||
firstTime = 0;
|
firstTime = 0;
|
||||||
@@ -290,6 +339,7 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
result = rak9154Sensor.runOnce();
|
result = rak9154Sensor.runOnce();
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
refreshLocalMeasurementPacket();
|
||||||
}
|
}
|
||||||
// it's possible to have this module enabled, only for displaying values on the screen.
|
// it's possible to have this module enabled, only for displaying values on the screen.
|
||||||
// therefore, we should only enable the sensor loop if measurement is also enabled
|
// therefore, we should only enable the sensor loop if measurement is also enabled
|
||||||
@@ -306,6 +356,7 @@ int32_t EnvironmentTelemetryModule::runOnce()
|
|||||||
result = delay;
|
result = delay;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refreshLocalMeasurementPacket();
|
||||||
|
|
||||||
uint32_t lastTelemetry =
|
uint32_t lastTelemetry =
|
||||||
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
|
transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0;
|
||||||
@@ -566,6 +617,9 @@ bool EnvironmentTelemetryModule::getEnvironmentTelemetry(meshtastic_Telemetry *m
|
|||||||
hasSensor = true;
|
hasSensor = true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
if (valid)
|
||||||
|
applyTemperatureOffset(&m->variant.environment_metrics);
|
||||||
|
|
||||||
return valid && hasSensor;
|
return valid && hasSensor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,11 +684,15 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly)
|
|||||||
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
p->priority = meshtastic_MeshPacket_Priority_RELIABLE;
|
||||||
else
|
else
|
||||||
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
p->priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||||
// release previous packet before occupying a new spot
|
const bool shouldReplaceDisplayPacket =
|
||||||
if (lastMeasurementPacket != nullptr)
|
(lastMeasurementPacket == nullptr || lastMeasurementPacket->from == nodeDB->getNodeNum());
|
||||||
packetPool.release(lastMeasurementPacket);
|
if (shouldReplaceDisplayPacket) {
|
||||||
|
// release previous packet before occupying a new spot
|
||||||
|
if (lastMeasurementPacket != nullptr)
|
||||||
|
packetPool.release(lastMeasurementPacket);
|
||||||
|
|
||||||
lastMeasurementPacket = packetPool.allocCopy(*p);
|
lastMeasurementPacket = packetPool.allocCopy(*p);
|
||||||
|
}
|
||||||
if (phoneOnly) {
|
if (phoneOnly) {
|
||||||
LOG_INFO("Send packet to phone");
|
LOG_INFO("Send packet to phone");
|
||||||
service->sendToPhone(p);
|
service->sendToPhone(p);
|
||||||
|
|||||||
Reference in New Issue
Block a user