1 Commits
Author SHA1 Message Date
shade 8d41f01ea5 версия 0.2.1beta: перенос строк, кривая батареи, RSSI на V4, быстрое BLE, отложенная запись на nRF52
Длинные сообщения переносятся по словам (базовый DisplayDriver + SSD1306/SH1106/ST7735/ST7789LCD/GxEPD).
Процент батареи по кривой разряда Li-ion вместо линейной формулы (обе формулы UI унифицированы).
RSSI_OFFSET для Heltec V4 (компенсация усиления LNA GC1109).
BLE: MTU 512, 2M PHY, интервал 15 мс, GAP-вызовы вне колбэка (фриз ~10 с), fast mode с форвардером и авто-возвратом в экономный режим.
nRF52: отложенная запись prefs/контактов/каналов, пока подключено приложение; reboot/powerOff через MyMesh (в т.ч. из UITask::shutdown).
E-ink: периодический полный refresh (EINK_MAX_PARTIAL_REFRESH=60), квадратные иконки в drawXbm.
Платы: таблица разделов partitions/esp32s3_16mb.csv для Station G2 / G3 / T-Beam 1W (spiffs на прежних адресах).
Ядро: num_channels в setChannel, guard перед data[len]=0, Packet::_rssi, uf2conv на пустой строке, предупреждение в build.sh.
2026-10-16 08:45:33 +03:00
38 changed files with 681 additions and 54 deletions

No files matched your search

