Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d41f01ea5 |
No files matched your search
@@ -71,6 +71,32 @@ Heltec T114 и T190, ThinkNode M9, Meshadventurer на E22. Ниже — что
|
|||||||
- В набор сборки входят 26 прошивок: 15 companion, 8 репетитеров и 3 room server /
|
- В набор сборки входят 26 прошивок: 15 companion, 8 репетитеров и 3 room server /
|
||||||
sensor. Как собрать и опубликовать — [`ci/README.md`](./ci/README.md).
|
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. Он описывает базу и остаётся справедливым
|
Дальше — оригинальный README проекта MeshCore. Он описывает базу и остаётся справедливым
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ def convert_from_hex_to_uf2(buf):
|
|||||||
currblock = None
|
currblock = None
|
||||||
blocks = []
|
blocks = []
|
||||||
for line in buf.split('\n'):
|
for line in buf.split('\n'):
|
||||||
if line[0] != ":":
|
if not line or line[0] != ":":
|
||||||
continue
|
continue
|
||||||
i = 1
|
i = 1
|
||||||
rec = []
|
rec = []
|
||||||
|
|||||||
@@ -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)
|
# 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
|
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.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true
|
||||||
cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
|
cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -110,6 +110,14 @@
|
|||||||
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
|
#define DIRECT_SEND_PERHOP_FACTOR 6.0f
|
||||||
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
|
#define DIRECT_SEND_PERHOP_EXTRA_MILLIS 250
|
||||||
#define LAZY_CONTACTS_WRITE_DELAY 5000
|
#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=="
|
#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) {
|
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) {
|
||||||
_iter_started = false;
|
_iter_started = false;
|
||||||
_cli_rescue = false;
|
_cli_rescue = false;
|
||||||
|
_ble_serial = NULL;
|
||||||
cli_command[0] = 0;
|
cli_command[0] = 0;
|
||||||
offline_queue_len = 0;
|
offline_queue_len = 0;
|
||||||
app_target_ver = 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;
|
next_ack_idx = 0;
|
||||||
sign_data = NULL;
|
sign_data = NULL;
|
||||||
dirty_contacts_expiry = 0;
|
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(advert_paths, 0, sizeof(advert_paths));
|
||||||
memset(send_scope.key, 0, sizeof(send_scope.key));
|
memset(send_scope.key, 0, sizeof(send_scope.key));
|
||||||
send_unscoped = false;
|
send_unscoped = false;
|
||||||
@@ -1284,6 +1298,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
_iter = startContactsIterator();
|
_iter = startContactsIterator();
|
||||||
_iter_started = true;
|
_iter_started = true;
|
||||||
_most_recent_lastmod = 0;
|
_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) {
|
} else if (cmd_frame[0] == CMD_SET_ADVERT_NAME && len >= 2) {
|
||||||
int nlen = len - 1;
|
int nlen = len - 1;
|
||||||
@@ -1442,6 +1457,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
|
||||||
}
|
}
|
||||||
} else if (cmd_frame[0] == CMD_SYNC_NEXT_MESSAGE) {
|
} 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;
|
int out_len;
|
||||||
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
|
if ((out_len = getFromOfflineQueue(out_frame)) > 0) {
|
||||||
_serial->writeFrame(out_frame, out_len);
|
_serial->writeFrame(out_frame, out_len);
|
||||||
@@ -1539,10 +1555,8 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
writeOKFrame();
|
writeOKFrame();
|
||||||
}
|
}
|
||||||
} else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) {
|
} 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?
|
// requestReboot() сам сбросит отложенную запись (и при занятом BLE отложит reboot)
|
||||||
saveContacts();
|
requestReboot(true);
|
||||||
}
|
|
||||||
board.reboot();
|
|
||||||
} else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) {
|
} else if (cmd_frame[0] == CMD_GET_BATT_AND_STORAGE) {
|
||||||
uint8_t reply[11];
|
uint8_t reply[11];
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@@ -1779,6 +1793,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
stopConnection(pub_key);
|
stopConnection(pub_key);
|
||||||
writeOKFrame();
|
writeOKFrame();
|
||||||
} else if (cmd_frame[0] == CMD_GET_CHANNEL && len >= 2) {
|
} 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];
|
uint8_t channel_idx = cmd_frame[1];
|
||||||
ChannelDetails channel;
|
ChannelDetails channel;
|
||||||
if (getChannel(channel_idx, 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) {
|
if (strcmp(command, "reboot") == 0) {
|
||||||
board.reboot(); // doesn't return
|
requestReboot(true); // сбросит отложенную запись; может не вернуться
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
|
if (strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
|
||||||
board.powerOff(); // doesn't return
|
requestReboot(false); // сбросит отложенную запись; может не вернуться
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2272,9 +2287,9 @@ void MyMesh::checkCLIRescueCmd() {
|
|||||||
bool success = _store->formatFileSystem();
|
bool success = _store->formatFileSystem();
|
||||||
if (success) {
|
if (success) {
|
||||||
_store->saveMainIdentity(self_id);
|
_store->saveMainIdentity(self_id);
|
||||||
savePrefs();
|
savePrefsNow(); // файловая система только что пересоздана — пишем сразу, не откладывая
|
||||||
saveContacts();
|
saveContacts();
|
||||||
saveChannels();
|
saveChannelsNow();
|
||||||
Serial.println(" > erase and rebuild done");
|
Serial.println(" > erase and rebuild done");
|
||||||
} else {
|
} else {
|
||||||
Serial.println(" Error: erase failed");
|
Serial.println(" Error: erase failed");
|
||||||
@@ -2405,7 +2420,7 @@ void MyMesh::checkCLIRescueCmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (strcmp(cli_command, "reboot") == 0) {
|
} else if (strcmp(cli_command, "reboot") == 0) {
|
||||||
board.reboot(); // doesn't return
|
requestReboot(true); // сбросит отложенную запись
|
||||||
} else {
|
} else {
|
||||||
Serial.println(" Error: unknown command");
|
Serial.println(" Error: unknown command");
|
||||||
}
|
}
|
||||||
@@ -2435,6 +2450,7 @@ void MyMesh::checkSerialInterface() {
|
|||||||
4); // include the most recent lastmod, so app can update their 'since'
|
4); // include the most recent lastmod, so app can update their 'since'
|
||||||
_serial->writeFrame(out_frame, 5);
|
_serial->writeFrame(out_frame, 5);
|
||||||
_iter_started = false;
|
_iter_started = false;
|
||||||
|
_serial->setFastMode(false); // contact sync finished: dial BLE back to the idle interval
|
||||||
}
|
}
|
||||||
//} else if (!_serial->isWriteBusy()) {
|
//} else if (!_serial->isWriteBusy()) {
|
||||||
// checkConnections(); // TODO - deprecate the 'Connections' stuff
|
// checkConnections(); // TODO - deprecate the 'Connections' stuff
|
||||||
@@ -2455,10 +2471,105 @@ void MyMesh::loop() {
|
|||||||
#endif
|
#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?
|
// is there are pending dirty contacts write needed?
|
||||||
if (dirty_contacts_expiry && millisHasNowPassed(dirty_contacts_expiry)) {
|
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;
|
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
|
// To check if there is pending work
|
||||||
bool MyMesh::hasPendingWork() const {
|
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;
|
||||||
}
|
}
|
||||||
@@ -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_GET_STATUS 0x01 // same as _GET_STATS
|
||||||
#define REQ_TYPE_KEEP_ALIVE 0x02
|
#define REQ_TYPE_KEEP_ALIVE 0x02
|
||||||
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
#define REQ_TYPE_GET_TELEMETRY_DATA 0x03
|
||||||
@@ -200,13 +215,63 @@ protected:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// Отложенная запись prefs: метод только помечает данные «грязными» и ставит срок,
|
||||||
|
// реальную запись во флеш делает loop() (см. flushPendingWrites()). Так UI продолжает
|
||||||
|
// звать savePrefs() как раньше, но на nRF52 запись не попадает в момент, когда радио
|
||||||
|
// занято BLE-соединением (иначе flash_nrf5x ждёт окно radio-idle и loop() виснет).
|
||||||
void savePrefs() {
|
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_lat = sensors.node_lat;
|
||||||
_prefs.node_lon = sensors.node_lon;
|
_prefs.node_lon = sensors.node_lon;
|
||||||
_store->savePrefs(_prefs);
|
_store->savePrefs(_prefs);
|
||||||
_prefs.clearDirty();
|
_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). Проверяет допустимые
|
// Ручная настройка радио (экран RADIO в ui-new). Проверяет допустимые
|
||||||
// диапазоны, применяет параметры к радио и обновляет prefs в памяти;
|
// диапазоны, применяет параметры к радио и обновляет prefs в памяти;
|
||||||
// запись в файл — только через savePrefs().
|
// запись в файл — только через savePrefs().
|
||||||
@@ -248,9 +313,25 @@ private:
|
|||||||
bool isValidClientRepeatFreq(uint32_t f) const;
|
bool isValidClientRepeatFreq(uint32_t f) const;
|
||||||
|
|
||||||
// helpers, short-cuts
|
// 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();
|
void saveContacts();
|
||||||
|
|
||||||
|
// Обслуживание отложенного reboot/powerOff (см. loop()); сам запрос — requestReboot().
|
||||||
|
void servicePendingReboot();
|
||||||
|
|
||||||
DataStore* _store;
|
DataStore* _store;
|
||||||
NodePrefs _prefs;
|
NodePrefs _prefs;
|
||||||
uint32_t pending_login;
|
uint32_t pending_login;
|
||||||
@@ -258,6 +339,7 @@ private:
|
|||||||
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
||||||
uint32_t pending_req; // pending _BINARY_REQ
|
uint32_t pending_req; // pending _BINARY_REQ
|
||||||
BaseSerialInterface *_serial;
|
BaseSerialInterface *_serial;
|
||||||
|
BaseSerialInterface *_ble_serial; // только BLE-интерфейс (может быть NULL)
|
||||||
Listener* _listener;
|
Listener* _listener;
|
||||||
|
|
||||||
ContactsIterator _iter;
|
ContactsIterator _iter;
|
||||||
@@ -273,6 +355,11 @@ private:
|
|||||||
uint8_t *sign_data;
|
uint8_t *sign_data;
|
||||||
uint32_t sign_data_len;
|
uint32_t sign_data_len;
|
||||||
unsigned long dirty_contacts_expiry;
|
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;
|
TransportKey send_scope;
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ void setup() {
|
|||||||
#if defined(BLE_PIN_CODE)
|
#if defined(BLE_PIN_CODE)
|
||||||
bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
|
bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin());
|
||||||
interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface);
|
interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface);
|
||||||
|
// сообщаем mesh'у именно BLE-интерфейс: на nRF52 отложенная запись во флеш ждёт,
|
||||||
|
// пока он не подключён (у USB isConnected() всегда true, ориентироваться на него нельзя)
|
||||||
|
the_mesh.setBleSerialInterface(&bluetooth_interface);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// add wifi interface
|
// add wifi interface
|
||||||
|
|||||||
@@ -49,6 +49,44 @@
|
|||||||
#define NOTIFY_MAIN_ROWS 8 // строк в меню уведомлений (включая Timezone)
|
#define NOTIFY_MAIN_ROWS 8 // строк в меню уведомлений (включая Timezone)
|
||||||
#define NOTIFY_ROW_TZ 5 // строка Timezone (стоит перед Quiet hours)
|
#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) {
|
static void drawMenuRow(DisplayDriver& display, int y, bool sel, const char* label, const char* value) {
|
||||||
if (sel) {
|
if (sel) {
|
||||||
@@ -312,18 +350,7 @@ class HomeScreen : public UIScreen {
|
|||||||
|
|
||||||
|
|
||||||
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
|
||||||
// Convert millivolts to percentage
|
const int batteryPercentage = battPercentage(batteryMilliVolts);
|
||||||
#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%
|
|
||||||
|
|
||||||
// battery icon
|
// battery icon
|
||||||
// что показывать рядом с батареей — напряжение, процент или ничего (страница POWER)
|
// что показывать рядом с батареей — напряжение, процент или ничего (страница POWER)
|
||||||
@@ -1188,9 +1215,7 @@ uint16_t HomeScreen::battMilliVolts() {
|
|||||||
|
|
||||||
void HomeScreen::renderPowerPage(DisplayDriver& display, uint16_t mv) {
|
void HomeScreen::renderPowerPage(DisplayDriver& display, uint16_t mv) {
|
||||||
char tmp[24];
|
char tmp[24];
|
||||||
int pct = ((int)mv - BATT_MIN_MILLIVOLTS) * 100 / (BATT_MAX_MILLIVOLTS - BATT_MIN_MILLIVOLTS);
|
const int pct = battPercentage(mv);
|
||||||
if (pct < 0) pct = 0;
|
|
||||||
if (pct > 100) pct = 100;
|
|
||||||
|
|
||||||
// Крупная иконка батареи, справа — заряд и напряжение.
|
// Крупная иконка батареи, справа — заряд и напряжение.
|
||||||
// Сверху 12 px занимает статус-бар, под ним (13..17) — точки страниц,
|
// Сверху 12 px занимает статус-бар, под ним (13..17) — точки страниц,
|
||||||
@@ -1959,15 +1984,19 @@ void UITask::shutdown(bool restart){
|
|||||||
|
|
||||||
#endif // PIN_BUZZER
|
#endif // PIN_BUZZER
|
||||||
|
|
||||||
|
// Перезагрузку и выключение просим у MyMesh, а не у платы напрямую: на nRF52
|
||||||
|
// запись prefs/контактов отложена, пока подключено приложение, и MyMesh сам
|
||||||
|
// рвёт связь, сохраняет данные и только потом перезагружает или выключает.
|
||||||
|
// Прямой вызов board потерял бы последнюю настройку (тему, тихие часы, радио).
|
||||||
if (restart) {
|
if (restart) {
|
||||||
_board->reboot();
|
the_mesh.requestReboot(true);
|
||||||
} else {
|
} else {
|
||||||
// перед выключением гасим экран, иначе на некоторых панелях остаётся картинка
|
// перед выключением гасим экран, иначе на некоторых панелях остаётся картинка
|
||||||
display.forceFullRefresh();
|
display.forceFullRefresh();
|
||||||
display.clear();
|
display.clear();
|
||||||
display.endFrame();
|
display.endFrame();
|
||||||
// Power off board including radio, display, GPS and components
|
// Power off board including radio, display, GPS and components
|
||||||
_board->powerOff();
|
the_mesh.requestReboot(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
@@ -204,6 +204,7 @@ void Dispatcher::checkRecv() {
|
|||||||
} else {
|
} else {
|
||||||
if (tryParsePacket(pkt, raw, len)) {
|
if (tryParsePacket(pkt, raw, len)) {
|
||||||
pkt->_snr = _radio->getLastSNR() * 4.0f;
|
pkt->_snr = _radio->getLastSNR() * 4.0f;
|
||||||
|
pkt->_rssi = (int8_t)_radio->getLastRSSI();
|
||||||
score = _radio->packetScore(_radio->getLastSNR(), len);
|
score = _radio->packetScore(_radio->getLastSNR(), len);
|
||||||
air_time = _radio->getEstAirtimeFor(len);
|
air_time = _radio->getEstAirtimeFor(len);
|
||||||
rx_air_time += air_time;
|
rx_air_time += air_time;
|
||||||
@@ -360,6 +361,7 @@ Packet* Dispatcher::obtainNewPacket() {
|
|||||||
} else {
|
} else {
|
||||||
pkt->payload_len = pkt->path_len = 0;
|
pkt->payload_len = pkt->path_len = 0;
|
||||||
pkt->_snr = 0;
|
pkt->_snr = 0;
|
||||||
|
pkt->_rssi = 0;
|
||||||
}
|
}
|
||||||
return pkt;
|
return pkt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ Packet::Packet() {
|
|||||||
header = 0;
|
header = 0;
|
||||||
path_len = 0;
|
path_len = 0;
|
||||||
payload_len = 0;
|
payload_len = 0;
|
||||||
|
_snr = 0;
|
||||||
|
_rssi = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Packet::isValidPathLen(uint8_t path_len) {
|
bool Packet::isValidPathLen(uint8_t path_len) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ public:
|
|||||||
uint8_t path[MAX_PATH_SIZE];
|
uint8_t path[MAX_PATH_SIZE];
|
||||||
uint8_t payload[MAX_PACKET_PAYLOAD];
|
uint8_t payload[MAX_PACKET_PAYLOAD];
|
||||||
int8_t _snr;
|
int8_t _snr;
|
||||||
|
int8_t _rssi; // уровень приёма, дБм: нужен тем, кто обрабатывает пакет позже приёма
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \brief calculate the hash of payload + type
|
* \brief calculate the hash of payload + type
|
||||||
@@ -90,6 +91,7 @@ public:
|
|||||||
bool isMarkedDoNotRetransmit() const { return header == 0xFF; }
|
bool isMarkedDoNotRetransmit() const { return header == 0xFF; }
|
||||||
|
|
||||||
float getSNR() const { return ((float)_snr) / 4.0f; }
|
float getSNR() const { return ((float)_snr) / 4.0f; }
|
||||||
|
float getRSSI() const { return (float)_rssi; } // дБм
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* \returns the encoded/wire format length of this packet
|
* \returns the encoded/wire format length of this packet
|
||||||
|
|||||||
@@ -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
|
uint8_t flags = data[4] >> 2; // message attempt number, and other flags
|
||||||
|
|
||||||
// len can be > original length, but 'text' will be padded with zeroes
|
// 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
|
data[len] = 0; // need to make a C string again, with null terminator
|
||||||
|
|
||||||
if (flags == TXT_TYPE_PLAIN) {
|
if (flags == TXT_TYPE_PLAIN) {
|
||||||
@@ -424,6 +428,10 @@ void BaseChatMesh::onGroupDataRecv(mesh::Packet* packet, uint8_t type, const mes
|
|||||||
memcpy(×tamp, data, 4);
|
memcpy(×tamp, data, 4);
|
||||||
|
|
||||||
// len can be > original length, but 'text' will be padded with zeroes
|
// 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
|
data[len] = 0; // need to make a C string again, with null terminator
|
||||||
|
|
||||||
// notify UI of this new message
|
// 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) {
|
if (idx >= 0 && idx < MAX_GROUP_CHANNELS) {
|
||||||
channels[idx] = src;
|
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) {
|
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
|
mesh::Utils::sha256(channels[idx].channel.hash, sizeof(channels[idx].channel.hash), src.channel.secret, 16); // 128-bit key
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,4 +19,13 @@ public:
|
|||||||
virtual bool isWriteBusy() const = 0;
|
virtual bool isWriteBusy() const = 0;
|
||||||
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
|
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
|
||||||
virtual size_t checkRecvFrame(uint8_t dest[]) = 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) {}
|
||||||
};
|
};
|
||||||
@@ -157,6 +157,17 @@ public:
|
|||||||
return false;
|
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 {
|
size_t writeFrame(const uint8_t src[], size_t len) override {
|
||||||
// don't write when disabled or nothing provided
|
// don't write when disabled or nothing provided
|
||||||
if(!_enabled || len == 0){
|
if(!_enabled || len == 0){
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "SerialBLEInterface.h"
|
#include "SerialBLEInterface.h"
|
||||||
#include "esp_mac.h"
|
#include "esp_mac.h"
|
||||||
|
#include <esp_idf_version.h>
|
||||||
|
|
||||||
// See the following for generating UUIDs:
|
// See the following for generating UUIDs:
|
||||||
// https://www.uuidgenerator.net/
|
// https://www.uuidgenerator.net/
|
||||||
@@ -10,6 +11,13 @@
|
|||||||
|
|
||||||
#define ADVERT_RESTART_DELAY 1000 // millis
|
#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) {
|
void SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
|
||||||
_pin_code = 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
|
// Create the BLE Device
|
||||||
BLEDevice::init(dev_name);
|
BLEDevice::init(dev_name);
|
||||||
BLEDevice::setSecurityCallbacks(this);
|
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;
|
BLESecurity sec;
|
||||||
sec.setStaticPIN(pin_code);
|
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) {
|
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));
|
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;
|
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) {
|
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
|
||||||
@@ -162,6 +189,10 @@ void SerialBLEInterface::disable() {
|
|||||||
pServer->disconnect(last_conn_id);
|
pServer->disconnect(last_conn_id);
|
||||||
pService->stop();
|
pService->stop();
|
||||||
oldDeviceConnected = deviceConnected = false;
|
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;
|
adv_restart_time = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,13 +217,88 @@ size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
|
|||||||
return 0;
|
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 {
|
bool SerialBLEInterface::isWriteBusy() const {
|
||||||
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
|
return millis() < _last_write + BLE_WRITE_MIN_INTERVAL; // still too soon to start another write?
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
|
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
|
if (send_queue_len > 0 // first, check send queue
|
||||||
&& millis() >= _last_write + BLE_WRITE_MIN_INTERVAL // space the writes apart
|
&& 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 != oldDeviceConnected) {
|
||||||
if (!deviceConnected) { // disconnecting
|
if (!deviceConnected) { // disconnecting
|
||||||
clearBuffers();
|
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...");
|
BLE_DEBUG_PRINTLN("SerialBLEInterface -> disconnecting...");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../BaseSerialInterface.h"
|
#include "../BaseSerialInterface.h"
|
||||||
|
#include <esp_gap_ble_api.h>
|
||||||
#include <BLEDevice.h>
|
#include <BLEDevice.h>
|
||||||
#include <BLEServer.h>
|
#include <BLEServer.h>
|
||||||
#include <BLEUtils.h>
|
#include <BLEUtils.h>
|
||||||
@@ -19,6 +20,11 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
|
|||||||
uint32_t _pin_code;
|
uint32_t _pin_code;
|
||||||
unsigned long _last_write;
|
unsigned long _last_write;
|
||||||
unsigned long adv_restart_time;
|
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 {
|
struct Frame {
|
||||||
uint8_t len;
|
uint8_t len;
|
||||||
@@ -65,6 +71,11 @@ public:
|
|||||||
FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state
|
FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state
|
||||||
);
|
);
|
||||||
send_queue_len = 0;
|
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;
|
bool isConnected() const override;
|
||||||
|
|
||||||
|
void setFastMode(bool fast) override;
|
||||||
|
|
||||||
bool isWriteBusy() const override;
|
bool isWriteBusy() const override;
|
||||||
size_t writeFrame(const uint8_t src[], size_t len) override;
|
size_t writeFrame(const uint8_t src[], size_t len) override;
|
||||||
size_t checkRecvFrame(uint8_t dest[]) override;
|
size_t checkRecvFrame(uint8_t dest[]) override;
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
#include "RadioLibWrappers.h"
|
#include "RadioLibWrappers.h"
|
||||||
#include "SX126xReset.h"
|
#include "SX126xReset.h"
|
||||||
|
|
||||||
|
// RSSI_OFFSET компенсирует усиление внешнего LNA (например, GC1109 на Heltec V4):
|
||||||
|
// задаётся отрицательным, равным усилению LNA, тогда в приложение и на экран
|
||||||
|
// попадает реальный уровень сигнала. Для плат без LNA оставляем 0.
|
||||||
|
#ifndef RSSI_OFFSET
|
||||||
|
#define RSSI_OFFSET 0
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifndef USE_SX1262
|
#ifndef USE_SX1262
|
||||||
#define USE_SX1262
|
#define USE_SX1262
|
||||||
#endif
|
#endif
|
||||||
@@ -27,9 +34,9 @@ public:
|
|||||||
return ((CustomSX1262 *)_radio)->isReceiving();
|
return ((CustomSX1262 *)_radio)->isReceiving();
|
||||||
}
|
}
|
||||||
float getCurrentRSSI() override {
|
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 getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); }
|
||||||
|
|
||||||
float packetScore(float snr, int packet_len) override {
|
float packetScore(float snr, int packet_len) override {
|
||||||
|
|||||||
@@ -15,6 +15,35 @@ class DisplayDriver {
|
|||||||
int _w, _h;
|
int _w, _h;
|
||||||
protected:
|
protected:
|
||||||
DisplayDriver(int w, int h) { _w = w; _h = h; }
|
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:
|
public:
|
||||||
//enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light
|
//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 setColor(ColorVal c) = 0;
|
||||||
virtual void setCursor(int x, int y) = 0;
|
virtual void setCursor(int x, int y) = 0;
|
||||||
virtual void print(const char* str) = 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 fillRect(int x, int y, int w, int h) = 0;
|
||||||
virtual void drawRect(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;
|
virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0;
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ void GxEPDDisplay::startFrame(ColorVal bkg) {
|
|||||||
|
|
||||||
void GxEPDDisplay::setTextSize(int sz) {
|
void GxEPDDisplay::setTextSize(int sz) {
|
||||||
display_crc.update<int>(sz);
|
display_crc.update<int>(sz);
|
||||||
|
_font_size = sz; // printWordWrap(): шаг строки
|
||||||
switch(sz) {
|
switch(sz) {
|
||||||
case 1: // Small
|
case 1: // Small
|
||||||
display.setFont(&FreeSans9pt7b);
|
display.setFont(&FreeSans9pt7b);
|
||||||
@@ -139,6 +140,8 @@ void GxEPDDisplay::setColor(ColorVal c) {
|
|||||||
void GxEPDDisplay::setCursor(int x, int y) {
|
void GxEPDDisplay::setCursor(int x, int y) {
|
||||||
display_crc.update<int>(x);
|
display_crc.update<int>(x);
|
||||||
display_crc.update<int>(y);
|
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);
|
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>(w);
|
||||||
display_crc.update<int>(h);
|
display_crc.update<int>(h);
|
||||||
display_crc.update<uint8_t>(bits, w * h / 8);
|
display_crc.update<uint8_t>(bits, w * h / 8);
|
||||||
// Calculate the base position in display coordinates
|
// Единый масштаб по обеим осям: иначе на экране с не-квадратным пикселем
|
||||||
uint16_t startX = x * scale_x;
|
// (scale_x != scale_y) иконки выходят растянутыми. Центрируем внутри
|
||||||
uint16_t startY = y * 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
|
// Width in bytes for bitmap processing
|
||||||
uint16_t widthInBytes = (w + 7) / 8;
|
uint16_t widthInBytes = (w + 7) / 8;
|
||||||
|
|
||||||
// Process the bitmap row by row
|
// Process the bitmap row by row
|
||||||
for (uint16_t by = 0; by < h; by++) {
|
for (uint16_t by = 0; by < h; by++) {
|
||||||
// Calculate the target y-coordinates for this logical row
|
int y1 = startY + (int)(by * icon_scale);
|
||||||
int y1 = startY + (int)(by * scale_y);
|
int y2 = startY + (int)((by + 1) * icon_scale);
|
||||||
int y2 = startY + (int)((by + 1) * scale_y);
|
|
||||||
int block_h = y2 - y1;
|
int block_h = y2 - y1;
|
||||||
|
if (block_h < 1) block_h = 1;
|
||||||
// Scan across the row bit by bit
|
|
||||||
for (uint16_t bx = 0; bx < w; bx++) {
|
for (uint16_t bx = 0; bx < w; bx++) {
|
||||||
// Calculate the target x-coordinates for this logical column
|
int x1 = startX + (int)(bx * icon_scale);
|
||||||
int x1 = startX + (int)(bx * scale_x);
|
int x2 = startX + (int)((bx + 1) * icon_scale);
|
||||||
int x2 = startX + (int)((bx + 1) * scale_x);
|
|
||||||
int block_w = x2 - x1;
|
int block_w = x2 - x1;
|
||||||
|
if (block_w < 1) block_w = 1;
|
||||||
// Get the current bit
|
|
||||||
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
|
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
|
||||||
uint8_t bitMask = 0x80 >> (bx & 7);
|
uint8_t bitMask = 0x80 >> (bx & 7);
|
||||||
bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask;
|
bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask;
|
||||||
|
|
||||||
// If the bit is set, draw a block of pixels
|
|
||||||
if (bitSet) {
|
if (bitSet) {
|
||||||
// Draw the block as a filled rectangle
|
|
||||||
display.fillRect(x1, y1, block_w, block_h, _curr_color);
|
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);
|
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() {
|
void GxEPDDisplay::endFrame() {
|
||||||
if (_isOn == false) return;
|
if (_isOn == false) return;
|
||||||
uint32_t crc = display_crc.finalize();
|
uint32_t crc = display_crc.finalize();
|
||||||
|
|||||||
@@ -70,5 +70,9 @@ public:
|
|||||||
void drawRect(int x, int y, int w, int h) override;
|
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;
|
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||||
uint16_t getTextWidth(const char* str) 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;
|
void endFrame() override;
|
||||||
};
|
};
|
||||||
@@ -83,6 +83,7 @@ void SH1106Display::startFrame(ColorVal bkg)
|
|||||||
void SH1106Display::setTextSize(int sz)
|
void SH1106Display::setTextSize(int sz)
|
||||||
{
|
{
|
||||||
display.setTextSize(sz);
|
display.setTextSize(sz);
|
||||||
|
_font_size = sz; // printWordWrap(): шаг строки
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
_size = sz;
|
_size = sz;
|
||||||
#endif
|
#endif
|
||||||
@@ -96,6 +97,8 @@ void SH1106Display::setColor(ColorVal c)
|
|||||||
|
|
||||||
void SH1106Display::setCursor(int x, int y)
|
void SH1106Display::setCursor(int x, int y)
|
||||||
{
|
{
|
||||||
|
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
|
||||||
|
_cursor_y_raw = y;
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
display.setCursor(x, y + (_size * 7));
|
display.setCursor(x, y + (_size * 7));
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -47,5 +47,8 @@ public:
|
|||||||
void drawRect(int x, int y, int w, int h) override;
|
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;
|
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
|
||||||
uint16_t getTextWidth(const char *str) override;
|
uint16_t getTextWidth(const char *str) override;
|
||||||
|
// Библиотека Adafruit SH110X переносит длинные строки сама — на время
|
||||||
|
// printWordWrap() это отключается (см. DisplayDriver.h)
|
||||||
|
void setBuiltinWordWrap(bool on) override { display.setTextWrap(on); }
|
||||||
void endFrame() override;
|
void endFrame() override;
|
||||||
};
|
};
|
||||||
@@ -77,6 +77,7 @@ void SSD1306Display::startFrame(ColorVal bkg) {
|
|||||||
|
|
||||||
void SSD1306Display::setTextSize(int sz) {
|
void SSD1306Display::setTextSize(int sz) {
|
||||||
display.setTextSize(sz);
|
display.setTextSize(sz);
|
||||||
|
_font_size = sz; // printWordWrap(): шаг строки
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
_size = sz;
|
_size = sz;
|
||||||
#endif
|
#endif
|
||||||
@@ -88,6 +89,8 @@ void SSD1306Display::setColor(ColorVal c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SSD1306Display::setCursor(int x, int y) {
|
void SSD1306Display::setCursor(int x, int y) {
|
||||||
|
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
|
||||||
|
_cursor_y_raw = y;
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
display.setCursor(x, y + (_size * 7));
|
display.setCursor(x, y + (_size * 7));
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -50,5 +50,8 @@ public:
|
|||||||
void drawRect(int x, int y, int w, int h) override;
|
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;
|
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||||
uint16_t getTextWidth(const char* str) override;
|
uint16_t getTextWidth(const char* str) override;
|
||||||
|
// Библиотека Adafruit GFX переносит длинные строки сама — на время
|
||||||
|
// printWordWrap() это отключается (см. DisplayDriver.h)
|
||||||
|
void setBuiltinWordWrap(bool on) override { display.setTextWrap(on); }
|
||||||
void endFrame() override;
|
void endFrame() override;
|
||||||
};
|
};
|
||||||
@@ -602,6 +602,7 @@ void ST7735Display::startFrame(ColorVal bkg) {
|
|||||||
|
|
||||||
void ST7735Display::setTextSize(int sz) {
|
void ST7735Display::setTextSize(int sz) {
|
||||||
sprite->setTextSize(sz);
|
sprite->setTextSize(sz);
|
||||||
|
_font_size = sz; // printWordWrap(): шаг строки
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
_size = sz;
|
_size = sz;
|
||||||
#endif
|
#endif
|
||||||
@@ -613,6 +614,8 @@ void ST7735Display::setColor(ColorVal c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ST7735Display::setCursor(int x, int y) {
|
void ST7735Display::setCursor(int x, int y) {
|
||||||
|
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
|
||||||
|
_cursor_y_raw = y;
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
sprite->setCursor(x*SCALE_X, y*SCALE_Y + (_size * 7));
|
sprite->setCursor(x*SCALE_X, y*SCALE_Y + (_size * 7));
|
||||||
#else
|
#else
|
||||||
@@ -640,6 +643,13 @@ uint16_t ST7735Display::getTextWidth(const char* str) {
|
|||||||
return sprite->textWidth(str) / SCALE_X;
|
return sprite->textWidth(str) / SCALE_X;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TFT_eSPI переносит строку сама, когда она не влезает по краю спрайта. На время
|
||||||
|
// printWordWrap() перенос выключаем: иначе библиотечный перенос уводит остаток
|
||||||
|
// строки на свою позицию и ломает вертикальную раскладку, которую ведём мы.
|
||||||
|
void ST7735Display::setBuiltinWordWrap(bool on) {
|
||||||
|
if (sprite) sprite->setTextWrap(on);
|
||||||
|
}
|
||||||
|
|
||||||
void ST7735Display::endFrame() {
|
void ST7735Display::endFrame() {
|
||||||
// blit the canvas buffer to LCD
|
// blit the canvas buffer to LCD
|
||||||
set_CS(LOW);
|
set_CS(LOW);
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ public:
|
|||||||
void drawRect(int x, int y, int w, int h) override;
|
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;
|
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||||
uint16_t getTextWidth(const char* str) override;
|
uint16_t getTextWidth(const char* str) override;
|
||||||
|
// TFT_eSPI переносит длинные строки сама — на время printWordWrap() это
|
||||||
|
// отключается (см. DisplayDriver.h). Спрайт — переменная файла .cpp.
|
||||||
|
void setBuiltinWordWrap(bool on) override;
|
||||||
void endFrame() override;
|
void endFrame() override;
|
||||||
|
|
||||||
// Темы (палитры заданы в таблице THEMES в ST7735Display.cpp)
|
// Темы (палитры заданы в таблице THEMES в ST7735Display.cpp)
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ void ST7789LCDDisplay::startFrame(ColorVal bkg) {
|
|||||||
|
|
||||||
void ST7789LCDDisplay::setTextSize(int sz) {
|
void ST7789LCDDisplay::setTextSize(int sz) {
|
||||||
display.setTextSize(sz * DISPLAY_SCALE_X);
|
display.setTextSize(sz * DISPLAY_SCALE_X);
|
||||||
|
_font_size = sz; // printWordWrap(): шаг строки
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
_size = sz;
|
_size = sz;
|
||||||
#endif
|
#endif
|
||||||
@@ -151,6 +152,8 @@ void ST7789LCDDisplay::setColor(ColorVal c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ST7789LCDDisplay::setCursor(int x, int y) {
|
void ST7789LCDDisplay::setCursor(int x, int y) {
|
||||||
|
_cursor_x_raw = x; // printWordWrap(): откуда печатать продолжение строки
|
||||||
|
_cursor_y_raw = y;
|
||||||
#ifdef OLED_RU
|
#ifdef OLED_RU
|
||||||
// Кастомный шрифт Adafruit рисуется от базовой линии: глиф поднят над ней
|
// Кастомный шрифт Adafruit рисуется от базовой линии: глиф поднят над ней
|
||||||
// на 7 * фактический размер (setTextSize выше округляет sz * DISPLAY_SCALE_X)
|
// на 7 * фактический размер (setTextSize выше округляет sz * DISPLAY_SCALE_X)
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ public:
|
|||||||
void drawRect(int x, int y, int w, int h) override;
|
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;
|
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||||
uint16_t getTextWidth(const char* str) override;
|
uint16_t getTextWidth(const char* str) override;
|
||||||
|
// Библиотека Adafruit ST7789 переносит длинные строки сама — на время
|
||||||
|
// printWordWrap() это отключается (см. DisplayDriver.h)
|
||||||
|
void setBuiltinWordWrap(bool on) override { display.setTextWrap(on); }
|
||||||
void endFrame() override;
|
void endFrame() override;
|
||||||
|
|
||||||
// Темы (палитры общие с ST7735Display — src/helpers/ui/ColorThemes.h)
|
// Темы (палитры общие с ST7735Display — src/helpers/ui/ColorThemes.h)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ build_flags =
|
|||||||
${sensor_base.build_flags}
|
${sensor_base.build_flags}
|
||||||
-I variants/heltec_v4
|
-I variants/heltec_v4
|
||||||
-D HELTEC_LORA_V4
|
-D HELTEC_LORA_V4
|
||||||
|
-D RSSI_OFFSET=-17 ; компенсация усиления LNA GC1109 (~17 дБ)
|
||||||
-D OLED_RU=1
|
-D OLED_RU=1
|
||||||
-D UI_SHOW_CLOCK=1
|
-D UI_SHOW_CLOCK=1
|
||||||
-D USE_SX1262
|
-D USE_SX1262
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[LilyGo_TBeam_1W]
|
[LilyGo_TBeam_1W]
|
||||||
extends = esp32_base
|
extends = esp32_base
|
||||||
board = t_beam_1w
|
board = t_beam_1w
|
||||||
|
board_build.partitions = partitions/esp32s3_16mb.csv ; свой layout: app 2.5 МБ, а spiffs/настройки на прежних адресах
|
||||||
build_flags =
|
build_flags =
|
||||||
${esp32_base.build_flags}
|
${esp32_base.build_flags}
|
||||||
-I variants/lilygo_tbeam_1w
|
-I variants/lilygo_tbeam_1w
|
||||||
|
|||||||
@@ -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
|
||||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||||
-D LILYGO_TECHO
|
-D LILYGO_TECHO
|
||||||
|
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
|
||||||
-D UI_SHOW_CLOCK=1
|
-D UI_SHOW_CLOCK=1
|
||||||
-D RADIO_CLASS=CustomSX1262
|
-D RADIO_CLASS=CustomSX1262
|
||||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
-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
|
||||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||||
-D LILYGO_TECHO
|
-D LILYGO_TECHO
|
||||||
|
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
|
||||||
-D RADIO_CLASS=CustomSX1262
|
-D RADIO_CLASS=CustomSX1262
|
||||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||||
-D LORA_TX_POWER=22
|
-D LORA_TX_POWER=22
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
|
|||||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||||
-I variants/mesh_pocket
|
-I variants/mesh_pocket
|
||||||
-D HELTEC_MESH_POCKET
|
-D HELTEC_MESH_POCKET
|
||||||
|
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
|
||||||
-D RADIO_CLASS=CustomSX1262
|
-D RADIO_CLASS=CustomSX1262
|
||||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||||
-D LORA_TX_POWER=22
|
-D LORA_TX_POWER=22
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[Station_G2]
|
[Station_G2]
|
||||||
extends = esp32_base
|
extends = esp32_base
|
||||||
board = station-g2
|
board = station-g2
|
||||||
|
board_build.partitions = partitions/esp32s3_16mb.csv ; свой layout: app 2.5 МБ, а spiffs/настройки на прежних адресах
|
||||||
build_flags =
|
build_flags =
|
||||||
${esp32_base.build_flags}
|
${esp32_base.build_flags}
|
||||||
${sensor_base.build_flags}
|
${sensor_base.build_flags}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
[Station_G3_ESP32]
|
[Station_G3_ESP32]
|
||||||
extends = esp32_base
|
extends = esp32_base
|
||||||
board = station-g3-esp32
|
board = station-g3-esp32
|
||||||
|
board_build.partitions = partitions/esp32s3_16mb.csv ; свой layout: app 2.5 МБ, а spiffs/настройки на прежних адресах
|
||||||
build_flags =
|
build_flags =
|
||||||
${esp32_base.build_flags}
|
${esp32_base.build_flags}
|
||||||
${sensor_base.build_flags}
|
${sensor_base.build_flags}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ build_flags = ${nrf52_base.build_flags}
|
|||||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||||
-I variants/thinknode_m1
|
-I variants/thinknode_m1
|
||||||
-D THINKNODE_M1=1
|
-D THINKNODE_M1=1
|
||||||
|
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
|
||||||
-D RADIO_CLASS=CustomSX1262
|
-D RADIO_CLASS=CustomSX1262
|
||||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||||
-D P_LORA_DIO_1=20
|
-D P_LORA_DIO_1=20
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ build_flags = ${esp32_base.build_flags}
|
|||||||
-I variants/thinknode_m5
|
-I variants/thinknode_m5
|
||||||
-I src/helpers/sensors
|
-I src/helpers/sensors
|
||||||
-D THINKNODE_M5
|
-D THINKNODE_M5
|
||||||
|
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
|
||||||
-D PIN_BUZZER=9
|
-D PIN_BUZZER=9
|
||||||
-D PIN_BOARD_SCL=1
|
-D PIN_BOARD_SCL=1
|
||||||
-D PIN_BOARD_SDA=2
|
-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 lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52
|
||||||
-I variants/wio-tracker-l1
|
-I variants/wio-tracker-l1
|
||||||
-D WIO_TRACKER_L1
|
-D WIO_TRACKER_L1
|
||||||
|
-D EINK_MAX_PARTIAL_REFRESH=60 ; раз в 60 частичных обновлений — полное, чтобы экран не «замыливался»
|
||||||
-D WIO_TRACKER_L1_EINK
|
-D WIO_TRACKER_L1_EINK
|
||||||
-D USE_SX1262
|
-D USE_SX1262
|
||||||
-D RADIO_CLASS=CustomSX1262
|
-D RADIO_CLASS=CustomSX1262
|
||||||
|
|||||||
Reference in new issue
Block a user