mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-12 04:47:23 +00:00
Compare commits
11 Commits
bme680
...
TinyFontTe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89a35e302d | ||
|
|
d18f3f7a65 | ||
|
|
6c09cf9d3d | ||
|
|
79a91578b7 | ||
|
|
17cd83085b | ||
|
|
b7bdcbe43e | ||
|
|
df063f40ff | ||
|
|
84bb1e33a6 | ||
|
|
955347bf50 | ||
|
|
4284fc2aec | ||
|
|
034aaa376a |
@@ -33,8 +33,6 @@ lib_deps =
|
||||
adafruit/Adafruit seesaw Library@1.7.9
|
||||
# renovate: datasource=git-refs depName=RAK12034-BMX160 packageName=https://github.com/RAKWireless/RAK12034-BMX160 gitBranch=main
|
||||
https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip
|
||||
# renovate: datasource=custom.pio depName=adafruit/Adafruit BME680 Library packageName=adafruit/library/Adafruit BME680
|
||||
adafruit/Adafruit BME680 Library@^2.0.5
|
||||
|
||||
build_flags =
|
||||
${arduino_base.build_flags}
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.15" date="2025-11-13">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.15</url>
|
||||
</release>
|
||||
<release version="2.7.14" date="2025-11-03">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.14</url>
|
||||
</release>
|
||||
|
||||
6
debian/changelog
vendored
6
debian/changelog
vendored
@@ -1,3 +1,9 @@
|
||||
meshtasticd (2.7.15.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.15
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Thu, 13 Nov 2025 12:31:57 +0000
|
||||
|
||||
meshtasticd (2.7.14.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.14
|
||||
|
||||
@@ -194,7 +194,7 @@ static HasBatteryLevel *batteryLevel; // Default to NULL for no battery level se
|
||||
|
||||
#ifdef BATTERY_PIN
|
||||
|
||||
static void adcEnable()
|
||||
void battery_adcEnable()
|
||||
{
|
||||
#ifdef ADC_CTRL // enable adc voltage divider when we need to read
|
||||
#ifdef ADC_USE_PULLUP
|
||||
@@ -214,7 +214,7 @@ static void adcEnable()
|
||||
#endif
|
||||
}
|
||||
|
||||
static void adcDisable()
|
||||
static void battery_adcDisable()
|
||||
{
|
||||
#ifdef ADC_CTRL // disable adc voltage divider when we need to read
|
||||
#ifdef ADC_USE_PULLUP
|
||||
@@ -320,7 +320,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
uint32_t raw = 0;
|
||||
float scaled = 0;
|
||||
|
||||
adcEnable();
|
||||
battery_adcEnable();
|
||||
#ifdef ARCH_ESP32 // ADC block for espressif platforms
|
||||
raw = espAdcRead();
|
||||
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
|
||||
@@ -332,7 +332,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
raw = raw / BATTERY_SENSE_SAMPLES;
|
||||
scaled = operativeAdcMultiplier * ((1000 * AREF_VOLTAGE) / pow(2, BATTERY_SENSE_RESOLUTION_BITS)) * raw;
|
||||
#endif
|
||||
adcDisable();
|
||||
battery_adcDisable();
|
||||
|
||||
if (!initial_read_done) {
|
||||
// Flush the smoothing filter with an ADC reading, if the reading is plausibly correct
|
||||
@@ -906,13 +906,8 @@ void Power::readPowerStatus()
|
||||
low_voltage_counter++;
|
||||
LOG_DEBUG("Low voltage counter: %d/10", low_voltage_counter);
|
||||
if (low_voltage_counter > 10) {
|
||||
#ifdef ARCH_NRF52
|
||||
// We can't trigger deep sleep on NRF52, it's freezing the board
|
||||
LOG_DEBUG("Low voltage detected, but not trigger deep sleep");
|
||||
#else
|
||||
LOG_INFO("Low voltage detected, trigger deep sleep");
|
||||
powerFSM.trigger(EVENT_LOW_BATTERY);
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
low_voltage_counter = 0;
|
||||
@@ -1552,4 +1547,4 @@ bool Power::meshSolarInit()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -410,18 +410,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// BME680 BSEC2 support detection
|
||||
#if !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
||||
#if defined(RAK_4631) || defined(TBEAM_V10)
|
||||
|
||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 1
|
||||
#define MESHTASTIC_BME680_HEADER <bsec2.h>
|
||||
#else
|
||||
#define MESHTASTIC_BME680_BSEC2_SUPPORTED 0
|
||||
#define MESHTASTIC_BME680_HEADER <Adafruit_BME680.h>
|
||||
#endif // defined(RAK_4631)
|
||||
#endif // !defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Global switches to turn off features for a minimized build
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -240,6 +240,9 @@ GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis)
|
||||
buffer[bytesRead] = b;
|
||||
bytesRead++;
|
||||
if ((bytesRead == 767) || (b == '\r')) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(debugmsg.c_str());
|
||||
#endif
|
||||
if (strnstr((char *)buffer, message, bytesRead) != nullptr) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG("Found: %s", message); // Log the found message
|
||||
@@ -247,9 +250,6 @@ GPS_RESPONSE GPS::getACK(const char *message, uint32_t waitMillis)
|
||||
return GNSS_RESPONSE_OK;
|
||||
} else {
|
||||
bytesRead = 0;
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(debugmsg.c_str());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1275,6 +1275,24 @@ GnssModel_t GPS::probe(int serialSpeed)
|
||||
memset(&ublox_info, 0, sizeof(ublox_info));
|
||||
delay(100);
|
||||
|
||||
#if defined(PIN_GPS_RESET) && PIN_GPS_RESET != -1
|
||||
digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms
|
||||
delay(10);
|
||||
digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE);
|
||||
|
||||
// attempt to detect the chip based on boot messages
|
||||
std::vector<ChipInfo> passive_detect = {
|
||||
{"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335},
|
||||
{"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352},
|
||||
{"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352},
|
||||
{"UC6580", "UC6580", GNSS_MODEL_UC6580},
|
||||
// as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now.
|
||||
/*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/};
|
||||
GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed);
|
||||
if (detectedDriver != GNSS_MODEL_UNKNOWN) {
|
||||
return detectedDriver;
|
||||
}
|
||||
#endif
|
||||
// Close all NMEA sentences, valid for L76K, ATGM336H (and likely other AT6558 devices)
|
||||
_serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n");
|
||||
delay(20);
|
||||
@@ -1473,12 +1491,12 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
}
|
||||
|
||||
if (c == ',' || (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n')) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
// check if we can see our chips
|
||||
for (const auto &chipInfo : responseMap) {
|
||||
if (strstr(response, chipInfo.detectionString.c_str()) != nullptr) {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
LOG_INFO("%s detected", chipInfo.chipName.c_str());
|
||||
delete[] response; // Cleanup before return
|
||||
return chipInfo.driver;
|
||||
@@ -1486,6 +1504,9 @@ GnssModel_t GPS::getProbeResponse(unsigned long timeout, const std::vector<ChipI
|
||||
}
|
||||
}
|
||||
if (responseLen >= 2 && response[responseLen - 2] == '\r' && response[responseLen - 1] == '\n') {
|
||||
#ifdef GPS_DEBUG
|
||||
LOG_DEBUG(response);
|
||||
#endif
|
||||
// Reset the response buffer for the next potential message
|
||||
responseLen = 0;
|
||||
response[0] = '\0';
|
||||
@@ -1572,8 +1593,6 @@ GPS *GPS::createGps()
|
||||
|
||||
#ifdef PIN_GPS_RESET
|
||||
pinMode(PIN_GPS_RESET, OUTPUT);
|
||||
digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms
|
||||
delay(10);
|
||||
digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
|
||||
#ifdef BUILD_EPOCH
|
||||
if (tv.tv_sec < BUILD_EPOCH) {
|
||||
if (Throttle::isWithinTimespanMs(lastTimeValidationWarning, TIME_VALIDATION_WARNING_INTERVAL_MS) == false) {
|
||||
LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH);
|
||||
LOG_WARN("Ignore time (%lu) before build epoch (%lu)!", printableEpoch, BUILD_EPOCH);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
return RTCSetResultInvalidTime;
|
||||
@@ -319,7 +319,7 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t)
|
||||
// Calculate max allowed time safely to avoid overflow in logging
|
||||
uint64_t maxAllowedTime = (uint64_t)BUILD_EPOCH + FORTY_YEARS;
|
||||
uint32_t maxAllowedPrintable = (maxAllowedTime > UINT32_MAX) ? UINT32_MAX : (uint32_t)maxAllowedTime;
|
||||
LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch,
|
||||
LOG_WARN("Ignore time (%lu) too far in the future (build epoch: %lu, max allowed: %lu)!", printableEpoch,
|
||||
(uint32_t)BUILD_EPOCH, maxAllowedPrintable);
|
||||
lastTimeValidationWarning = millis();
|
||||
}
|
||||
|
||||
@@ -103,3 +103,44 @@
|
||||
#define FONT_HEIGHT_SMALL _fontHeight(FONT_SMALL)
|
||||
#define FONT_HEIGHT_MEDIUM _fontHeight(FONT_MEDIUM)
|
||||
#define FONT_HEIGHT_LARGE _fontHeight(FONT_LARGE)
|
||||
|
||||
// ============================================================================
|
||||
// FINAL OVERRIDE: Force TomThumb font
|
||||
// ============================================================================
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
|
||||
#include "graphics/fonts/OLEDDisplayFontsTomThumb.h"
|
||||
|
||||
// -----------------------------
|
||||
// Replace all Meshtastic fonts
|
||||
// -----------------------------
|
||||
#undef FONT_SMALL_LOCAL
|
||||
#undef FONT_MEDIUM_LOCAL
|
||||
#undef FONT_LARGE_LOCAL
|
||||
|
||||
#undef FONT_SMALL
|
||||
#undef FONT_MEDIUM
|
||||
#undef FONT_LARGE
|
||||
|
||||
#define FONT_SMALL_LOCAL TomThumb4x6
|
||||
#define FONT_MEDIUM_LOCAL TomThumb4x6
|
||||
#define FONT_LARGE_LOCAL TomThumb4x6
|
||||
|
||||
#define FONT_SMALL TomThumb4x6
|
||||
#define FONT_MEDIUM TomThumb4x6
|
||||
#define FONT_LARGE TomThumb4x6
|
||||
|
||||
// -------------------------------------------------------
|
||||
// Override the *line height used for spacing*, NOT glyphs
|
||||
// TomThumb is 6 px tall → we give it 11 px layout height
|
||||
// -------------------------------------------------------
|
||||
#undef FONT_HEIGHT_SMALL
|
||||
#undef FONT_HEIGHT_MEDIUM
|
||||
#undef FONT_HEIGHT_LARGE
|
||||
|
||||
#define FONT_HEIGHT_SMALL 6
|
||||
#define FONT_HEIGHT_MEDIUM 6
|
||||
#define FONT_HEIGHT_LARGE 6
|
||||
|
||||
#endif
|
||||
// ============================================================================
|
||||
@@ -380,6 +380,17 @@ const int *getTextPositions(OLEDDisplay *display)
|
||||
{
|
||||
static int textPositions[7]; // Static array that persists beyond function scope
|
||||
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
textPositions[0] = textZeroLine;
|
||||
textPositions[1] = textFirstLine_tiny;
|
||||
textPositions[2] = textSecondLine_tiny;
|
||||
textPositions[3] = textThirdLine_tiny;
|
||||
textPositions[4] = textFourthLine_tiny;
|
||||
textPositions[5] = textFifthLine_tiny;
|
||||
textPositions[6] = textSixthLine_tiny;
|
||||
return textPositions;
|
||||
#endif
|
||||
|
||||
if (isHighResolution) {
|
||||
textPositions[0] = textZeroLine;
|
||||
textPositions[1] = textFirstLine_medium;
|
||||
|
||||
@@ -35,6 +35,21 @@ namespace graphics
|
||||
#define textFifthLine_large (textFourthLine_large + (FONT_HEIGHT_SMALL + 5))
|
||||
#define textSixthLine_large (textFifthLine_large + (FONT_HEIGHT_SMALL + 5))
|
||||
|
||||
// Tiny Font Spacing (TomThumb)
|
||||
// Only active when DISPLAY_FORCE_TOMTHUMB_FONT is defined
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
|
||||
#define TINY_REDUCE 5
|
||||
|
||||
#define textFirstLine_tiny (FONT_HEIGHT_SMALL + 1)
|
||||
#define textSecondLine_tiny (textFirstLine_tiny + (FONT_HEIGHT_SMALL - TINY_REDUCE + 5))
|
||||
#define textThirdLine_tiny (textSecondLine_tiny + (FONT_HEIGHT_SMALL - TINY_REDUCE + 5))
|
||||
#define textFourthLine_tiny (textThirdLine_tiny + (FONT_HEIGHT_SMALL - TINY_REDUCE + 5))
|
||||
#define textFifthLine_tiny (textFourthLine_tiny + (FONT_HEIGHT_SMALL - TINY_REDUCE + 5))
|
||||
#define textSixthLine_tiny (textFifthLine_tiny + (FONT_HEIGHT_SMALL - TINY_REDUCE + 5))
|
||||
|
||||
#endif
|
||||
|
||||
// Quick screen access
|
||||
#define SCREEN_WIDTH display->getWidth()
|
||||
#define SCREEN_HEIGHT display->getHeight()
|
||||
|
||||
@@ -194,17 +194,12 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
int line = 0;
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
int second = 0;
|
||||
|
||||
if (rtc_sec > 0) {
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
@@ -215,11 +210,11 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
}
|
||||
|
||||
bool isPM = hour >= 12;
|
||||
// hour = hour > 12 ? hour - 12 : hour;
|
||||
if (config.display.use_12h_clock) {
|
||||
hour %= 12;
|
||||
if (hour == 0)
|
||||
if (hour == 0) {
|
||||
hour = 12;
|
||||
}
|
||||
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
||||
} else {
|
||||
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
|
||||
@@ -229,24 +224,56 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
char secondString[8];
|
||||
snprintf(secondString, sizeof(secondString), "%02d", second);
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
float scale = 1.5;
|
||||
#elif defined(CHATTER_2)
|
||||
float scale = 1.1;
|
||||
#else
|
||||
float scale = 0.75;
|
||||
if (isHighResolution) {
|
||||
scale = 1.5;
|
||||
}
|
||||
#endif
|
||||
static bool scaleInitialized = false;
|
||||
static float scale = 0.75f;
|
||||
static float segmentWidth = SEGMENT_WIDTH * 0.75f;
|
||||
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
|
||||
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
if (!scaleInitialized) {
|
||||
float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable)
|
||||
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
||||
float step = 0.05f; // Step increment per iteration
|
||||
|
||||
float target_width = display->getWidth() * screenwidth_target_ratio;
|
||||
float target_height =
|
||||
display->getHeight() -
|
||||
(isHighResolution
|
||||
? 46
|
||||
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
|
||||
|
||||
float calculated_width_size = 0.0f;
|
||||
float calculated_height_size = 0.0f;
|
||||
|
||||
while (true) {
|
||||
segmentWidth = SEGMENT_WIDTH * scale;
|
||||
segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4);
|
||||
calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2);
|
||||
|
||||
if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) {
|
||||
break;
|
||||
}
|
||||
|
||||
scale += step;
|
||||
}
|
||||
|
||||
// If we overshot width, back off one step and recompute segment sizes
|
||||
if (calculated_width_size > target_width || calculated_height_size > target_height) {
|
||||
scale -= step;
|
||||
segmentWidth = SEGMENT_WIDTH * scale;
|
||||
segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
}
|
||||
|
||||
scaleInitialized = true;
|
||||
}
|
||||
|
||||
size_t len = strlen(timeString);
|
||||
|
||||
// calculate hours:minutes string width
|
||||
uint16_t timeStringWidth = strlen(timeString) * 5;
|
||||
uint16_t timeStringWidth = len * 5; // base spacing between characters
|
||||
|
||||
for (uint8_t i = 0; i < strlen(timeString); i++) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
|
||||
if (character == ':') {
|
||||
@@ -257,19 +284,21 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
}
|
||||
|
||||
uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2);
|
||||
|
||||
uint16_t startingHourMinuteTextX = hourMinuteTextX;
|
||||
|
||||
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2);
|
||||
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2;
|
||||
|
||||
// iterate over characters in hours:minutes string and draw segmented characters
|
||||
for (uint8_t i = 0; i < strlen(timeString); i++) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
|
||||
if (character == ':') {
|
||||
drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale);
|
||||
|
||||
hourMinuteTextX += segmentHeight + 6;
|
||||
if (scale >= 2.0f) {
|
||||
hourMinuteTextX += (uint16_t)(4.5f * scale);
|
||||
}
|
||||
} else {
|
||||
drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale);
|
||||
|
||||
@@ -279,38 +308,29 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
||||
hourMinuteTextX += 5;
|
||||
}
|
||||
|
||||
// draw seconds string
|
||||
// draw seconds string + AM/PM
|
||||
display->setFont(FONT_SMALL);
|
||||
int xOffset = (isHighResolution) ? 0 : -1;
|
||||
if (hour >= 10) {
|
||||
xOffset += (isHighResolution) ? 32 : 18;
|
||||
}
|
||||
int yOffset = (isHighResolution) ? 3 : 1;
|
||||
#ifdef SENSECAP_INDICATOR
|
||||
yOffset -= 3;
|
||||
#endif
|
||||
#ifdef T_DECK
|
||||
yOffset -= 5;
|
||||
#endif
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
||||
isPM ? "pm" : "am");
|
||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am");
|
||||
}
|
||||
|
||||
#ifndef USE_EINK
|
||||
xOffset = (isHighResolution) ? 18 : 10;
|
||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
||||
if (scale >= 2.0f) {
|
||||
xOffset -= (int)(4.5f * scale);
|
||||
}
|
||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1,
|
||||
secondString);
|
||||
#endif
|
||||
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
|
||||
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon);
|
||||
}
|
||||
|
||||
// Draw an analog clock
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
@@ -321,11 +341,6 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
int line = 0;
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||
}
|
||||
#endif
|
||||
// clock face center coordinates
|
||||
int16_t centerX = display->getWidth() / 2;
|
||||
int16_t centerY = display->getHeight() / 2;
|
||||
|
||||
@@ -24,7 +24,6 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
|
||||
|
||||
// UI elements for clock displays
|
||||
// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
||||
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
|
||||
} // namespace ClockRenderer
|
||||
|
||||
|
||||
@@ -581,11 +581,8 @@ void menuHandler::systemBaseMenu()
|
||||
|
||||
optionsArray[options] = "Notifications";
|
||||
optionsEnumArray[options++] = Notifications;
|
||||
#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \
|
||||
defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT
|
||||
optionsArray[options] = "Display Options";
|
||||
optionsEnumArray[options++] = ScreenOptions;
|
||||
#endif
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
optionsArray[options] = "Bluetooth";
|
||||
@@ -785,17 +782,24 @@ void menuHandler::nodeNameLengthMenu()
|
||||
|
||||
void menuHandler::resetNodeDBMenu()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||
static const char *optionsArray[] = {"Back", "Reset All", "Preserve Favorites"};
|
||||
BannerOverlayOptions bannerOptions;
|
||||
bannerOptions.message = "Confirm Reset NodeDB";
|
||||
bannerOptions.optionsArrayPtr = optionsArray;
|
||||
bannerOptions.optionsCount = 2;
|
||||
bannerOptions.optionsCount = 3;
|
||||
bannerOptions.bannerCallback = [](int selected) -> void {
|
||||
if (selected == 1) {
|
||||
if (selected == 1 || selected == 2) {
|
||||
disableBluetooth();
|
||||
screen->setFrames(Screen::FOCUS_DEFAULT);
|
||||
}
|
||||
if (selected == 1) {
|
||||
LOG_INFO("Initiate node-db reset");
|
||||
nodeDB->resetNodes();
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
} else if (selected == 2) {
|
||||
LOG_INFO("Initiate node-db reset but keeping favorites");
|
||||
nodeDB->resetNodes(1);
|
||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
|
||||
@@ -24,7 +24,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "MessageRenderer.h"
|
||||
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
#define MESSAGE_TINY_Y_OFFSET -3
|
||||
#else
|
||||
#define MESSAGE_TINY_Y_OFFSET 0
|
||||
#endif
|
||||
// Core includes
|
||||
#include "NodeDB.h"
|
||||
#include "configuration.h"
|
||||
|
||||
@@ -424,7 +424,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon)
|
||||
{
|
||||
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||
int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
rowYOffset += 4;
|
||||
#endif
|
||||
bool locationScreen = false;
|
||||
|
||||
if (strcmp(title, "Bearings") == 0)
|
||||
@@ -443,6 +447,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
|
||||
// Space below header
|
||||
y += COMMON_HEADER_HEIGHT;
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
y += 2; // Push entire list down by 2 pixels for TomThumb
|
||||
#endif
|
||||
|
||||
int totalEntries = nodeDB->getNumMeshNodes();
|
||||
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||
|
||||
@@ -278,6 +278,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta
|
||||
uint16_t totalLines = lineCount + alertBannerOptions;
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
effectiveLineHeight = FONT_HEIGHT_SMALL + 2;
|
||||
#endif
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint8_t linesShown = lineCount;
|
||||
const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
@@ -408,6 +411,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
effectiveLineHeight = FONT_HEIGHT_SMALL + 2;
|
||||
#endif
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint8_t linesShown = lineCount;
|
||||
const char *linePointers[visibleTotalLines + 1] = {0}; // this is sort of a dynamic allocation
|
||||
@@ -633,6 +639,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
uint16_t screenHeight = display->height();
|
||||
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
effectiveLineHeight = FONT_HEIGHT_SMALL + 2;
|
||||
#endif
|
||||
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||
@@ -664,6 +673,9 @@ void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplay
|
||||
|
||||
// === Draw Content ===
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
#ifdef DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
lineY += 2; // Offset entire options list downward
|
||||
#endif
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
if (needs_bell && i == 0) {
|
||||
|
||||
410
src/graphics/fonts/OLEDDisplayFontsTomThumb.cpp
Normal file
410
src/graphics/fonts/OLEDDisplayFontsTomThumb.cpp
Normal file
@@ -0,0 +1,410 @@
|
||||
#include "OLEDDisplayFontsTomThumb.h"
|
||||
|
||||
const uint8_t TomThumb4x6[] PROGMEM = {
|
||||
0x05, // heightMinus1 = 5 → height = 6
|
||||
0x04, // width (unused by Meshtastic, but must exist)
|
||||
0x20, // first char
|
||||
0xBD, // last char
|
||||
// Jump Table:
|
||||
0xFF, 0xFF, 0x00, 0x04, // space (advance 3->4)
|
||||
0x00, 0x00, 0x02, 0x04, // exclam (advance 3->4)
|
||||
0x00, 0x02, 0x03, 0x04, // quotedbl (advance 3->4)
|
||||
0x00, 0x05, 0x03, 0x04, // numbersign (advance 3->4)
|
||||
0x00, 0x08, 0x03, 0x04, // dollar (advance 3->4)
|
||||
0x00, 0x0B, 0x03, 0x04, // percent (advance 3->4)
|
||||
0x00, 0x0E, 0x03, 0x04, // ampersand (advance 3->4)
|
||||
0x00, 0x11, 0x02, 0x04, // quotesingle (advance 3->4)
|
||||
0x00, 0x13, 0x03, 0x04, // parenleft (advance 3->4)
|
||||
0x00, 0x16, 0x02, 0x04, // parenright (advance 3->4)
|
||||
0x00, 0x18, 0x03, 0x04, // asterisk (advance 3->4)
|
||||
0x00, 0x1B, 0x03, 0x04, // plus (advance 3->4)
|
||||
0x00, 0x1E, 0x02, 0x04, // comma (advance 3->4)
|
||||
0x00, 0x20, 0x03, 0x04, // hyphen (advance 3->4)
|
||||
0x00, 0x23, 0x02, 0x04, // period (advance 3->4)
|
||||
0x00, 0x25, 0x03, 0x04, // slash (advance 3->4)
|
||||
0x00, 0x28, 0x03, 0x04, // zero (advance 3->4)
|
||||
0x00, 0x2B, 0x02, 0x04, // one (advance 3->4)
|
||||
0x00, 0x2D, 0x03, 0x04, // two (advance 3->4)
|
||||
0x00, 0x30, 0x03, 0x04, // three (advance 3->4)
|
||||
0x00, 0x33, 0x03, 0x04, // four (advance 3->4)
|
||||
0x00, 0x36, 0x03, 0x04, // five (advance 3->4)
|
||||
0x00, 0x39, 0x03, 0x04, // six (advance 3->4)
|
||||
0x00, 0x3C, 0x03, 0x04, // seven (advance 3->4)
|
||||
0x00, 0x3F, 0x03, 0x04, // eight (advance 3->4)
|
||||
0x00, 0x42, 0x03, 0x04, // nine (advance 3->4)
|
||||
0x00, 0x45, 0x02, 0x04, // colon (advance 3->4)
|
||||
0x00, 0x47, 0x02, 0x04, // semicolon (advance 3->4)
|
||||
0x00, 0x49, 0x03, 0x04, // less (advance 3->4)
|
||||
0x00, 0x4C, 0x03, 0x04, // equal (advance 3->4)
|
||||
0x00, 0x4F, 0x03, 0x04, // greater (advance 3->4)
|
||||
0x00, 0x52, 0x03, 0x04, // question (advance 3->4)
|
||||
0x00, 0x55, 0x03, 0x04, // at (advance 3->4)
|
||||
0x00, 0x58, 0x03, 0x04, // A (advance 3->4)
|
||||
0x00, 0x5B, 0x03, 0x04, // B (advance 3->4)
|
||||
0x00, 0x5E, 0x03, 0x04, // C (advance 3->4)
|
||||
0x00, 0x61, 0x03, 0x04, // D (advance 3->4)
|
||||
0x00, 0x64, 0x03, 0x04, // E (advance 3->4)
|
||||
0x00, 0x67, 0x03, 0x04, // F (advance 3->4)
|
||||
0x00, 0x6A, 0x03, 0x04, // G (advance 3->4)
|
||||
0x00, 0x6D, 0x03, 0x04, // H (advance 3->4)
|
||||
0x00, 0x70, 0x03, 0x04, // I (advance 3->4)
|
||||
0x00, 0x73, 0x03, 0x04, // J (advance 3->4)
|
||||
0x00, 0x76, 0x03, 0x04, // K (advance 3->4)
|
||||
0x00, 0x79, 0x03, 0x04, // L (advance 3->4)
|
||||
0x00, 0x7C, 0x03, 0x04, // M (advance 3->4)
|
||||
0x00, 0x7F, 0x03, 0x04, // N (advance 3->4)
|
||||
0x00, 0x82, 0x03, 0x04, // O (advance 3->4)
|
||||
0x00, 0x85, 0x03, 0x04, // P (advance 3->4)
|
||||
0x00, 0x88, 0x03, 0x04, // Q (advance 3->4)
|
||||
0x00, 0x8B, 0x03, 0x04, // R (advance 3->4)
|
||||
0x00, 0x8E, 0x03, 0x04, // S (advance 3->4)
|
||||
0x00, 0x91, 0x03, 0x04, // T (advance 3->4)
|
||||
0x00, 0x94, 0x03, 0x04, // U (advance 3->4)
|
||||
0x00, 0x97, 0x03, 0x04, // V (advance 3->4)
|
||||
0x00, 0x9A, 0x03, 0x04, // W (advance 3->4)
|
||||
0x00, 0x9D, 0x03, 0x04, // X (advance 3->4)
|
||||
0x00, 0xA0, 0x03, 0x04, // Y (advance 3->4)
|
||||
0x00, 0xA3, 0x03, 0x04, // Z (advance 3->4)
|
||||
0x00, 0xA6, 0x03, 0x04, // bracketleft (advance 3->4)
|
||||
0x00, 0xA9, 0x03, 0x04, // backslash (advance 3->4)
|
||||
0x00, 0xAC, 0x03, 0x04, // bracketright (advance 3->4)
|
||||
0x00, 0xAF, 0x03, 0x04, // asciicircum (advance 3->4)
|
||||
0x00, 0xB2, 0x03, 0x04, // underscore (advance 3->4)
|
||||
0x00, 0xB5, 0x02, 0x04, // grave (advance 3->4)
|
||||
0x00, 0xB7, 0x03, 0x04, // a (advance 3->4)
|
||||
0x00, 0xBA, 0x03, 0x04, // b (advance 3->4)
|
||||
0x00, 0xBD, 0x03, 0x04, // c (advance 3->4)
|
||||
0x00, 0xC0, 0x03, 0x04, // d (advance 3->4)
|
||||
0x00, 0xC3, 0x03, 0x04, // e (advance 3->4)
|
||||
0x00, 0xC6, 0x03, 0x04, // f (advance 3->4)
|
||||
0x00, 0xC9, 0x03, 0x04, // g (advance 3->4)
|
||||
0x00, 0xCC, 0x03, 0x04, // h (advance 3->4)
|
||||
0x00, 0xCF, 0x02, 0x04, // i (advance 3->4)
|
||||
0x00, 0xD1, 0x03, 0x04, // j (advance 3->4)
|
||||
0x00, 0xD4, 0x03, 0x04, // k (advance 3->4)
|
||||
0x00, 0xD7, 0x03, 0x04, // l (advance 3->4)
|
||||
0x00, 0xDA, 0x03, 0x04, // m (advance 3->4)
|
||||
0x00, 0xDD, 0x03, 0x04, // n (advance 3->4)
|
||||
0x00, 0xE0, 0x03, 0x04, // o (advance 3->4)
|
||||
0x00, 0xE3, 0x03, 0x04, // p (advance 3->4)
|
||||
0x00, 0xE6, 0x03, 0x04, // q (advance 3->4)
|
||||
0x00, 0xE9, 0x03, 0x04, // r (advance 3->4)
|
||||
0x00, 0xEC, 0x03, 0x04, // s (advance 3->4)
|
||||
0x00, 0xEF, 0x03, 0x04, // t (advance 3->4)
|
||||
0x00, 0xF2, 0x03, 0x04, // u (advance 3->4)
|
||||
0x00, 0xF5, 0x03, 0x04, // v (advance 3->4)
|
||||
0x00, 0xF8, 0x03, 0x04, // w (advance 3->4)
|
||||
0x00, 0xFB, 0x03, 0x04, // x (advance 3->4)
|
||||
0x00, 0xFE, 0x03, 0x04, // y (advance 3->4)
|
||||
0x01, 0x01, 0x03, 0x04, // z (advance 3->4)
|
||||
0x01, 0x04, 0x03, 0x04, // braceleft (advance 3->4)
|
||||
0x01, 0x07, 0x02, 0x04, // bar (advance 3->4)
|
||||
0x01, 0x09, 0x03, 0x04, // braceright (advance 3->4)
|
||||
0x01, 0x0C, 0x03, 0x04, // asciitilde (advance 3->4)
|
||||
0x01, 0x0F, 0x02, 0x04, // exclamdown (advance 3->4)
|
||||
0x01, 0x11, 0x03, 0x04, // cent
|
||||
0x01, 0x14, 0x03, 0x04, // sterling
|
||||
0x01, 0x17, 0x03, 0x04, // currency
|
||||
0x01, 0x1A, 0x03, 0x04, // yen
|
||||
0x01, 0x1D, 0x02, 0x04, // brokenbar
|
||||
0x01, 0x1F, 0x03, 0x04, // section
|
||||
0x01, 0x22, 0x03, 0x04, // dieresis
|
||||
0x01, 0x25, 0x03, 0x04, // copyright
|
||||
0x01, 0x28, 0x03, 0x04, // ordfeminine
|
||||
0x01, 0x2B, 0x02, 0x04, // guillemotleft
|
||||
0x01, 0x2D, 0x03, 0x04, // logicalnot
|
||||
0x01, 0x30, 0x02, 0x04, // softhyphen
|
||||
0x01, 0x32, 0x03, 0x04, // registered
|
||||
0x01, 0x35, 0x03, 0x04, // macron
|
||||
0x01, 0x38, 0x03, 0x04, // degree
|
||||
0x01, 0x3B, 0x03, 0x04, // plusminus
|
||||
0x01, 0x3E, 0x03, 0x04, // twosuperior
|
||||
0x01, 0x41, 0x03, 0x04, // threesuperior
|
||||
0x01, 0x44, 0x03, 0x04, // acute
|
||||
0x01, 0x47, 0x03, 0x04, // mu
|
||||
0x01, 0x4A, 0x03, 0x04, // paragraph
|
||||
0x01, 0x4D, 0x03, 0x04, // periodcentered
|
||||
0x01, 0x50, 0x03, 0x04, // cedilla
|
||||
0x01, 0x53, 0x02, 0x04, // onesuperior
|
||||
0x01, 0x55, 0x03, 0x04, // ordmasculine
|
||||
0x01, 0x58, 0x03, 0x04, // guillemotright
|
||||
0x01, 0x5B, 0x03, 0x04, // onequarter
|
||||
0x01, 0x5E, 0x03, 0x04, // onehalf
|
||||
0x01, 0x61, 0x03, 0x04, // threequarters
|
||||
0x01, 0x64, 0x03, 0x04, // questiondown
|
||||
0x01, 0x67, 0x03, 0x04, // Agrave
|
||||
0x01, 0x6A, 0x03, 0x04, // Aacute
|
||||
0x01, 0x6D, 0x03, 0x04, // Acircumflex
|
||||
0x01, 0x70, 0x03, 0x04, // Atilde
|
||||
0x01, 0x73, 0x03, 0x04, // Adieresis
|
||||
0x01, 0x76, 0x03, 0x04, // Aring
|
||||
0x01, 0x79, 0x03, 0x04, // AE
|
||||
0x01, 0x7C, 0x03, 0x04, // Ccedilla
|
||||
0x01, 0x7F, 0x03, 0x04, // Egrave
|
||||
0x01, 0x82, 0x03, 0x04, // Eacute
|
||||
0x01, 0x85, 0x03, 0x04, // Ecircumflex
|
||||
0x01, 0x88, 0x03, 0x04, // Edieresis
|
||||
0x01, 0x8B, 0x03, 0x04, // Igrave
|
||||
0x01, 0x8E, 0x03, 0x04, // Iacute
|
||||
0x01, 0x91, 0x03, 0x04, // Icircumflex
|
||||
0x01, 0x94, 0x03, 0x04, // Idieresis
|
||||
0x01, 0x97, 0x03, 0x04, // Eth
|
||||
0x01, 0x9A, 0x03, 0x04, // Ntilde
|
||||
0x01, 0x9D, 0x03, 0x04, // Ograve
|
||||
0x01, 0xA0, 0x03, 0x04, // Oacute
|
||||
0x01, 0xA3, 0x03, 0x04, // Ocircumflex
|
||||
0x01, 0xA6, 0x03, 0x04, // Otilde
|
||||
0x01, 0xA9, 0x03, 0x04, // Odieresis
|
||||
0x01, 0xAC, 0x03, 0x04, // multiply
|
||||
0x01, 0xAF, 0x03, 0x04, // Oslash
|
||||
0x01, 0xB2, 0x03, 0x04, // Ugrave
|
||||
0x01, 0xB5, 0x03, 0x04, // Uacute
|
||||
0x01, 0xB8, 0x03, 0x04, // Ucircumflex
|
||||
0x01, 0xBB, 0x03, 0x04, // Udieresis
|
||||
0x01, 0xBE, 0x03, 0x04, // Yacute
|
||||
0x01, 0xC1, 0x03, 0x04, // Thorn
|
||||
0x01, 0xC4, 0x03, 0x04, // germandbls
|
||||
0x01, 0xC7, 0x03, 0x04, // agrave
|
||||
0x01, 0xCA, 0x03, 0x04, // aacute
|
||||
0x01, 0xCD, 0x03, 0x04, // acircumflex
|
||||
0x01, 0xD0, 0x03, 0x04, // atilde
|
||||
0x01, 0xD3, 0x03, 0x04, // adieresis
|
||||
0x01, 0xD6, 0x03, 0x04, // aring
|
||||
0x01, 0xD9, 0x03, 0x04, // ae
|
||||
0x01, 0xDC, 0x03, 0x04, // ccedilla
|
||||
0x01, 0xDF, 0x03, 0x04, // egrave
|
||||
0x01, 0xE2, 0x03, 0x04, // eacute
|
||||
0x01, 0xE5, 0x03, 0x04, // ecircumflex
|
||||
0x01, 0xE8, 0x03, 0x04, // edieresis
|
||||
0x01, 0xEB, 0x03, 0x04, // igrave
|
||||
0x01, 0xEE, 0x02, 0x04, // iacute
|
||||
0x01, 0xF0, 0x03, 0x04, // icircumflex
|
||||
0x01, 0xF3, 0x03, 0x04, // idieresis
|
||||
0x01, 0xF6, 0x03, 0x04, // eth
|
||||
0x01, 0xF9, 0x03, 0x04, // ntilde
|
||||
0x01, 0xFC, 0x03, 0x04, // ograve
|
||||
0x01, 0xFF, 0x03, 0x04, // oacute
|
||||
0x02, 0x02, 0x03, 0x04, // ocircumflex
|
||||
0x02, 0x05, 0x03, 0x04, // otilde
|
||||
0x02, 0x08, 0x03, 0x04, // odieresis
|
||||
0x02, 0x0B, 0x03, 0x04, // divide
|
||||
0x02, 0x0E, 0x03, 0x04, // oslash
|
||||
0x02, 0x11, 0x03, 0x04, // ugrave
|
||||
0x02, 0x14, 0x03, 0x04, // uacute
|
||||
0x02, 0x17, 0x03, 0x04, // ucircumflex
|
||||
0x02, 0x1A, 0x03, 0x04, // udieresis
|
||||
0x02, 0x1D, 0x03, 0x04, // yacute
|
||||
0x02, 0x20, 0x03, 0x04, // thorn
|
||||
|
||||
// =================
|
||||
// Font Bitmap Data:
|
||||
// =================
|
||||
0x00, 0x17, // exclam
|
||||
0x03, 0x00, 0x04, // quotedbl
|
||||
0x1F, 0x0A, 0x1F, // numbersign
|
||||
0x0A, 0x1F, 0x05, // dollar
|
||||
0x09, 0x04, 0x12, // percent
|
||||
0x0F, 0x17, 0x1C, // ampersand
|
||||
0x00, 0x04, // quotesingle
|
||||
0x00, 0x0E, 0x11, // parenleft
|
||||
0x11, 0x0E, // parenright
|
||||
0x05, 0x02, 0x05, // asterisk
|
||||
0x04, 0x0E, 0x04, // plus
|
||||
0x10, 0x08, // comma
|
||||
0x04, 0x04, 0x04, // hyphen
|
||||
0x00, 0x10, // period
|
||||
0x18, 0x04, 0x04, // slash
|
||||
0x1E, 0x11, 0x0F, // zero
|
||||
0x02, 0x1F, // one
|
||||
0x19, 0x15, 0x12, // two
|
||||
0x11, 0x15, 0x0A, // three
|
||||
0x07, 0x04, 0x1F, // four
|
||||
0x17, 0x15, 0x09, // five
|
||||
0x1E, 0x15, 0x1D, // six
|
||||
0x19, 0x05, 0x04, // seven
|
||||
0x1F, 0x15, 0x1F, // eight
|
||||
0x17, 0x15, 0x0F, // nine
|
||||
0x00, 0x0A, // colon
|
||||
0x10, 0x0A, // semicolon
|
||||
0x04, 0x0A, 0x11, // less
|
||||
0x0A, 0x0A, 0x0A, // equal
|
||||
0x11, 0x0A, 0x04, // greater
|
||||
0x01, 0x15, 0x04, // question
|
||||
0x0E, 0x15, 0x16, // at
|
||||
0x1E, 0x05, 0x1E, // A
|
||||
0x1F, 0x15, 0x0A, // B
|
||||
0x0E, 0x11, 0x11, // C
|
||||
0x1F, 0x11, 0x0E, // D
|
||||
0x1F, 0x15, 0x15, // E
|
||||
0x1F, 0x05, 0x05, // F
|
||||
0x0E, 0x15, 0x1D, // G
|
||||
0x1F, 0x04, 0x1F, // H
|
||||
0x11, 0x1F, 0x11, // I
|
||||
0x08, 0x10, 0x0F, // J
|
||||
0x1F, 0x04, 0x1B, // K
|
||||
0x1F, 0x10, 0x10, // L
|
||||
0x1F, 0x06, 0x1F, // M
|
||||
0x1F, 0x0E, 0x1F, // N
|
||||
0x0E, 0x11, 0x0E, // O
|
||||
0x1F, 0x05, 0x02, // P
|
||||
0x0E, 0x19, 0x1E, // Q
|
||||
0x1F, 0x0D, 0x16, // R
|
||||
0x12, 0x15, 0x09, // S
|
||||
0x01, 0x1F, 0x01, // T
|
||||
0x0F, 0x10, 0x1F, // U
|
||||
0x07, 0x18, 0x07, // V
|
||||
0x1F, 0x0C, 0x1F, // W
|
||||
0x1B, 0x04, 0x1B, // X
|
||||
0x03, 0x1C, 0x04, // Y
|
||||
0x19, 0x15, 0x13, // Z
|
||||
0x1F, 0x11, 0x11, // bracketleft
|
||||
0x02, 0x04, 0x08, // backslash
|
||||
0x11, 0x11, 0x1F, // bracketright
|
||||
0x02, 0x01, 0x02, // asciicircum
|
||||
0x10, 0x10, 0x10, // underscore
|
||||
0x01, 0x02, // grave
|
||||
0x1A, 0x16, 0x1C, // a
|
||||
0x1F, 0x12, 0x0C, // b
|
||||
0x0C, 0x12, 0x12, // c
|
||||
0x0C, 0x12, 0x1F, // d
|
||||
0x0C, 0x1A, 0x16, // e
|
||||
0x04, 0x1E, 0x05, // f
|
||||
0x0C, 0x2A, 0x1E, // g
|
||||
0x1F, 0x02, 0x1C, // h
|
||||
0x00, 0x1D, // i
|
||||
0x10, 0x20, 0x1D, // j
|
||||
0x1F, 0x0C, 0x12, // k
|
||||
0x11, 0x1F, 0x10, // l
|
||||
0x1E, 0x0E, 0x1E, // m
|
||||
0x1E, 0x02, 0x1C, // n
|
||||
0x0C, 0x12, 0x0C, // o
|
||||
0x3E, 0x12, 0x0C, // p
|
||||
0x0C, 0x12, 0x3E, // q
|
||||
0x1C, 0x02, 0x02, // r
|
||||
0x14, 0x1E, 0x0A, // s
|
||||
0x02, 0x1F, 0x12, // t
|
||||
0x0E, 0x10, 0x1E, // u
|
||||
0x0E, 0x18, 0x0E, // v
|
||||
0x1E, 0x1C, 0x1E, // w
|
||||
0x12, 0x0C, 0x12, // x
|
||||
0x06, 0x28, 0x1E, // y
|
||||
0x1A, 0x1E, 0x16, // z
|
||||
0x04, 0x1B, 0x11, // braceleft
|
||||
0x00, 0x1B, // bar
|
||||
0x11, 0x1B, 0x04, // braceright
|
||||
0x02, 0x03, 0x01, // asciitilde
|
||||
0x00, 0x1D, // exclamdown
|
||||
0x0E, 0x1B, 0x0A, // cent
|
||||
0x14, 0x1F, 0x15, // sterling
|
||||
0x15, 0x0E, 0x15, // currency
|
||||
0x0B, 0x1C, 0x0B, // yen
|
||||
0x00, 0x1B, // brokenbar
|
||||
0x14, 0x1B, 0x05, // section
|
||||
0x01, 0x00, 0x01, // dieresis
|
||||
0x02, 0x05, 0x05, // copyright
|
||||
0x16, 0x15, 0x17, // ordfeminine
|
||||
0x02, 0x05, // guillemotleft
|
||||
0x02, 0x02, 0x06, // logicalnot
|
||||
0x04, 0x04, // softhyphen
|
||||
0x07, 0x03, 0x04, // registered
|
||||
0x01, 0x01, 0x01, // macron
|
||||
0x02, 0x05, 0x02, // degree
|
||||
0x12, 0x17, 0x12, // plusminus
|
||||
0x01, 0x07, 0x04, // twosuperior
|
||||
0x05, 0x07, 0x07, // threesuperior
|
||||
0x00, 0x02, 0x01, // acute
|
||||
0x1F, 0x08, 0x07, // mu
|
||||
0x02, 0x1D, 0x1F, // paragraph
|
||||
0x0E, 0x0E, 0x0E, // periodcentered
|
||||
0x10, 0x14, 0x08, // cedilla
|
||||
0x00, 0x07, // onesuperior
|
||||
0x12, 0x15, 0x12, // ordmasculine
|
||||
0x00, 0x05, 0x02, // guillemotright
|
||||
0x03, 0x08, 0x18, // onequarter
|
||||
0x0B, 0x18, 0x10, // onehalf
|
||||
0x03, 0x0B, 0x18, // threequarters
|
||||
0x18, 0x15, 0x10, // questiondown
|
||||
0x18, 0x0D, 0x1A, // Agrave
|
||||
0x1A, 0x0D, 0x18, // Aacute
|
||||
0x19, 0x0D, 0x19, // Acircumflex
|
||||
0x1A, 0x0F, 0x19, // Atilde
|
||||
0x1D, 0x0A, 0x1D, // Adieresis
|
||||
0x1F, 0x0B, 0x1C, // Aring
|
||||
0x1E, 0x1F, 0x15, // AE
|
||||
0x06, 0x29, 0x19, // Ccedilla
|
||||
0x1C, 0x1D, 0x16, // Egrave
|
||||
0x1E, 0x1D, 0x14, // Eacute
|
||||
0x1D, 0x1D, 0x15, // Ecircumflex
|
||||
0x1D, 0x1C, 0x15, // Edieresis
|
||||
0x14, 0x1D, 0x16, // Igrave
|
||||
0x16, 0x1D, 0x14, // Iacute
|
||||
0x15, 0x1D, 0x15, // Icircumflex
|
||||
0x15, 0x1C, 0x15, // Idieresis
|
||||
0x1F, 0x15, 0x0E, // Eth
|
||||
0x1D, 0x0B, 0x1E, // Ntilde
|
||||
0x1C, 0x15, 0x1E, // Ograve
|
||||
0x1E, 0x15, 0x1C, // Oacute
|
||||
0x1D, 0x15, 0x1D, // Ocircumflex
|
||||
0x1D, 0x17, 0x1E, // Otilde
|
||||
0x1D, 0x14, 0x1D, // Odieresis
|
||||
0x0A, 0x04, 0x0A, // multiply
|
||||
0x1E, 0x15, 0x0F, // Oslash
|
||||
0x1D, 0x12, 0x1C, // Ugrave
|
||||
0x1C, 0x12, 0x1D, // Uacute
|
||||
0x1D, 0x11, 0x1D, // Ucircumflex
|
||||
0x1D, 0x10, 0x1D, // Udieresis
|
||||
0x0C, 0x1A, 0x0D, // Yacute
|
||||
0x1F, 0x0A, 0x0E, // Thorn
|
||||
0x3E, 0x15, 0x0B, // germandbls
|
||||
0x18, 0x15, 0x1E, // agrave
|
||||
0x1A, 0x15, 0x1C, // aacute
|
||||
0x19, 0x15, 0x1D, // acircumflex
|
||||
0x1A, 0x17, 0x1D, // atilde
|
||||
0x19, 0x14, 0x1D, // adieresis
|
||||
0x18, 0x17, 0x1F, // aring
|
||||
0x1C, 0x1E, 0x0E, // ae
|
||||
0x04, 0x2A, 0x1A, // ccedilla
|
||||
0x08, 0x1D, 0x1E, // egrave
|
||||
0x0A, 0x1D, 0x1C, // eacute
|
||||
0x09, 0x1D, 0x1D, // ecircumflex
|
||||
0x09, 0x1C, 0x1D, // edieresis
|
||||
0x00, 0x1D, 0x02, // igrave
|
||||
0x02, 0x1D, // iacute
|
||||
0x01, 0x1D, 0x01, // icircumflex
|
||||
0x01, 0x1C, 0x01, // idieresis
|
||||
0x0A, 0x17, 0x1D, // eth
|
||||
0x1D, 0x07, 0x1A, // ntilde
|
||||
0x08, 0x15, 0x0A, // ograve
|
||||
0x0A, 0x15, 0x08, // oacute
|
||||
0x09, 0x15, 0x09, // ocircumflex
|
||||
0x09, 0x17, 0x0A, // otilde
|
||||
0x09, 0x14, 0x09, // odieresis
|
||||
0x04, 0x15, 0x04, // divide
|
||||
0x1C, 0x16, 0x0E, // oslash
|
||||
0x0D, 0x12, 0x1C, // ugrave
|
||||
0x0C, 0x12, 0x1D, // uacute
|
||||
0x0D, 0x11, 0x1D, // ucircumflex
|
||||
0x0D, 0x10, 0x1D, // udieresis
|
||||
0x04, 0x2A, 0x1D, // yacute
|
||||
0x3E, 0x14, 0x08 // thorn
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// FONT_INFO wrapper required by Meshtastic
|
||||
// ============================================================================
|
||||
//
|
||||
// NOTE:
|
||||
// Meshtastic OLED renderer does *not* use the FONT_CHAR_INFO jump table when
|
||||
// the font uses the raw-array jump table format. But this struct MUST exist.
|
||||
//
|
||||
static const FONT_CHAR_INFO TomThumb4x6_CharInfo[] PROGMEM = {};
|
||||
|
||||
// ============================================================================
|
||||
// Final FONT_INFO Export
|
||||
// ============================================================================
|
||||
const FONT_INFO TomThumb4x6_Info = {.heightBits = 6, // REAL glyph height
|
||||
.baseline = 4, // Correct baseline for 6px font
|
||||
.startChar = 0x20,
|
||||
.endChar = 0xBD,
|
||||
.charInfo = TomThumb4x6_CharInfo,
|
||||
.data = TomThumb4x6};
|
||||
32
src/graphics/fonts/OLEDDisplayFontsTomThumb.h
Normal file
32
src/graphics/fonts/OLEDDisplayFontsTomThumb.h
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// Information about a single character
|
||||
typedef struct {
|
||||
uint8_t widthBits; // Glyph width in bits
|
||||
uint16_t offset; // Offset into the bitmap table
|
||||
} FONT_CHAR_INFO;
|
||||
|
||||
// Information about the whole font
|
||||
typedef struct {
|
||||
uint8_t heightBits; // Character height in pixels (6px)
|
||||
uint8_t baseline; // baseline (height-1) = 5
|
||||
uint8_t startChar; // First supported char = 0x20
|
||||
uint8_t endChar; // Last supported char = 0xBD
|
||||
const FONT_CHAR_INFO *charInfo; // Jump table
|
||||
const uint8_t *data; // Bitmap table
|
||||
} FONT_INFO;
|
||||
|
||||
// Raw PROGMEM font data (jump table + bitmap stream)
|
||||
extern const uint8_t TomThumb4x6[] PROGMEM;
|
||||
|
||||
// Wrapper combining the tables so OLED code can use it
|
||||
extern const FONT_INFO TomThumb4x6_Info;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -34,7 +34,8 @@ void NeighborInfoModule::printNodeDBNeighbors()
|
||||
}
|
||||
}
|
||||
|
||||
/* Send our initial owner announcement 35 seconds after we start (to give network time to setup) */
|
||||
/* Send our initial owner announcement 35 seconds after we start (to give
|
||||
* network time to setup) */
|
||||
NeighborInfoModule::NeighborInfoModule()
|
||||
: ProtobufModule("neighborinfo", meshtastic_PortNum_NEIGHBORINFO_APP, &meshtastic_NeighborInfo_msg),
|
||||
concurrency::OSThread("NeighborInfo")
|
||||
@@ -53,8 +54,8 @@ NeighborInfoModule::NeighborInfoModule()
|
||||
}
|
||||
|
||||
/*
|
||||
Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time
|
||||
Assumes that the neighborInfo packet has been allocated
|
||||
Collect neighbor info from the nodeDB's history, capping at a maximum number of
|
||||
entries and max time Assumes that the neighborInfo packet has been allocated
|
||||
@returns the number of entries collected
|
||||
*/
|
||||
uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighborInfo)
|
||||
@@ -71,8 +72,8 @@ uint32_t NeighborInfoModule::collectNeighborInfo(meshtastic_NeighborInfo *neighb
|
||||
if ((neighborInfo->neighbors_count < MAX_NUM_NEIGHBORS) && (nbr.node_id != my_node_id)) {
|
||||
neighborInfo->neighbors[neighborInfo->neighbors_count].node_id = nbr.node_id;
|
||||
neighborInfo->neighbors[neighborInfo->neighbors_count].snr = nbr.snr;
|
||||
// Note: we don't set the last_rx_time and node_broadcast_intervals_secs here, because we don't want to send this over
|
||||
// the mesh
|
||||
// Note: we don't set the last_rx_time and node_broadcast_intervals_secs
|
||||
// here, because we don't want to send this over the mesh
|
||||
neighborInfo->neighbors_count++;
|
||||
}
|
||||
}
|
||||
@@ -88,8 +89,9 @@ void NeighborInfoModule::cleanUpNeighbors()
|
||||
uint32_t now = getTime();
|
||||
NodeNum my_node_id = nodeDB->getNodeNum();
|
||||
for (auto it = neighbors.rbegin(); it != neighbors.rend();) {
|
||||
// We will remove a neighbor if we haven't heard from them in twice the broadcast interval
|
||||
// cannot use isWithinTimespanMs() as it->last_rx_time is seconds since 1970
|
||||
// We will remove a neighbor if we haven't heard from them in twice the
|
||||
// broadcast interval cannot use isWithinTimespanMs() as it->last_rx_time is
|
||||
// seconds since 1970
|
||||
if ((now - it->last_rx_time > it->node_broadcast_interval_secs * 2) && (it->node_id != my_node_id)) {
|
||||
LOG_DEBUG("Remove neighbor with node ID 0x%x", it->node_id);
|
||||
it = std::vector<meshtastic_Neighbor>::reverse_iterator(
|
||||
@@ -132,25 +134,55 @@ int32_t NeighborInfoModule::runOnce()
|
||||
return Default::getConfiguredOrDefaultMs(moduleConfig.neighbor_info.update_interval, default_neighbor_info_broadcast_secs);
|
||||
}
|
||||
|
||||
meshtastic_MeshPacket *NeighborInfoModule::allocReply()
|
||||
{
|
||||
LOG_INFO("NeighborInfoRequested.");
|
||||
if (lastSentReply && Throttle::isWithinTimespanMs(lastSentReply, 3 * 60 * 1000)) {
|
||||
LOG_DEBUG("Skip Neighbors reply since we sent a reply <3min ago");
|
||||
ignoreRequest = true; // Mark it as ignored for MeshModule
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero;
|
||||
collectNeighborInfo(&neighborInfo);
|
||||
|
||||
meshtastic_MeshPacket *reply = allocDataProtobuf(neighborInfo);
|
||||
|
||||
if (reply) {
|
||||
lastSentReply = millis(); // Track when we sent this reply
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
/*
|
||||
Collect a received neighbor info packet from another node
|
||||
Pass it to an upper client; do not persist this data on the mesh
|
||||
*/
|
||||
bool NeighborInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *np)
|
||||
{
|
||||
LOG_DEBUG("NeighborInfo: handleReceivedProtobuf");
|
||||
if (np) {
|
||||
printNeighborInfo("RECEIVED", np);
|
||||
updateNeighbors(mp, np);
|
||||
// Ignore dummy/interceptable packets: single neighbor with nodeId 0 and snr 0
|
||||
if (np->neighbors_count != 1 || np->neighbors[0].node_id != 0 || np->neighbors[0].snr != 0.0f) {
|
||||
LOG_DEBUG(" Updating neighbours");
|
||||
updateNeighbors(mp, np);
|
||||
} else {
|
||||
LOG_DEBUG(" Ignoring dummy neighbor info packet (single neighbor with nodeId 0, snr 0)");
|
||||
}
|
||||
} else if (mp.hop_start != 0 && mp.hop_start == mp.hop_limit) {
|
||||
LOG_DEBUG("Get or create neighbor: %u with snr %f", mp.from, mp.rx_snr);
|
||||
// If the hopLimit is the same as hopStart, then it is a neighbor
|
||||
getOrCreateNeighbor(mp.from, mp.from, 0, mp.rx_snr); // Set the broadcast interval to 0, as we don't know it
|
||||
getOrCreateNeighbor(mp.from, mp.from, 0,
|
||||
mp.rx_snr); // Set the broadcast interval to 0, as we don't know it
|
||||
}
|
||||
// Allow others to handle this packet
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
Copy the content of a current NeighborInfo packet into a new one and update the last_sent_by_id to our NodeNum
|
||||
Copy the content of a current NeighborInfo packet into a new one and update the
|
||||
last_sent_by_id to our NodeNum
|
||||
*/
|
||||
void NeighborInfoModule::alterReceivedProtobuf(meshtastic_MeshPacket &p, meshtastic_NeighborInfo *n)
|
||||
{
|
||||
@@ -168,8 +200,10 @@ void NeighborInfoModule::resetNeighbors()
|
||||
|
||||
void NeighborInfoModule::updateNeighbors(const meshtastic_MeshPacket &mp, const meshtastic_NeighborInfo *np)
|
||||
{
|
||||
// The last sent ID will be 0 if the packet is from the phone, which we don't count as
|
||||
// an edge. So we assume that if it's zero, then this packet is from our node.
|
||||
LOG_DEBUG("updateNeighbors");
|
||||
// The last sent ID will be 0 if the packet is from the phone, which we don't
|
||||
// count as an edge. So we assume that if it's zero, then this packet is from
|
||||
// our node.
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
|
||||
getOrCreateNeighbor(mp.from, np->last_sent_by_id, np->node_broadcast_interval_secs, mp.rx_snr);
|
||||
}
|
||||
@@ -188,7 +222,8 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen
|
||||
// if found, update it
|
||||
neighbors[i].snr = snr;
|
||||
neighbors[i].last_rx_time = getTime();
|
||||
// Only if this is the original sender, the broadcast interval corresponds to it
|
||||
// Only if this is the original sender, the broadcast interval corresponds
|
||||
// to it
|
||||
if (originalSender == n && node_broadcast_interval_secs != 0)
|
||||
neighbors[i].node_broadcast_interval_secs = node_broadcast_interval_secs;
|
||||
return &neighbors[i];
|
||||
@@ -200,10 +235,12 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen
|
||||
new_nbr.node_id = n;
|
||||
new_nbr.snr = snr;
|
||||
new_nbr.last_rx_time = getTime();
|
||||
// Only if this is the original sender, the broadcast interval corresponds to it
|
||||
// Only if this is the original sender, the broadcast interval corresponds to
|
||||
// it
|
||||
if (originalSender == n && node_broadcast_interval_secs != 0)
|
||||
new_nbr.node_broadcast_interval_secs = node_broadcast_interval_secs;
|
||||
else // Assume the same broadcast interval as us for the neighbor if we don't know it
|
||||
else // Assume the same broadcast interval as us for the neighbor if we don't
|
||||
// know it
|
||||
new_nbr.node_broadcast_interval_secs = moduleConfig.neighbor_info.update_interval;
|
||||
|
||||
if (neighbors.size() < MAX_NUM_NEIGHBORS) {
|
||||
|
||||
@@ -28,6 +28,10 @@ class NeighborInfoModule : public ProtobufModule<meshtastic_NeighborInfo>, priva
|
||||
*/
|
||||
virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_NeighborInfo *nb) override;
|
||||
|
||||
/* Messages can be received that have the want_response bit set. If set, this callback will be invoked
|
||||
* so that subclasses can (optionally) send a response back to the original sender. */
|
||||
virtual meshtastic_MeshPacket *allocReply() override;
|
||||
|
||||
/*
|
||||
* Collect neighbor info from the nodeDB's history, capping at a maximum number of entries and max time
|
||||
* @return the number of entries collected
|
||||
@@ -66,5 +70,8 @@ class NeighborInfoModule : public ProtobufModule<meshtastic_NeighborInfo>, priva
|
||||
/* These are for debugging only */
|
||||
void printNeighborInfo(const char *header, const meshtastic_NeighborInfo *np);
|
||||
void printNodeDBNeighbors();
|
||||
|
||||
private:
|
||||
uint32_t lastSentReply = 0; // Last time we sent a position reply (used for reply throttling only)
|
||||
};
|
||||
extern NeighborInfoModule *neighborInfoModule;
|
||||
@@ -53,7 +53,7 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c
|
||||
#include "Sensor/LTR390UVSensor.h"
|
||||
#endif
|
||||
|
||||
#if __has_include(MESHTASTIC_BME680_HEADER)
|
||||
#if __has_include(<bsec2.h>)
|
||||
#include "Sensor/BME680Sensor.h"
|
||||
#endif
|
||||
|
||||
@@ -214,7 +214,7 @@ void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner)
|
||||
#if __has_include(<Adafruit_LTR390.h>)
|
||||
addSensor<LTR390UVSensor>(i2cScanner, ScanI2C::DeviceType::LTR390UV);
|
||||
#endif
|
||||
#if __has_include(MESHTASTIC_BME680_HEADER)
|
||||
#if __has_include(<bsec2.h>)
|
||||
addSensor<BME680Sensor>(i2cScanner, ScanI2C::DeviceType::BME_680);
|
||||
#endif
|
||||
#if __has_include(<Adafruit_BMP280.h>)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(MESHTASTIC_BME680_HEADER)
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<bsec2.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "BME680Sensor.h"
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
BME680Sensor::BME680Sensor() : TelemetrySensor(meshtastic_TelemetrySensorType_BME680, "BME680") {}
|
||||
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
int32_t BME680Sensor::runOnce()
|
||||
{
|
||||
if (!bme680.run()) {
|
||||
@@ -18,13 +17,10 @@ int32_t BME680Sensor::runOnce()
|
||||
}
|
||||
return 35;
|
||||
}
|
||||
#endif // defined(MESHTASTIC_BME680_BSEC2_SUPPORTED)
|
||||
|
||||
bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
{
|
||||
status = 0;
|
||||
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
if (!bme680.begin(dev->address.address, *bus))
|
||||
checkStatus("begin");
|
||||
|
||||
@@ -46,25 +42,12 @@ bool BME680Sensor::initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev)
|
||||
if (status == 0)
|
||||
LOG_DEBUG("BME680Sensor::runOnce: bme680.status %d", bme680.status);
|
||||
|
||||
#else
|
||||
bme680 = makeBME680(bus);
|
||||
|
||||
if (!bme680->begin(dev->address.address)) {
|
||||
LOG_ERROR("Init sensor: %s failed at begin()", sensorName);
|
||||
return status;
|
||||
}
|
||||
|
||||
status = 1;
|
||||
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
|
||||
initI2CSensor();
|
||||
return status;
|
||||
}
|
||||
|
||||
bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
{
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
if (bme680.getData(BSEC_OUTPUT_RAW_PRESSURE).signal == 0)
|
||||
return false;
|
||||
|
||||
@@ -82,27 +65,9 @@ bool BME680Sensor::getMetrics(meshtastic_Telemetry *measurement)
|
||||
// Check if we need to save state to filesystem (every STATE_SAVE_PERIOD ms)
|
||||
measurement->variant.environment_metrics.iaq = bme680.getData(BSEC_OUTPUT_IAQ).signal;
|
||||
updateState();
|
||||
#else
|
||||
if (!bme680->performReading()) {
|
||||
LOG_ERROR("BME680Sensor::getMetrics: performReading failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
measurement->variant.environment_metrics.has_temperature = true;
|
||||
measurement->variant.environment_metrics.has_relative_humidity = true;
|
||||
measurement->variant.environment_metrics.has_barometric_pressure = true;
|
||||
measurement->variant.environment_metrics.has_gas_resistance = true;
|
||||
|
||||
measurement->variant.environment_metrics.temperature = bme680->readTemperature();
|
||||
measurement->variant.environment_metrics.relative_humidity = bme680->readHumidity();
|
||||
measurement->variant.environment_metrics.barometric_pressure = bme680->readPressure() / 100.0F;
|
||||
measurement->variant.environment_metrics.gas_resistance = bme680->readGas() / 1000.0;
|
||||
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
void BME680Sensor::loadState()
|
||||
{
|
||||
#ifdef FSCom
|
||||
@@ -179,6 +144,5 @@ void BME680Sensor::checkStatus(const char *functionName)
|
||||
else if (bme680.sensor.status > BME68X_OK)
|
||||
LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status);
|
||||
}
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,40 +1,23 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(MESHTASTIC_BME680_HEADER)
|
||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include(<bsec2.h>)
|
||||
|
||||
#include "../mesh/generated/meshtastic/telemetry.pb.h"
|
||||
#include "TelemetrySensor.h"
|
||||
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
#include <bme68xLibrary.h>
|
||||
#include <bsec2.h>
|
||||
#else
|
||||
#include <Adafruit_BME680.h>
|
||||
#include <memory>
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
|
||||
#define STATE_SAVE_PERIOD UINT32_C(360 * 60 * 1000) // That's 6 hours worth of millis()
|
||||
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
const uint8_t bsec_config[] = {
|
||||
#include "config/bme680/bme680_iaq_33v_3s_4d/bsec_iaq.txt"
|
||||
};
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
|
||||
class BME680Sensor : public TelemetrySensor
|
||||
{
|
||||
private:
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
Bsec2 bme680;
|
||||
#else
|
||||
using BME680Ptr = std::unique_ptr<Adafruit_BME680>;
|
||||
|
||||
static BME680Ptr makeBME680(TwoWire *bus) { return std::make_unique<Adafruit_BME680>(bus); }
|
||||
|
||||
BME680Ptr bme680;
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
|
||||
protected:
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
const char *bsecConfigFileName = "/prefs/bsec.dat";
|
||||
uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE] = {0};
|
||||
uint8_t accuracy = 0;
|
||||
@@ -51,13 +34,10 @@ class BME680Sensor : public TelemetrySensor
|
||||
void loadState();
|
||||
void updateState();
|
||||
void checkStatus(const char *functionName);
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
|
||||
public:
|
||||
BME680Sensor();
|
||||
#if MESHTASTIC_BME680_BSEC2_SUPPORTED == 1
|
||||
virtual int32_t runOnce() override;
|
||||
#endif // MESHTASTIC_BME680_BSEC2_SUPPORTED
|
||||
virtual bool getMetrics(meshtastic_Telemetry *measurement) override;
|
||||
virtual bool initDevice(TwoWire *bus, ScanI2C::FoundDevice *dev) override;
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ constexpr int reconnectMax = 5;
|
||||
static uint8_t bytes[meshtastic_MqttClientProxyMessage_size + 30]; // 12 for channel name and 16 for nodeid
|
||||
|
||||
static bool isMqttServerAddressPrivate = false;
|
||||
static bool isConnected = false;
|
||||
|
||||
inline void onReceiveProto(char *topic, byte *payload, size_t length)
|
||||
{
|
||||
@@ -320,8 +321,10 @@ bool connectPubSub(const PubSubConfig &config, PubSubClient &pubSub, Client &cli
|
||||
std::string nodeId = nodeDB->getNodeId();
|
||||
const bool connected = pubSub.connect(nodeId.c_str(), config.mqttUsername, config.mqttPassword);
|
||||
if (connected) {
|
||||
isConnected = true;
|
||||
LOG_INFO("MQTT connected");
|
||||
} else {
|
||||
isConnected = false;
|
||||
LOG_WARN("Failed to connect to MQTT server");
|
||||
}
|
||||
return connected;
|
||||
@@ -507,6 +510,7 @@ bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, boo
|
||||
|
||||
void MQTT::reconnect()
|
||||
{
|
||||
isConnected = false;
|
||||
if (wantsLink()) {
|
||||
if (moduleConfig.mqtt.proxy_to_client_enabled) {
|
||||
LOG_INFO("MQTT connect via client proxy instead");
|
||||
@@ -534,7 +538,7 @@ void MQTT::reconnect()
|
||||
runASAP = true;
|
||||
reconnectCount = 0;
|
||||
isMqttServerAddressPrivate = isPrivateIpAddress(clientConnection->remoteIP());
|
||||
|
||||
isConnected = true;
|
||||
publishNodeInfo();
|
||||
sendSubscriptions();
|
||||
} else {
|
||||
@@ -688,7 +692,7 @@ void MQTT::publishNodeInfo()
|
||||
}
|
||||
void MQTT::publishQueuedMessages()
|
||||
{
|
||||
if (mqttQueue.isEmpty())
|
||||
if (mqttQueue.isEmpty() || !isConnected)
|
||||
return;
|
||||
|
||||
LOG_DEBUG("Publish enqueued MQTT message");
|
||||
@@ -895,4 +899,4 @@ void MQTT::perhapsReportToMap()
|
||||
|
||||
// Update the last report time
|
||||
last_report_to_map = millis();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "power.h"
|
||||
|
||||
#include <hal/nrf_lpcomp.h>
|
||||
|
||||
#ifdef BQ25703A_ADDR
|
||||
#include "BQ25713.h"
|
||||
@@ -389,6 +392,23 @@ void cpuDeepSleep(uint32_t msecToWake)
|
||||
nrf_gpio_cfg_sense_set(BUTTON_PIN, sense); // Apply SENSE to wake up the device from the deep sleep
|
||||
#endif
|
||||
|
||||
#ifdef BATTERY_LPCOMP_INPUT
|
||||
// Wake up if power rises again
|
||||
nrf_lpcomp_config_t c;
|
||||
c.reference = BATTERY_LPCOMP_THRESHOLD;
|
||||
c.detection = NRF_LPCOMP_DETECT_UP;
|
||||
c.hyst = NRF_LPCOMP_HYST_NOHYST;
|
||||
nrf_lpcomp_configure(NRF_LPCOMP, &c);
|
||||
nrf_lpcomp_input_select(NRF_LPCOMP, BATTERY_LPCOMP_INPUT);
|
||||
nrf_lpcomp_enable(NRF_LPCOMP);
|
||||
|
||||
battery_adcEnable();
|
||||
|
||||
nrf_lpcomp_task_trigger(NRF_LPCOMP, NRF_LPCOMP_TASK_START);
|
||||
while (!nrf_lpcomp_event_check(NRF_LPCOMP, NRF_LPCOMP_EVENT_READY))
|
||||
;
|
||||
#endif
|
||||
|
||||
auto ok = sd_power_system_off();
|
||||
if (ok != NRF_SUCCESS) {
|
||||
LOG_ERROR("FIXME: Ignoring soft device (EasyDMA pending?) and forcing system-off!");
|
||||
@@ -420,4 +440,4 @@ void enterDfuMode()
|
||||
#else
|
||||
enterUf2Dfu();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +146,20 @@ void getMacAddr(uint8_t *dmac)
|
||||
}
|
||||
}
|
||||
|
||||
std::string cleanupNameForAutoconf(std::string name)
|
||||
{
|
||||
// Convert spaces -> dashes, lowercase
|
||||
|
||||
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) {
|
||||
if (c == ' ') {
|
||||
return '-';
|
||||
}
|
||||
return (char)std::tolower(c);
|
||||
});
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/** apps run under portduino can optionally define a portduinoSetup() to
|
||||
* use portduino specific init code (such as gpioBind) to setup portduino on their host machine,
|
||||
* before running 'arduino' code.
|
||||
@@ -218,6 +232,11 @@ void portduinoSetup()
|
||||
// If LoRa `Module: auto` (default in config.yaml),
|
||||
// attempt to auto config based on Product Strings
|
||||
if (portduino_config.lora_module == use_autoconf) {
|
||||
bool found_hat = false;
|
||||
bool found_rak_eeprom = false;
|
||||
bool found_ch341 = false;
|
||||
|
||||
char hat_vendor[96] = {0};
|
||||
char autoconf_product[96] = {0};
|
||||
// Try CH341
|
||||
try {
|
||||
@@ -227,21 +246,32 @@ void portduinoSetup()
|
||||
ch341Hal->getProductString(autoconf_product, 95);
|
||||
delete ch341Hal;
|
||||
std::cout << "autoconf: Found CH341 device " << autoconf_product << std::endl;
|
||||
|
||||
found_ch341 = true;
|
||||
} catch (...) {
|
||||
std::cout << "autoconf: Could not locate CH341 device" << std::endl;
|
||||
}
|
||||
// Try Pi HAT+
|
||||
if (strlen(autoconf_product) < 6) {
|
||||
std::cout << "autoconf: Looking for Pi HAT+..." << std::endl;
|
||||
if (access("/proc/device-tree/hat/vendor", R_OK) == 0) {
|
||||
std::ifstream hatVendorFile("/proc/device-tree/hat/vendor");
|
||||
if (hatVendorFile.is_open()) {
|
||||
hatVendorFile.read(hat_vendor, 95);
|
||||
hatVendorFile.close();
|
||||
}
|
||||
}
|
||||
if (access("/proc/device-tree/hat/product", R_OK) == 0) {
|
||||
std::ifstream hatProductFile("/proc/device-tree/hat/product");
|
||||
if (hatProductFile.is_open()) {
|
||||
hatProductFile.read(autoconf_product, 95);
|
||||
hatProductFile.close();
|
||||
}
|
||||
std::cout << "autoconf: Found Pi HAT+ " << autoconf_product << " at /proc/device-tree/hat/product" << std::endl;
|
||||
std::cout << "autoconf: Found Pi HAT+ " << hat_vendor << " " << autoconf_product << " at /proc/device-tree/hat"
|
||||
<< std::endl;
|
||||
found_hat = true;
|
||||
} else {
|
||||
std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat/product" << std::endl;
|
||||
std::cout << "autoconf: Could not locate Pi HAT+ at /proc/device-tree/hat" << std::endl;
|
||||
}
|
||||
}
|
||||
// attempt to load autoconf data from an EEPROM on 0x50
|
||||
@@ -297,6 +327,7 @@ void portduinoSetup()
|
||||
autoconf_product[0] = 0x0;
|
||||
} else {
|
||||
std::cout << "autoconf: Found eeprom data " << autoconf_raw << std::endl;
|
||||
found_rak_eeprom = true;
|
||||
if (mac_start != nullptr) {
|
||||
std::cout << "autoconf: Found mac data " << mac_start << std::endl;
|
||||
if (strlen(mac_start) == 12)
|
||||
@@ -325,12 +356,29 @@ void portduinoSetup()
|
||||
if (strlen(autoconf_product) > 0) {
|
||||
// From configProducts map in PortduinoGlue.h
|
||||
std::string product_config = "";
|
||||
try {
|
||||
|
||||
if (configProducts.find(autoconf_product) != configProducts.end()) {
|
||||
product_config = configProducts.at(autoconf_product);
|
||||
} catch (std::out_of_range &e) {
|
||||
std::cerr << "autoconf: Unable to find config for " << autoconf_product << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
} else {
|
||||
if (found_hat) {
|
||||
product_config =
|
||||
cleanupNameForAutoconf("lora-hat-" + std::string(hat_vendor) + "-" + autoconf_product + ".yaml");
|
||||
} else if (found_ch341) {
|
||||
product_config = cleanupNameForAutoconf("lora-usb-" + std::string(autoconf_product) + ".yaml");
|
||||
}
|
||||
|
||||
// Don't try to automatically find config for a device with RAK eeprom.
|
||||
if (found_rak_eeprom) {
|
||||
std::cerr << "autoconf: Found unknown RAK product " << autoconf_product << std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (access((portduino_config.available_directory + product_config).c_str(), R_OK) != 0) {
|
||||
std::cerr << "autoconf: Unable to find config for " << autoconf_product << "(tried " << product_config << ")"
|
||||
<< std::endl;
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
if (loadConfig((portduino_config.available_directory + product_config).c_str())) {
|
||||
std::cout << "autoconf: Using " << product_config << " as config file for " << autoconf_product << std::endl;
|
||||
} else {
|
||||
|
||||
@@ -144,4 +144,6 @@ class Power : private concurrency::OSThread
|
||||
#endif
|
||||
};
|
||||
|
||||
void battery_adcEnable();
|
||||
|
||||
extern Power *power;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#define LED_PIN LED
|
||||
|
||||
#define USE_SSD1306 // Heltec_v3 has a SSD1306 display
|
||||
#define DISPLAY_FORCE_SMALL_FONTS
|
||||
#define DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
|
||||
#define RESET_OLED RST_OLED
|
||||
#define I2C_SDA SDA_OLED // I2C pins for this board
|
||||
|
||||
@@ -210,6 +210,16 @@ No longer populated on PCB
|
||||
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
|
||||
#define ADC_MULTIPLIER (4.916F)
|
||||
|
||||
// rf52840 AIN2 = Pin 4
|
||||
#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_2
|
||||
|
||||
// We have AIN2 with a VBAT divider so AIN2 = VBAT * (100/490)
|
||||
// We have the device going deep sleep under 3.1V, which is AIN2 = 0.63V
|
||||
// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN2 = 0.67V
|
||||
// Ratio 0.67/3.3 = 0.20, so we can pick a bit higher, 2/8 VDD, which means
|
||||
// VBAT=4.04V
|
||||
#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_2_8
|
||||
|
||||
#define HAS_RTC 0
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -267,6 +267,20 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG
|
||||
#define VBAT_AR_INTERNAL AR_INTERNAL_3_0
|
||||
#define ADC_MULTIPLIER 1.73
|
||||
|
||||
// RAK4630 AIN0 = nrf52840 AIN3 = Pin 5
|
||||
#define BATTERY_LPCOMP_INPUT NRF_LPCOMP_INPUT_3
|
||||
|
||||
// We have AIN3 with a VBAT divider so AIN3 = VBAT * (1.5/2.5)
|
||||
// We have the device going deep sleep under 3.1V, which is AIN3 = 1.86V
|
||||
// So we can wake up when VBAT>=VDD is restored to 3.3V, where AIN3 = 1.98V
|
||||
// 1.98/3.3 = 6/10, but that's close to the VBAT divider, so we
|
||||
// pick 6/8VDD, which means VBAT=4.1V.
|
||||
// Reference:
|
||||
// VDD=3.3V AIN3=5/8*VDD=2.06V VBAT=1.66*AIN3=3.41V
|
||||
// VDD=3.3V AIN3=11/16*VDD=2.26V VBAT=1.66*AIN3=3.76V
|
||||
// VDD=3.3V AIN3=6/8*VDD=2.47V VBAT=1.66*AIN3=4.1V
|
||||
#define BATTERY_LPCOMP_THRESHOLD NRF_LPCOMP_REF_SUPPLY_11_16
|
||||
|
||||
#define HAS_RTC 1
|
||||
|
||||
#define HAS_ETHERNET 1
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
#define VARIANT_MCK (64000000ul) // Master clock frequency
|
||||
#define USE_LFXO // 32.768kHz crystal for LFCLK
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Sample size change
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
#define DISPLAY_FORCE_SMALL_FONTS
|
||||
#define DISPLAY_FORCE_TOMTHUMB_FONT
|
||||
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
// Pin Capacity Definitions
|
||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[VERSION]
|
||||
major = 2
|
||||
minor = 7
|
||||
build = 14
|
||||
build = 15
|
||||
|
||||
Reference in New Issue
Block a user