+26
View File
@@ -71,6 +71,32 @@ Heltec T114 и T190, ThinkNode M9, Meshadventurer на E22. Ниже — что
- В набор сборки входят 26 прошивок: 15 companion, 8 репетитеров и 3 room server /
sensor. Как собрать и опубликовать — [`ci/README.md`](./ci/README.md).
### Версия 0.2.1beta
Внутренняя сборка для проверки. Что в ней появилось:
- **Длинные сообщения переносятся по словам** на монохромных экранах и на
чёрно-белых (e-ink): раньше строка уходила за край. Очень длинное слово
(например, ссылка) режется по ширине, текст не залезает под нижний край экрана.
- **Процент батареи считается по кривой разряда Li-ion**, а не по прямой линии от
3000 до 4200 мВ. Ниже 3.8 В показания стали заметно честнее — это видно и в
строке состояния, и на странице POWER.
- **Правильный уровень сигнала на платах с усилителем** (Heltec V4): раньше
измеренное значение было завышено примерно на 17 дБ. Теперь и в приложении, и на
странице поиска репитеров цифры соответствуют реальности.
- **Быстрый Bluetooth:** синхронизация контактов и каналов идёт заметно быстрее, и
исчезла задержка около 10 секунд, которая возникала сразу после подключения
телефона. Когда приложение ничего не запрашивает, соединение само переходит в
экономный режим, чтобы не расходовать батарею.
- **Устройства на nRF52 больше не подвисают при сохранении настроек**, если
подключено приложение: запись откладывается до отключения телефона.
- **Чёрно-белые экраны реже «замыливаются»:** раз в 60 обновлений проходит полная
очистка, а иконки больше не растянуты по ширине.
- **Исправления:** после загрузки каналов первый свой канал больше не затирается
новым; очень длинное входящее сообщение больше не может привести к сбою; у плат с
16 МБ флеша (Station G2, Station G3, T-Beam 1W) обновление прошивки больше не
стирает настройки, контакты и каналы.
---
Дальше — оригинальный README проекта MeshCore. Он описывает базу и остаётся справедливым
+1 -1
View File
@@ -166,7 +166,7 @@ def convert_from_hex_to_uf2(buf):
currblock = None
blocks = []
for line in buf.split('\n'):
if line[0] != ":":
if not line or line[0] != ":":
continue
i = 1
rec = []
+1 -1
View File
@@ -169,7 +169,7 @@ build_firmware() {
# build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2)
if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then
python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840
python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840 || echo "WARNING: uf2conv failed for $1 — .uf2 не создан"
cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
fi
+125 -11
View File
@@ -110,6 +110,14 @@
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
#define LAZY_CONTACTS_WRITE_DELAY 5000
// Пока BLE-соединение активно, запись во флеш на nRF52 не делаем вообще: повторяем
// попытку с этим интервалом, «грязный» флаг сохраняется (см. MyMesh::loop()).
#define BLE_BUSY_RETRY_MILLIS 1000
// Сколько ждать после обрыва BLE, прежде чем писать во флеш / делать reboot.
#define BLE_DISCONNECT_WAIT_MILLIS 500
// Крайний срок для отложенного reboot/powerOff: если BLE так и не отпустит радио,
// перезагружаемся/выключаемся без записи (лучше потерять настройку, чем зависнуть).
#define PENDING_REBOOT_DEADLINE_MILLIS 10000
#define PUBLIC_GROUP_PSK "izOH6cXN6mrJ5e26oRXNcg=="
@@ -905,6 +913,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) {
_iter_started = false;
_cli_rescue = false;
_ble_serial = NULL;
cli_command[0] = 0;
offline_queue_len = 0;
app_target_ver = 0;
@@ -912,6 +921,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
next_ack_idx = 0;
sign_data = NULL;
dirty_contacts_expiry = 0;
dirty_prefs_expiry = 0;
dirty_channels_expiry = 0;
_pending_reboot_at = 0;
_pending_reboot_deadline = 0;
_pending_reboot_restart = false;
memset(advert_paths, 0, sizeof(advert_paths));
memset(send_scope.key, 0, sizeof(send_scope.key));
send_unscoped = false;
@@ -1284,6 +1298,7 @@ void MyMesh::handleCmdFrame(size_t len) {
_iter = startContactsIterator();
_iter_started = true;
_most_recent_lastmod = 0;
_serial->setFastMode(true); // bulk contact streaming: switch BLE to the sync interval
}
} else if (cmd_frame[0] == CMD_SET_ADVERT_NAME && len >= 2) {
int nlen = len - 1;
@@ -1442,6 +1457,7 @@ void MyMesh::handleCmdFrame(size_t len) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
}
} else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) {
_serial->setFastMode(true); // keep the sync interval while the app drains the message queue
int out_len;
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
_serial->writeFrame(out_frame, out_len);
@@ -1539,10 +1555,8 @@ void MyMesh::handleCmdFrame(size_t len) {
writeOKFrame();
}
} else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed?
saveContacts();
}
board.reboot();
// requestReboot() сам сбросит отложенную запись (и при занятом BLE отложит reboot)
requestReboot(true);
} else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) {
uint8_t reply[11];
int i = 0;
@@ -1779,6 +1793,7 @@ void MyMesh::handleCmdFrame(size_t len) {
stopConnection(pub_key);
writeOKFrame();
} else if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2) {
_serial->setFastMode(true); // apps pull every channel at once: stay in the sync interval
uint8_t channel_idx = cmd_frame[1];
ChannelDetails channel;
if (getChannel(channel_idx, channel)) {
@@ -2127,12 +2142,12 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char*
}
if (strcmp(command, "reboot") == 0) {
board.reboot(); // doesn't return
requestReboot(true); // сбросит отложенную запись; может не вернуться
return true;
}
if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
board.powerOff(); // doesn't return
requestReboot(false); // сбросит отложенную запись; может не вернуться
return true;
}
@@ -2272,9 +2287,9 @@ void MyMesh::checkCLIRescueCmd() {
bool success = _store->formatFileSystem();
if (success) {
_store->saveMainIdentity(self_id);
savePrefs();
savePrefsNow(); // файловая система только что пересоздана — пишем сразу, не откладывая
saveContacts();
saveChannels();
saveChannelsNow();
Serial.println(" > erase and rebuild done");
} else {
Serial.println(" Error: erase failed");
@@ -2405,7 +2420,7 @@ void MyMesh::checkCLIRescueCmd() {
}
} else if (strcmp(cli_command, "reboot") == 0) {
board.reboot(); // doesn't return
requestReboot(true); // сбросит отложенную запись
} else {
Serial.println(" Error: unknown command");
}
@@ -2435,6 +2450,7 @@ void MyMesh::checkSerialInterface() {
4); // include the most recent lastmod, so app can update their 'since'
_serial->writeFrame(out_frame, 5);
_iter_started = false;
_serial->setFastMode(false); // contact sync finished: dial BLE back to the idle interval
}
//} else if (!_serial->isWriteBusy()) {
// checkConnections(); // TODO - deprecate the 'Connections' stuff
@@ -2455,10 +2471,105 @@ void MyMesh::loop() {
#endif
}
// Отложенная запись prefs/contacts/channels. На nRF52 flash_nrf5x берёт семафор и ждёт
// окно radio-idle, которого при активном BLE-соединении (S140 v6.1.1) может не быть
// вовсе, поэтому пока BLE занят — только переносим срок, флаг «грязно» не теряем.
bool ble_busy = isFlashBlockedByBle();
// is there are pending dirty contacts write needed?
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
saveContacts();
if (ble_busy) {
dirty_contacts_expiry = futureMillis(BLE_BUSY_RETRY_MILLIS);
} else {
dirty_contacts_expiry = 0;
saveContacts();
}
}
// is there a pending dirty prefs write needed?
if (dirty_prefs_expiry && millisHasNowPassed(dirty_prefs_expiry)) {
if (ble_busy) {
dirty_prefs_expiry = futureMillis(BLE_BUSY_RETRY_MILLIS);
} else {
savePrefsNow();
}
}
// is there a pending dirty channels write needed?
if (dirty_channels_expiry && millisHasNowPassed(dirty_channels_expiry)) {
if (ble_busy) {
dirty_channels_expiry = futureMillis(BLE_BUSY_RETRY_MILLIS);
} else {
dirty_channels_expiry = 0;
saveChannelsNow();
}
}
servicePendingReboot();
}
// Записать отложенные prefs/contacts/channels немедленно. false — запись сейчас
// невозможна (BLE занимает радио), флаги при этом не сбрасываются.
bool MyMesh::flushPendingWrites() {
if (isFlashBlockedByBle()) return false;
if (dirty_contacts_expiry) {
dirty_contacts_expiry = 0;
saveContacts();
}
if (dirty_channels_expiry) {
dirty_channels_expiry = 0;
saveChannelsNow();
}
if (dirty_prefs_expiry) {
savePrefsNow();
}
return true;
}
// reboot/powerOff с оглядкой на отложенную запись: если радио занято BLE, действие
// откладывается, loop() освободит канал и только потом перезагрузит/выключит плату.
void MyMesh::requestReboot(bool restart) {
_pending_reboot_restart = restart;
if (hasPendingWrites() && isFlashBlockedByBle()) {
_pending_reboot_at = futureMillis(BLE_DISCONNECT_WAIT_MILLIS);
_pending_reboot_deadline = futureMillis(PENDING_REBOOT_DEADLINE_MILLIS);
return;
}
_pending_reboot_at = 0;
_pending_reboot_deadline = 0;
flushPendingWrites();
if (_pending_reboot_restart) {
board.reboot(); // doesn't return
} else {
board.powerOff(); // doesn't return
}
}
void MyMesh::servicePendingReboot() {
if (!_pending_reboot_at || !millisHasNowPassed(_pending_reboot_at)) return;
if (isFlashBlockedByBle() && hasPendingWrites()) {
if (!millisHasNowPassed(_pending_reboot_deadline)) {
// Рвём связь, как это делает CMD_FACTORY_RESET: SoftDevice отпускает радио, и
// запись во флеш перестаёт ждать окно radio-idle. Через BLE_DISCONNECT_WAIT_MILLIS
// придём сюда снова — уже с погашенным соединением.
if (_serial) _serial->disable();
_pending_reboot_at = futureMillis(BLE_DISCONNECT_WAIT_MILLIS);
return;
}
MESH_DEBUG_PRINTLN("reboot: BLE still connected, giving up on pending flash write");
}
_pending_reboot_at = 0;
_pending_reboot_deadline = 0;
flushPendingWrites();
if (_pending_reboot_restart) {
board.reboot(); // doesn't return
} else {
board.powerOff(); // doesn't return
}
}
@@ -2479,5 +2590,8 @@ bool MyMesh::advert() {
// To check if there is pending work
bool MyMesh::hasPendingWork() const {
return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0;
// отложенную запись prefs здесь специально не учитываем: пока BLE-соединение активно,
// она может ждать часами и main.cpp перестал бы усыплять nRF52. sleep() на nRF52 — это
// WFE по событию, loop() тикает и запись сделает вовремя.
return _mgr->getOutboundTotal() > 0 || dirty_contacts_expiry != 0 || _pending_reboot_at != 0;
}
+88 -1
View File
@@ -77,6 +77,21 @@
/* -------------------------------------------------------------------------------------- */
// Задержка отложенной записи prefs во флеш (мс). На nRF52 запись во флеш берёт семафор и
// ждёт окно radio-idle, которого при активном BLE-соединении (S140 v6.1.1) может не быть
// вовсе — прямой вызов savePrefs() из UI подвешивает loop(). Поэтому savePrefs() только
// помечает данные «грязными», а пишет их MyMesh::loop(), когда канал свободен.
// (Для контактов та же схема, задержка LAZY_CONTACTS_WRITE_DELAY — в MyMesh.cpp.)
#ifndef LAZY_PREFS_WRITE_DELAY
#define LAZY_PREFS_WRITE_DELAY 2000
#endif
// То же для каналов (CMD_SET_CHANNEL от приложения): файл каналов пишется не сразу, а из
// loop(); задержка короче контактной, потому что каналы меняются поштучно и редко.
#ifndef LAZY_CHANNELS_WRITE_DELAY
#define LAZY_CHANNELS_WRITE_DELAY 2000
#endif
#define REQ_TYPE_GET_STATUS 0x01 // same as _GET_STATS
#define REQ_TYPE_KEEP_ALIVE 0x02
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
@@ -200,13 +215,63 @@ protected:
}
public:
// Отложенная запись prefs: метод только помечает данные «грязными» и ставит срок,
// реальную запись во флеш делает loop() (см. flushPendingWrites()). Так UI продолжает
// звать savePrefs() как раньше, но на nRF52 запись не попадает в момент, когда радио
// занято BLE-соединением (иначе flash_nrf5x ждёт окно radio-idle и loop() виснет).
void savePrefs() {
#if defined(NRF52_PLATFORM)
if (_ble_serial == NULL) { // BLE в сборке нет — писать можно сразу, как и раньше
savePrefsNow();
} else {
dirty_prefs_expiry = futureMillis(LAZY_PREFS_WRITE_DELAY);
}
#else
savePrefsNow(); // остальные платформы: как и раньше, запись сразу
#endif
}
// Реальная запись prefs в файл. Вызывать только когда канал свободен
// (то есть isFlashBlockedByBle() == false).
void savePrefsNow() {
_prefs.node_lat = sensors.node_lat;
_prefs.node_lon = sensors.node_lon;
_store->savePrefs(_prefs);
_prefs.clearDirty();
dirty_prefs_expiry = 0;
}
// Есть ли отложенная (ещё не записанная) запись — нужно, чтобы не потерять её
// при reboot/powerOff.
bool hasPendingWrites() const {
return dirty_prefs_expiry != 0 || dirty_contacts_expiry != 0 || dirty_channels_expiry != 0;
}
// true, если писать во флеш прямо сейчас нельзя: nRF52 с активным BLE-соединением.
// Смотрим именно на BLE-интерфейс: у USB isConnected() всегда true, и по нему мы бы
// вообще перестали писать во флеш (см. MyMesh::setBleSerialInterface()).
bool isFlashBlockedByBle() const {
#if defined(NRF52_PLATFORM)
return _ble_serial != NULL && _ble_serial->isConnected();
#else
return false;
#endif
}
// main.cpp сообщает BLE-интерфейс сразу после его создания (только в сборках с BLE).
void setBleSerialInterface(BaseSerialInterface* ble) { _ble_serial = ble; }
// Записать всё «грязное» немедленно (в обход LAZY_*_WRITE_DELAY). Возвращает false,
// если запись сейчас невозможна — тогда флаги остаются, и запись сделает loop().
bool flushPendingWrites();
// reboot/powerOff с оглядкой на отложенную запись: если радио занято BLE, действие
// откладывается, loop() освободит канал (рвёт BLE, как CMD_FACTORY_RESET), запишет
// prefs/contacts и только потом перезагрузит/выключит плату.
// ЭТИМ методом должна пользоваться и UI-страница POWER вместо прямых
// _board->reboot()/_board->powerOff() — иначе отложенная запись теряется.
void requestReboot(bool restart);
// Ручная настройка радио (экран RADIO в ui-new). Проверяет допустимые
// диапазоны, применяет параметры к радио и обновляет prefs в памяти;
// запись в файл — только через savePrefs().
@@ -248,9 +313,25 @@ private:
bool isValidClientRepeatFreq(uint32_t f) const;
// helpers, short-cuts
void saveChannels() { _store->saveChannels(this); }
// Отложенная запись каналов: как и у prefs — на nRF52 с BLE файл пишет loop(), когда
// радио свободно; в остальных случаях (и в сборках без BLE) пишем сразу, как раньше.
void saveChannels() {
#if defined(NRF52_PLATFORM)
if (_ble_serial == NULL) {
saveChannelsNow();
} else {
dirty_channels_expiry = futureMillis(LAZY_CHANNELS_WRITE_DELAY);
}
#else
saveChannelsNow();
#endif
}
void saveChannelsNow() { _store->saveChannels(this); }
void saveContacts();
// Обслуживание отложенного reboot/powerOff (см. loop()); сам запрос — requestReboot().
void servicePendingReboot();
DataStore* _store;
NodePrefs _prefs;
uint32_t pending_login;
@@ -258,6 +339,7 @@ private:
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
uint32_t pending_req; // pending _BINARY_REQ
BaseSerialInterface *_serial;
BaseSerialInterface *_ble_serial; // только BLE-интерфейс (может быть NULL)
Listener* _listener;
ContactsIterator _iter;
@@ -273,6 +355,11 @@ private:
uint8_t *sign_data;
uint32_t sign_data_len;
unsigned long dirty_contacts_expiry;
unsigned long dirty_prefs_expiry;
unsigned long dirty_channels_expiry;
unsigned long _pending_reboot_at; // срок отложенного reboot/powerOff, 0 = нет
unsigned long _pending_reboot_deadline;// крайний срок ожидания записи при отложенном reboot
bool _pending_reboot_restart; // true = reboot, false = powerOff
TransportKey send_scope;
+3
View File
@@ -208,6 +208,9 @@ void setup() {
#if defined(BLE_PIN_CODE)
bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface);
// сообщаем mesh'у именно BLE-интерфейс: на nRF52 отложенная запись во флеш ждёт,
// пока он не подключён (у USB isConnected() всегда true, ориентироваться на него нельзя)
the_mesh.setBleSerialInterface(&bluetooth_interface);
#endif
// add wifi interface
+46 -17
View File
@@ -49,6 +49,44 @@
#define NOTIFY_MAIN_ROWS 8 // строк в меню уведомлений (включая Timezone)
#define NOTIFY_ROW_TZ 5 // строка Timezone (стоит перед Quiet hours)
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
// Процент заряда батареи. По умолчанию — по кривой разряда Li-ion (4.2 В):
// линейная формула от 3000 до 4200 мВ на 3.7 В врёт на 10–15 %, а процент виден
// и в статус-баре, и на странице POWER. Для другой химии: -D BATT_CURVE_LINEAR.
static int battPercentage(uint16_t milliVolts) {
#ifdef BATT_CURVE_LINEAR
int pct = ((int)milliVolts - BATT_MIN_MILLIVOLTS) * 100 / (BATT_MAX_MILLIVOLTS - BATT_MIN_MILLIVOLTS);
#else
static const uint16_t curve_v[] = { 4180, 4060, 3980, 3880, 3800, 3740, 3680, 3620, 3570, 3530, 3480, 3360, 3000 };
static const uint8_t curve_p[] = { 100, 90, 80, 70, 60, 50, 40, 30, 20, 15, 10, 5, 0 };
const int n = (int)(sizeof(curve_v) / sizeof(curve_v[0]));
int pct;
if (milliVolts >= curve_v[0]) {
pct = 100;
} else if (milliVolts <= curve_v[n - 1]) {
pct = 0;
} else {
pct = 0;
for (int i = 0; i < n - 1; i++) {
if (milliVolts <= curve_v[i] && milliVolts > curve_v[i + 1]) {
pct = (int)curve_p[i + 1]
+ (int)((milliVolts - curve_v[i + 1]) * (curve_p[i] - curve_p[i + 1])) / (int)(curve_v[i] - curve_v[i + 1]);
break;
}
}
}
#endif
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
return pct;
}
// Строка меню: выбранная — инверсией, значение — справа вторичным цветом
static void drawMenuRow(DisplayDriver& display, int y, bool sel, const char* label, const char* value) {
if (sel) {
@@ -312,18 +350,7 @@ class HomeScreen : public UIScreen {
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
#ifndef BATT_MIN_MILLIVOLTS
#define BATT_MIN_MILLIVOLTS 3000
#endif
#ifndef BATT_MAX_MILLIVOLTS
#define BATT_MAX_MILLIVOLTS 4200
#endif
const int minMilliVolts = BATT_MIN_MILLIVOLTS;
const int maxMilliVolts = BATT_MAX_MILLIVOLTS;
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
const int batteryPercentage = battPercentage(batteryMilliVolts);
// battery icon
// что показывать рядом с батареей — напряжение, процент или ничего (страница POWER)
@@ -1188,9 +1215,7 @@ uint16_t HomeScreen::battMilliVolts() {
void HomeScreen::renderPowerPage(DisplayDriver& display, uint16_t mv) {
char tmp[24];
int pct = ((int)mv - BATT_MIN_MILLIVOLTS) * 100 / (BATT_MAX_MILLIVOLTS - BATT_MIN_MILLIVOLTS);
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
const int pct = battPercentage(mv);
// Крупная иконка батареи, справа — заряд и напряжение.
// Сверху 12 px занимает статус-бар, под ним (13..17) — точки страниц,
@@ -1959,15 +1984,19 @@ void UITask::shutdown(bool restart){
#endif // PIN_BUZZER
// Перезагрузку и выключение просим у MyMesh, а не у платы напрямую: на nRF52
// запись prefs/контактов отложена, пока подключено приложение, и MyMesh сам
// рвёт связь, сохраняет данные и только потом перезагружает или выключает.
// Прямой вызов board потерял бы последнюю настройку (тему, тихие часы, радио).
if (restart) {
_board->reboot();
the_mesh.requestReboot(true);
} else {
// перед выключением гасим экран, иначе на некоторых панелях остаётся картинка
display.forceFullRefresh();
display.clear();
display.endFrame();
// Power off board including radio, display, GPS and components
_board->powerOff();
the_mesh.requestReboot(false);
}
}
+22
View File
@@ -0,0 +1,22 @@
# Таблица разделов для плат на ESP32-S3 с 16 МБ флеша (Station G2 / G3, T-Beam 1W).
#
# Зачем: в board json этих плат указано 16 МБ, но своей таблицы разделов нет, и
# PlatformIO молча брал arduino default.csv — раскладку на 4 МБ с приложением
# 1.25 МБ, куда сборка компаньона уже не влезает.
#
# Раскладка ниже держит nvs, otadata, spiffs и coredump ровно на тех же адресах,
# что и default.csv. Поэтому у уже прошитых узлов после обновления остаются
# настройки и содержимое LittleFS (identity, контакты, каналы). Растёт только
# app0 — за счёт второго OTA-слота.
#
# Второй OTA-слот здесь лишний: в сборках MeshCore для ESP32 нет обновления по
# воздуху (код обновления есть только в ветке nRF52), app1 никогда не использовался.
#
# app0: 0x280000 = 2.5 МБ (было 1.25 МБ).
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x280000,
spiffs, data, spiffs, 0x290000, 0x160000,
coredump, data, coredump,0x3F0000, 0x10000,
1 # Таблица разделов для плат на ESP32-S3 с 16 МБ флеша (Station G2 / G3, T-Beam 1W).
2 #
3 # Зачем: в board json этих плат указано 16 МБ, но своей таблицы разделов нет, и
4 # PlatformIO молча брал arduino default.csv — раскладку на 4 МБ с приложением
5 # 1.25 МБ, куда сборка компаньона уже не влезает.
6 #
7 # Раскладка ниже держит nvs, otadata, spiffs и coredump ровно на тех же адресах,
8 # что и default.csv. Поэтому у уже прошитых узлов после обновления остаются
9 # настройки и содержимое LittleFS (identity, контакты, каналы). Растёт только
10 # app0 — за счёт второго OTA-слота.
11 #
12 # Второй OTA-слот здесь лишний: в сборках MeshCore для ESP32 нет обновления по
13 # воздуху (код обновления есть только в ветке nRF52), app1 никогда не использовался.
14 #
15 # app0: 0x280000 = 2.5 МБ (было 1.25 МБ).
16 #
17 # Name, Type, SubType, Offset, Size, Flags
18 nvs, data, nvs, 0x9000, 0x5000,
19 otadata, data, ota, 0xe000, 0x2000,
20 app0, app, ota_0, 0x10000, 0x280000,
21 spiffs, data, spiffs, 0x290000, 0x160000,
22 coredump, data, coredump,0x3F0000, 0x10000,
+2
View File
@@ -204,6 +204,7 @@ void Dispatcher::checkRecv() {
} else {
if (tryParsePacket(pkt, raw, len)) {
pkt->_snr = _radio->getLastSNR() * 4.0f;
pkt->_rssi = (int8_t)_radio->getLastRSSI();
score = _radio->packetScore(_radio->getLastSNR(), len);
air_time = _radio->getEstAirtimeFor(len);
rx_air_time += air_time;
@@ -360,6 +361,7 @@ Packet* Dispatcher::obtainNewPacket() {
} else {
pkt->payload_len = pkt->path_len = 0;
pkt->_snr = 0;
pkt->_rssi = 0;
}
return pkt;
}
+2
View File
@@ -8,6 +8,8 @@ Packet::Packet() {
header = 0;
path_len = 0;
payload_len = 0;
_snr = 0;
_rssi = 0;
}
bool Packet::isValidPathLen(uint8_t path_len) {
+2
View File
@@ -49,6 +49,7 @@ public:
uint8_t path[MAX_PATH_SIZE];
uint8_t payload[MAX_PACKET_PAYLOAD];
int8_t _snr;
int8_t _rssi; // уровень приёма, дБм: нужен тем, кто обрабатывает пакет позже приёма
/**
* \brief calculate the hash of payload + type
@@ -90,6 +91,7 @@ public:
bool isMarkedDoNotRetransmit() const { return header == 0xFF; }
float getSNR() const { return ((float)_snr) / 4.0f; }
float getRSSI() const { return (float)_rssi; } // дБм
/**
* \returns the encoded/wire format length of this packet
+11
View File
@@ -234,6 +234,10 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
// len can be > original length, but 'text' will be padded with zeroes
if (len >= MAX_PACKET_PAYLOAD) { // иначе нулевой байт уйдёт за границу буфера
MESH_DEBUG_PRINTLN("onPeerDataRecv: dropping oversized payload len=%d", (uint32_t)len);
return;
}
data[len] = 0; // need to make a C string again, with null terminator
if (flags == TXT_TYPE_PLAIN) {
@@ -424,6 +428,10 @@ void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mes
memcpy(&timestamp, data, 4);
// len can be > original length, but 'text' will be padded with zeroes
if (len >= MAX_PACKET_PAYLOAD) { // иначе нулевой байт уйдёт за границу буфера
MESH_DEBUG_PRINTLN("onGroupDataRecv: dropping oversized payload len=%d", (uint32_t)len);
return;
}
data[len] = 0; // need to make a C string again, with null terminator
// notify UI of this new message
@@ -935,6 +943,9 @@ bool BaseChatMesh::setChannel(int idx, const ChannelDetails& src) {
if (idx >= 0 && idx < MAX_GROUP_CHANNELS) {
channels[idx] = src;
// loadChannels() пишет каналы через setChannel(), поэтому счётчик надо подтягивать:
// иначе num_channels остаётся 1 и следующий addChannel() затирает первый канал.
if (idx >= num_channels) num_channels = idx + 1;
if (memcmp(&src.channel.secret[16], zeroes, 16) == 0) {
mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), src.channel.secret, 16); // 128-bit key
} else {
+9
View File
@@ -19,4 +19,13 @@ public:
virtual bool isWriteBusy() const = 0;
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
virtual size_t checkRecvFrame(uint8_t dest[]) = 0;
/**
* Optional: dynamically switch the transport between a fast and an idle mode.
* fast=true - tight connection interval, for bulk sync (contact/channel streaming)
* fast=false - relaxed interval, for idle/messaging (lower power on both sides)
* Transports that have no such knob (USB, WiFi, plain serial) keep the default
* no-op, so callers never have to know which transport is actually in use.
*/
virtual void setFastMode(bool fast) {}
};
+11
View File
@@ -157,6 +157,17 @@ public:
return false;
}
// forward the fast/idle mode switch to every wrapped interface.
// Interfaces without such a mode (USB, WiFi, plain serial) inherit the no-op
// default from BaseSerialInterface, so forwarding to all of them is safe.
void setFastMode(bool fast) override {
for(auto iface : _interfaces){
if(iface.instance){
iface.instance->setFastMode(fast);
}
}
}
size_t writeFrame(const uint8_t src[], size_t len) override {
// don't write when disabled or nothing provided
if(!_enabled || len == 0){
+114 -2
View File
@@ -1,5 +1,6 @@
#include "SerialBLEInterface.h"
#include "esp_mac.h"
#include <esp_idf_version.h>
// See the following for generating UUIDs:
// https://www.uuidgenerator.net/
@@ -10,6 +11,13 @@
#define ADVERT_RESTART_DELAY 1000 // millis
// IDF 5.x fixed the spelling of these GAP calls ("prefered" -> "preferred"),
// and the old names were removed. ESP32-C6 builds use IDF 5.x, the rest IDF 4.4.
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0)
#define esp_ble_gap_set_prefered_default_phy esp_ble_gap_set_preferred_default_phy
#define esp_ble_gap_set_prefered_phy esp_ble_gap_set_preferred_phy
#endif
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
_pin_code = pin_code;
@@ -26,7 +34,13 @@ void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
// Create the BLE Device
BLEDevice::init(dev_name);
BLEDevice::setSecurityCallbacks(this);
BLEDevice::setMTU(MAX_FRAME_SIZE);
// 512 rather than MAX_FRAME_SIZE: the ATT payload is MTU - 3, so a MAX_FRAME_SIZE
// setting left our longest frames one packet short of a single notification/write.
BLEDevice::setMTU(512);
// Prefer 2M PHY for every future connection (BLE 5.0, doubles the air data rate).
// Peers that don't support it negotiate 1M PHY automatically.
esp_ble_gap_set_prefered_default_phy(ESP_BLE_GAP_PHY_2M_PREF_MASK, ESP_BLE_GAP_PHY_2M_PREF_MASK);
BLESecurity sec;
sec.setStaticPIN(pin_code);
@@ -95,6 +109,19 @@ void SerialBLEInterface::onConnect(BLEServer* pServer) {
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
// Only *schedule* the connection parameter / PHY update here, and apply it from
// checkRecvFrame(). This callback runs in the Bluedroid BT task, and calling BLE
// API functions re-entrantly from a BT task callback deadlocks its command queue,
// which freezes the connection for ~10 seconds.
memcpy(_pending_conn_bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
// The MTU exchange needs 1-2 connection events at the default interval, 100ms is ample.
_conn_params_update_time = millis() + 100;
// The PHY update goes 300ms later, so the two LL procedures never overlap.
_phy_update_time = millis() + 400;
// New session starts in fast mode; the app/mesh calls setFastMode(false) once idle.
_fast_mode = true;
_mode_change_time = 0;
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
@@ -162,6 +189,10 @@ void SerialBLEInterface::disable() {
pServer->disconnect(last_conn_id);
pService->stop();
oldDeviceConnected = deviceConnected = false;
_conn_params_update_time = 0; // no peer left for the deferred GAP procedures
_phy_update_time = 0;
_mode_change_time = 0;
_fast_mode = false;
adv_restart_time = 0;
}
@@ -186,13 +217,88 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
return 0;
}
#define BLE_WRITE_MIN_INTERVAL 60
// Пауза между BLE-уведомлениями. 15 мс = верхняя граница быстрого интервала
// соединения: успеваем отдать кадр на каждом событии связи и не переполняем
// очередь Bluedroid (в ней всего 4 кадра, а потеря кадра при синхронизации
// контактов означает расхождение списка у приложения).
#define BLE_WRITE_MIN_INTERVAL 15
// Простой записей, после которого соединение само возвращается в экономный режим.
#define BLE_FAST_MODE_IDLE_MILLIS 10000
// Switch the connection between sync speed and idle speed. The actual GAP call is
// deferred to checkRecvFrame(): see onConnect() for why.
void SerialBLEInterface::setFastMode(bool fast) {
if (fast == _fast_mode && _mode_change_time == 0) return; // nothing to do, nothing pending
_fast_mode = fast;
if (!deviceConnected) { // no peer to negotiate with; the next connect starts in fast mode
_mode_change_time = 0;
return;
}
if (fast) {
// Apply fast params quickly (20ms), so a sync starts at full speed.
if (_conn_params_update_time == 0 && _phy_update_time == 0) {
_mode_change_time = millis() + 20;
} else {
_mode_change_time = 0; // the initial setup already asks for fast params
}
} else {
// 2s cooldown before dialing back: lets in-flight frames clear and avoids
// oscillating when the app re-requests data immediately after.
_mode_change_time = millis() + 2000;
}
}
bool SerialBLEInterface::isWriteBusy() const {
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
}
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
// Apply the GAP procedures deferred by onConnect()/setFastMode(). One procedure per
// pass (else-if chain): two LL procedures back-to-back also wedge the Bluedroid queue.
if (deviceConnected) {
if (_conn_params_update_time && millis() >= _conn_params_update_time) {
_conn_params_update_time = 0;
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, _pending_conn_bda, sizeof(esp_bd_addr_t));
conn_params.min_int = 0x06; // 7.5ms (units of 1.25ms) - spec minimum, Android honours it
conn_params.max_int = 0x0C; // 15ms - iOS foreground floor; the stack picks what it accepts
conn_params.latency = 0;
conn_params.timeout = 0x01F0; // ~5s (units of 10ms), must stay above 2 x max interval
esp_ble_gap_update_conn_params(&conn_params);
} else if (_phy_update_time && millis() >= _phy_update_time) {
_phy_update_time = 0;
esp_ble_gap_set_prefered_phy(_pending_conn_bda, 0,
ESP_BLE_GAP_PHY_2M_PREF_MASK, ESP_BLE_GAP_PHY_2M_PREF_MASK,
ESP_BLE_GAP_PHY_OPTIONS_NO_PREF);
} else if (_mode_change_time && millis() >= _mode_change_time
&& _conn_params_update_time == 0 && _phy_update_time == 0) {
_mode_change_time = 0;
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, _pending_conn_bda, sizeof(esp_bd_addr_t));
if (_fast_mode) {
conn_params.min_int = 0x06; // 7.5ms - full speed for contact/channel streaming
conn_params.max_int = 0x0C; // 15ms
} else {
conn_params.min_int = 0x24; // 45ms - responsive while idle
conn_params.max_int = 0x48; // 90ms - power friendly, latency stays unnoticeable
}
conn_params.latency = 0;
conn_params.timeout = 0x01F0; // ~5s (units of 10ms)
esp_ble_gap_update_conn_params(&conn_params);
}
// Если приложение долго ничего не запрашивало — сами возвращаемся в экономный
// режим. Иначе fast (7.5–15 мс) остаётся до конца сессии: приложения опрашивают
// сообщения периодически, и каждый опрос продлевал бы быстрое соединение.
if (_fast_mode && _mode_change_time == 0
&& _conn_params_update_time == 0 && _phy_update_time == 0
&& millis() - _last_write > BLE_FAST_MODE_IDLE_MILLIS) {
setFastMode(false);
}
}
if (send_queue_len > 0 // first, check send queue
&& millis() >= _last_write + BLE_WRITE_MIN_INTERVAL // space the writes apart
) {
@@ -218,6 +324,12 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
if (deviceConnected != oldDeviceConnected) {
if (!deviceConnected) { // disconnecting
clearBuffers();
// Drop the deferred GAP procedures and leave fast mode: the connection they
// were queued for is gone, and the next connect schedules its own setup.
_conn_params_update_time = 0;
_phy_update_time = 0;
_mode_change_time = 0;
_fast_mode = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting...");
+13
View File
@@ -1,6 +1,7 @@
#pragma once
#include "../BaseSerialInterface.h"
#include <esp_gap_ble_api.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
@@ -19,6 +20,11 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
uint32_t _pin_code;
unsigned long _last_write;
unsigned long adv_restart_time;
unsigned long _conn_params_update_time; // 0 = no pending connection parameter update
unsigned long _phy_update_time; // 0 = no pending PHY update (staggered after conn params)
unsigned long _mode_change_time; // 0 = no pending fast/idle mode change
bool _fast_mode; // true = sync interval, false = idle interval
esp_bd_addr_t _pending_conn_bda; // peer address the pending updates are meant for
struct Frame {
uint8_t len;
@@ -65,6 +71,11 @@ public:
FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state
);
send_queue_len = 0;
_conn_params_update_time = 0;
_phy_update_time = 0;
_mode_change_time = 0;
_fast_mode = true;
memset(_pending_conn_bda, 0, sizeof(_pending_conn_bda));
}
/**
@@ -82,6 +93,8 @@ public:
bool isConnected() const override;
void setFastMode(bool fast) override;
bool isWriteBusy() const override;
size_t writeFrame(const uint8_t src[], size_t len) override;
size_t checkRecvFrame(uint8_t dest[]) override;
+9 -2
View File
@@ -4,6 +4,13 @@
#include "RadioLibWrappers.h"
#include "SX126xReset.h"
// RSSI_OFFSET компенсирует усиление внешнего LNA (например, GC1109 на Heltec V4):
// задаётся отрицательным, равным усилению LNA, тогда в приложение и на экран
// попадает реальный уровень сигнала. Для плат без LNA оставляем 0.
#ifndef RSSI_OFFSET
#define RSSI_OFFSET 0
#endif
#ifndef USE_SX1262
#define USE_SX1262
#endif
@@ -27,9 +34,9 @@ public:
return ((CustomSX1262 *)_radio)->isReceiving();
}
float getCurrentRSSI() override {
return ((CustomSX1262 *)_radio)->getRSSI(false);
return ((CustomSX1262 *)_radio)->getRSSI(false) + RSSI_OFFSET;
}
float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); }
float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI() + RSSI_OFFSET; }
float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); }
float packetScore(float snr, int packet_len) override {
+118 -1
View File
@@ -15,6 +15,35 @@ class DisplayDriver {
int _w, _h;
protected:
DisplayDriver(int w, int h) { _w = w; _h = h; }
// Текущий размер шрифта и «сырая» позиция курсора (та, что UI передал в
// setCursor(), без поправки драйвера на базовую линию вида y + size*7).
// Нужны printWordWrap(): ведут их те драйверы, которые печатают с переносом.
// _cursor_y_raw < 0 — драйвер позицию не ведёт, переносить не от чего.
int _font_size = 1;
int _cursor_x_raw = 0;
int _cursor_y_raw = -1;
// Шаг строки в логических пикселях для текущего размера шрифта. По умолчанию
// высота моноширинного glcdfont6x8 (8 px на единицу размера) — так считают
// все драйверы, рисующие его со сдвигом базовой линии на size*7. Драйвер с
// другим шрифтом (GxEPD: FreeSans) переопределяет.
virtual int getLineHeight() { return 8 * _font_size; }
// Разделитель слов в printWordWrap(): пробел, табуляция и концы строк.
// '\r' из CRLF пропускаем как разделитель, перенос делает только '\n'.
static bool isWordSeparator(char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}
// Библиотеки Adafruit GFX (SSD1306 / SH1106 / ST7789LCD / GxEPD) и TFT_eSPI
// (ST7735) сами переносят строку, которая не влезает по краю экрана. На время
// printWordWrap() этот перенос выключается: у Adafruit он портит ещё и
// измерение ширины (getTextWidth считает по уже перенесённым строкам), а у
// обеих — вертикальную раскладку, которую ведём мы. У драйверов без такого
// переноса — заглушка.
virtual void setBuiltinWordWrap(bool on) { (void)on; }
public:
//enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light
@@ -32,7 +61,95 @@ public:
virtual void setColor(ColorVal c) = 0;
virtual void setCursor(int x, int y) = 0;
virtual void print(const char* str) = 0;
virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override
// Печать с переносом длинной строки по словам: строка режется по границам
// слов так, чтобы каждая часть влезала в max_width. Слово, которое шире
// целой строки, режется по ширине (как в терминале), а из нескольких пробелов
// и переводов строки подряд остаётся один разделитель; '\n' переносит строку
// принудительно. Печать прекращается, когда очередная строка уже не влезает
// по высоте экрана. Продолжение печатается от того же x, где начали.
//
// Строки приходят уже в CP1251 (перевод делает UI), поэтому символ = байт и
// UTF-8 не разбираем. Ширину строки берём у драйвера (getTextWidth), а не
// считаем 6*size — иначе пропорциональные шрифты (GxEPD, TFT) врут.
virtual void printWordWrap(const char* str, int max_width) {
if (_cursor_y_raw < 0 || max_width <= 0) { // драйвер не ведёт позицию курсора
print(str);
return;
}
int line_h = getLineHeight();
if (line_h < 1) line_h = 8; // защита от setTextSize(0)
const int total = (int)strlen(str);
int pos = 0;
int y = _cursor_y_raw;
char line[192]; // UI_MSG_PREVIEW_SIZE не больше 165 байт
setBuiltinWordWrap(false); // мерим и печатаем без переноса библиотеки
while (pos < total && y + line_h <= height()) {
int len = 0; // сколько байт набрано в line[]
int used = pos; // сколько байт исходной строки израсходовано
while (used < total) {
// Разделители (пробел, табуляция, CR/LF) в начало строки не тащим
bool nl = false;
int ws = used;
while (ws < total && isWordSeparator(str[ws])) {
if (str[ws] == '\n') nl = true;
ws++;
}
if (ws >= total) { used = total; break; } // хвостовые разделители
if (nl && len > 0) break; // '\n': слово уйдёт на следующую строку
used = ws;
int we = ws; // конец слова
while (we < total && !isWordSeparator(str[we])) we++;
// Пробуем дописать слово к уже набранной строке
int save = len;
if (len > 0) {
if (len + 1 >= (int)sizeof(line)) break; // буфер полон — строка закончена
line[len++] = ' ';
}
int wl = we - ws; // сколько байт слова берём
int room = (int)sizeof(line) - 1 - len; // место под слово и NUL
if (wl > room) wl = room;
memcpy(line + len, str + ws, wl);
len += wl;
line[len] = 0;
if (wl > 0 && getTextWidth(line) <= max_width) {
used = ws + wl;
if (wl < we - ws) break; // слово не влезло в буфер
} else if (save == 0 && wl > 0) {
// Самое первое слово шире строки — режем его по ширине
int fit = 1;
for (int k = 1; k <= wl; k++) {
line[k] = 0;
if (getTextWidth(line) > max_width) break;
fit = k;
}
len = fit;
line[len] = 0;
used = ws + fit;
break;
} else {
len = save; // слово не влезает — целиком на новую строку
line[len] = 0;
break;
}
}
if (len == 0) break; // печатать нечего
setCursor(_cursor_x_raw, y);
print(line);
pos = used;
y += line_h;
}
setBuiltinWordWrap(true); // вернуть перенос остальным вызовам print()
}
virtual void fillRect(int x, int y, int w, int h) = 0;
virtual void drawRect(int x, int y, int w, int h) = 0;
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
+33 -18
View File
@@ -115,6 +115,7 @@ void GxEPDDisplay::startFrame(ColorVal bkg) {
void GxEPDDisplay::setTextSize(int sz) {
display_crc.update<int>(sz);
_font_size = sz; // printWordWrap(): шаг строки
switch(sz) {
case 1: // Small
display.setFont(&FreeSans9pt7b);
@@ -139,6 +140,8 @@ void GxEPDDisplay::setColor(ColorVal c) {
void GxEPDDisplay::setCursor(int x, int y) {
display_crc.update<int>(x);
display_crc.update<int>(y);
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
_cursor_y_raw = y;
display.setCursor((x+offset_x)*scale_x, (y+offset_y)*scale_y);
}
@@ -169,35 +172,34 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
display_crc.update<int>(w);
display_crc.update<int>(h);
display_crc.update<uint8_t>(bits, w * h / 8);
// Calculate the base position in display coordinates
uint16_t startX = x * scale_x;
uint16_t startY = y * scale_y;
// Единый масштаб по обеим осям: иначе на экране с не-квадратным пикселем
// (scale_x != scale_y) иконки выходят растянутыми. Центрируем внутри
// логического прямоугольника, чтобы прежние формулы центрирования работали.
float icon_scale = (scale_x < scale_y) ? scale_x : scale_y;
uint16_t startX = (uint16_t)(x * scale_x + (w * scale_x - w * icon_scale) / 2.0f);
uint16_t startY = (uint16_t)(y * scale_y);
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (uint16_t by = 0; by < h; by++) {
// Calculate the target y-coordinates for this logical row
int y1 = startY + (int)(by * scale_y);
int y2 = startY + (int)((by + 1) * scale_y);
int y1 = startY + (int)(by * icon_scale);
int y2 = startY + (int)((by + 1) * icon_scale);
int block_h = y2 - y1;
// Scan across the row bit by bit
if (block_h < 1) block_h = 1;
for (uint16_t bx = 0; bx < w; bx++) {
// Calculate the target x-coordinates for this logical column
int x1 = startX + (int)(bx * scale_x);
int x2 = startX + (int)((bx + 1) * scale_x);
int x1 = startX + (int)(bx * icon_scale);
int x2 = startX + (int)((bx + 1) * icon_scale);
int block_w = x2 - x1;
// Get the current bit
if (block_w < 1) block_w = 1;
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask;
// If the bit is set, draw a block of pixels
if (bitSet) {
// Draw the block as a filled rectangle
display.fillRect(x1, y1, block_w, block_h, _curr_color);
}
}
@@ -211,6 +213,19 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) {
return ceil((w + 1) / scale_x);
}
// Шаг строки для printWordWrap(). Здесь нельзя взять 8*size, как у моноширинных
// драйверов: GxEPD рисует шрифтами FreeSans 9/12/18pt, которые заметно выше, а y
// — это базовая линия (сдвига y + size*7, как у glcdfont6x8, нет). Берём высоту
// глифов текущего шрифта: в "Ag" есть и верхний выносной ('A'), и нижний ('g').
int GxEPDDisplay::getLineHeight() {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds("Ag", 0, 0, &x1, &y1, &w, &h);
// Координаты драйвера логические (умножаются на scale_y), поэтому и шаг
// переводим в них. +2 px — зазор, иначе выносные элементы соседних строк слипаются.
return (int)ceil((h + 2) / scale_y);
}
void GxEPDDisplay::endFrame() {
if (_isOn == false) return;
uint32_t crc = display_crc.finalize();
+4
View File
@@ -70,5 +70,9 @@ public:
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
int getLineHeight() override; // шаг строки для printWordWrap() (шрифты FreeSans)
// Adafruit GFX (база GxEPD2) переносит длинные строки сама — на время
// printWordWrap() это отключается (см. DisplayDriver.h)
void setBuiltinWordWrap(bool on) override { display.setTextWrap(on); }
void endFrame() override;
};
+3
View File
@@ -83,6 +83,7 @@ void SH1106Display::startFrame(ColorVal bkg)
void SH1106Display::setTextSize(int sz)
{
display.setTextSize(sz);
_font_size = sz; // printWordWrap(): шаг строки
#ifdef OLED_RU
_size = sz;
#endif
@@ -96,6 +97,8 @@ void SH1106Display::setColor(ColorVal c)
void SH1106Display::setCursor(int x, int y)
{
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
_cursor_y_raw = y;
#ifdef OLED_RU
display.setCursor(x, y + (_size * 7));
#else
+3
View File
@@ -47,5 +47,8 @@ public:
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
uint16_t getTextWidth(const char *str) override;
// Библиотека Adafruit SH110X переносит длинные строки сама — на время
// printWordWrap() это отключается (см. DisplayDriver.h)
void setBuiltinWordWrap(bool on) override { display.setTextWrap(on); }
void endFrame() override;
};
+3
View File
@@ -77,6 +77,7 @@ void SSD1306Display::startFrame(ColorVal bkg) {
void SSD1306Display::setTextSize(int sz) {
display.setTextSize(sz);
_font_size = sz; // printWordWrap(): шаг строки
#ifdef OLED_RU
_size = sz;
#endif
@@ -88,6 +89,8 @@ void SSD1306Display::setColor(ColorVal c) {
}
void SSD1306Display::setCursor(int x, int y) {
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
_cursor_y_raw = y;
#ifdef OLED_RU
display.setCursor(x, y + (_size * 7));
#else
+3
View File
@@ -50,5 +50,8 @@ public:
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
// Библиотека Adafruit GFX переносит длинные строки сама — на время
// printWordWrap() это отключается (см. DisplayDriver.h)
void setBuiltinWordWrap(bool on) override { display.setTextWrap(on); }
void endFrame() override;
};
+10
View File
@@ -602,6 +602,7 @@ void ST7735Display::startFrame(ColorVal bkg) {
void ST7735Display::setTextSize(int sz) {
sprite->setTextSize(sz);
_font_size = sz; // printWordWrap(): шаг строки
#ifdef OLED_RU
_size = sz;
#endif
@@ -613,6 +614,8 @@ void ST7735Display::setColor(ColorVal c) {
}
void ST7735Display::setCursor(int x, int y) {
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
_cursor_y_raw = y;
#ifdef OLED_RU
sprite->setCursor(x*SCALE_X, y*SCALE_Y + (_size * 7));
#else
@@ -640,6 +643,13 @@ uint16_t ST7735Display::getTextWidth(const char* str) {
return sprite->textWidth(str) / SCALE_X;
}
// TFT_eSPI переносит строку сама, когда она не влезает по краю спрайта. На время
// printWordWrap() перенос выключаем: иначе библиотечный перенос уводит остаток
// строки на свою позицию и ломает вертикальную раскладку, которую ведём мы.
void ST7735Display::setBuiltinWordWrap(bool on) {
if (sprite) sprite->setTextWrap(on);
}
void ST7735Display::endFrame() {
// blit the canvas buffer to LCD
set_CS(LOW);
+3
View File
@@ -54,6 +54,9 @@ public:
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
// TFT_eSPI переносит длинные строки сама — на время printWordWrap() это
// отключается (см. DisplayDriver.h). Спрайт — переменная файла .cpp.
void setBuiltinWordWrap(bool on) override;
void endFrame() override;
// Темы (палитры заданы в таблице THEMES в ST7735Display.cpp)
+3
View File
@@ -141,6 +141,7 @@ void ST7789LCDDisplay::startFrame(ColorVal bkg) {
void ST7789LCDDisplay::setTextSize(int sz) {
display.setTextSize(sz * DISPLAY_SCALE_X);
_font_size = sz; // printWordWrap(): шаг строки
#ifdef OLED_RU
_size = sz;
#endif
@@ -151,6 +152,8 @@ void ST7789LCDDisplay::setColor(ColorVal c) {
}
void ST7789LCDDisplay::setCursor(int x, int y) {
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
_cursor_y_raw = y;
#ifdef OLED_RU
// Кастомный шрифт Adafruit рисуется от базовой линии: глиф поднят над ней
// на 7 * фактический размер (setTextSize выше округляет sz * DISPLAY_SCALE_X)
+3
View File
@@ -72,6 +72,9 @@ public:
void drawRect(int x, int y, int w, int h) override;
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override;
// Библиотека Adafruit ST7789 переносит длинные строки сама — на время
// printWordWrap() это отключается (см. DisplayDriver.h)
void setBuiltinWordWrap(bool on) override { display.setTextWrap(on); }
void endFrame() override;
// Темы (палитры общие с ST7735Display — src/helpers/ui/ColorThemes.h)
+1
View File
@@ -6,6 +6,7 @@ build_flags =
${sensor_base.build_flags}
-I variants/heltec_v4
-D HELTEC_LORA_V4
-D RSSI_OFFSET=-17 ; компенсация усиления LNA GC1109 (~17 дБ)
-D OLED_RU=1
-D UI_SHOW_CLOCK=1
-D USE_SX1262
+1
View File
@@ -1,6 +1,7 @@
[LilyGo_TBeam_1W]
extends = esp32_base
board = t_beam_1w
board_build.partitions = partitions/esp32s3_16mb.csv ; свой layout: app 2.5 МБ, а spiffs/настройки на прежних адресах
build_flags =
${esp32_base.build_flags}
-I variants/lilygo_tbeam_1w
+1
View File
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-D LILYGO_TECHO
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
-D UI_SHOW_CLOCK=1
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-D LILYGO_TECHO
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
+1
View File
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/mesh_pocket
-D HELTEC_MESH_POCKET
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
+1
View File
@@ -1,6 +1,7 @@
[Station_G2]
extends = esp32_base
board = station-g2
board_build.partitions = partitions/esp32s3_16mb.csv ; свой layout: app 2.5 МБ, а spiffs/настройки на прежних адресах
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
+1
View File
@@ -1,6 +1,7 @@
[Station_G3_ESP32]
extends = esp32_base
board = station-g3-esp32
board_build.partitions = partitions/esp32s3_16mb.csv ; свой layout: app 2.5 МБ, а spiffs/настройки на прежних адресах
build_flags =
${esp32_base.build_flags}
${sensor_base.build_flags}
+1
View File
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
-I variants/thinknode_m1
-D THINKNODE_M1=1
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D P_LORA_DIO_1=20
+1
View File
@@ -5,6 +5,7 @@ build_flags = ${esp32_base.build_flags}
-I variants/thinknode_m5
-I src/helpers/sensors
-D THINKNODE_M5
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
-D PIN_BUZZER=9
-D PIN_BOARD_SCL=1
-D PIN_BOARD_SDA=2
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
-I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
-I variants/wio-tracker-l1
-D WIO_TRACKER_L1
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
-D WIO_TRACKER_L1_EINK
-D USE_SX1262
-D RADIO_CLASS=CustomSX1262