#include "UITask.h" #include extern "C" uint16_t MeshCoreStackFreeNow(void); extern "C" uint16_t MeshCoreStackFreeMin(void); namespace { const uint8_t kMaxRows = 17; const uint8_t kDisplayCols = 64; const uint8_t kMainContentCols = 56; const uint16_t kSoftkeyLabelX = 348; const uint8_t kSoftkeyLabelHalfHeight = 4; const uint8_t kMessageFirstRow = 1; const uint8_t kMessageEndRow = kMaxRows - 1; const uint8_t kMessageRows = kMessageEndRow - kMessageFirstRow; const uint8_t kRadioFieldCount = 5; const uint8_t kSettingsFieldCount = 1 + kRadioFieldCount; const uint8_t kKeyLeft = 0x02; const uint8_t kKeyRight = 0x03; const uint8_t kKeyUp = 0x04; const uint8_t kKeyDown = 0x05; const uint8_t kKeyPageUp = 0x06; const uint8_t kKeyPageDown = 0x07; const uint8_t kKeySoft6 = 0xF5; const uint8_t kKeySoft7 = 0xF6; const uint8_t kKeySoft8 = 0xF7; const uint8_t kKeySoft9 = 0xF8; const uint8_t kKeySoft10 = 0xF9; const uint8_t kKeyCancel = kKeySoft6; const unsigned long kDiagRefreshMillis = 1000; const unsigned long kBatteryRefreshMillis = 60000; const unsigned long kCursorBlinkMillis = 500; const uint8_t kRegPaDac = 0x4D; const uint16_t kBwOptionsX10[] = { 625, 1250, 2500, 5000 }; #if defined(USE_SX1262) const int8_t kMaxTxPower = 22; #else const int8_t kMaxTxPower = 17; #endif void copyText(char* dest, size_t dest_size, const char* src) { size_t i = 0; if (dest_size == 0) { return; } while (src && src[i] && i + 1 < dest_size) { dest[i] = src[i]; ++i; } dest[i] = 0; } void appendText(char* dest, size_t dest_size, const char* src) { size_t len = strlen(dest); size_t i = 0; if (len >= dest_size) { return; } while (src && src[i] && len + i + 1 < dest_size) { dest[len + i] = src[i]; ++i; } dest[len + i] = 0; } void appendUnsigned(char* dest, size_t dest_size, unsigned long value) { char temp[12]; ultoa(value, temp, 10); appendText(dest, dest_size, temp); } void appendSigned(char* dest, size_t dest_size, long value) { char temp[12]; ltoa(value, temp, 10); appendText(dest, dest_size, temp); } void appendHexByte(char* dest, size_t dest_size, uint8_t value) { static const char hex[] = "0123456789ABCDEF"; size_t len = strlen(dest); if (len + 2 >= dest_size) { return; } dest[len] = hex[value >> 4]; dest[len + 1] = hex[value & 0x0F]; dest[len + 2] = 0; } void appendHexWord(char* dest, size_t dest_size, uint16_t value) { appendHexByte(dest, dest_size, (uint8_t)(value >> 8)); appendHexByte(dest, dest_size, (uint8_t)value); } void appendFixed1(char* dest, size_t dest_size, uint16_t x10) { appendUnsigned(dest, dest_size, x10 / 10); appendText(dest, dest_size, "."); appendUnsigned(dest, dest_size, x10 % 10); } void appendFreqMHz(char* dest, size_t dest_size, uint32_t khz) { uint16_t frac = khz % 1000UL; appendUnsigned(dest, dest_size, khz / 1000UL); appendText(dest, dest_size, "."); if (frac < 100) { appendText(dest, dest_size, "0"); } if (frac < 10) { appendText(dest, dest_size, "0"); } appendUnsigned(dest, dest_size, frac); } void buildBatteryText(char* dest, size_t dest_size, uint16_t millivolts) { copyText(dest, dest_size, "Bat "); if (millivolts == 0) { appendText(dest, dest_size, "--.--V"); return; } appendUnsigned(dest, dest_size, millivolts / 1000U); appendText(dest, dest_size, "."); uint8_t hundredths = (millivolts % 1000U) / 10U; if (hundredths < 10) { appendText(dest, dest_size, "0"); } appendUnsigned(dest, dest_size, hundredths); appendText(dest, dest_size, "V"); } uint8_t findBwIndex(uint16_t bw_x10) { uint8_t best = 0; uint16_t best_delta = 0xFFFF; for (uint8_t i = 0; i < sizeof(kBwOptionsX10) / sizeof(kBwOptionsX10[0]); ++i) { uint16_t candidate = kBwOptionsX10[i]; uint16_t delta = (candidate > bw_x10) ? (candidate - bw_x10) : (bw_x10 - candidate); if (delta < best_delta) { best = i; best_delta = delta; } } return best; } bool radioSettingsEqual(const PublicApp::RadioSettings& a, const PublicApp::RadioSettings& b) { return a.freq_khz == b.freq_khz && a.bw_x10 == b.bw_x10 && a.sf == b.sf && a.cr == b.cr && a.tx_power == b.tx_power; } void settingPrefix(char* dest, size_t dest_size, uint8_t field, uint8_t selected_field) { copyText(dest, dest_size, (field == selected_field) ? "> " : " "); } void fitMainLine(char* line) { if (line) { line[kMainContentCols] = 0; } } void renderRightSoftkeyLabel(SmartResponseXEBoard* board, uint8_t index, const char* label) { board->writeDisplayText(kSoftkeyLabelX, (uint8_t)(8 + (30 * index) - kSoftkeyLabelHalfHeight), label); } void renderMainSoftkeyLabels(SmartResponseXEBoard* board) { renderRightSoftkeyLabel(board, 0, "Setup"); renderRightSoftkeyLabel(board, 1, "Debug"); renderRightSoftkeyLabel(board, 4, "Send"); } void renderSettingsSoftkeyLabels(SmartResponseXEBoard* board) { renderRightSoftkeyLabel(board, 0, "Save"); renderRightSoftkeyLabel(board, 1, "Cancel"); } void renderDebugSoftkeyLabels(SmartResponseXEBoard* board) { renderRightSoftkeyLabel(board, 0, "Back"); } const char* packetResultText(char code) { switch (code) { case 'O': return "accepted"; case '+': return "outbound rebroadcast heard"; case 'T': return "unsupported packet type"; case 'L': return "invalid packet length"; case 'H': return "different public channel"; case 'D': return "duplicate packet"; case 'M': return "authentication failed"; case 'Y': return "unsupported message flags"; case 'W': return "flash write failed"; case 'E': return "empty message"; default: return "none yet"; } } const char* watchdogStageText(char code) { switch (code) { case 'I': return "idle loop"; case 'S': return "startup"; case 'A': return "application loop"; case 'U': return "UI update"; case 'F': return "flash access"; case 'P': return "power transition"; case 'D': return "dispatcher loop"; case 'C': return "public packet handler"; case 'c': return "message authentication"; case 'p': return "message decryption"; case 'v': return "message validation"; case 'x': return "message text decoding"; case 'm': return "message storage"; case 'w': return "message flash write"; case 'o': return "message completion"; case 'R': return "radio service"; case 'X': return "radio receive start"; case 'L': return "radio packet length"; case 'r': return "radio packet read"; case 'T': return "airtime calculation"; case 't': return "radio transmit start"; case 'f': return "radio transmit finish"; case 'B': return "radio channel check"; case 'i': return "packet RSSI read"; case 'n': return "packet SNR read"; case 'k': return "decrypt key setup"; case 'd': return "decrypt completion"; case 'K': return "encrypt key setup"; case 'E': return "message encryption"; case 'e': return "final encryption block"; case 'h': return "authentication check"; case 'a': return "authenticated decrypt"; default: if (code >= '0' && code <= '9') { return "message decrypt block"; } return "internal operation"; } } char markerFor(const PublicMessage& msg, bool first_segment) { if (!first_segment) { return ' '; } if (msg.outbound) { return msg.rebroadcasted ? '+' : '>'; } return ' '; } void buildWrappedMessageLine(char* line, size_t line_size, const PublicMessage& msg, const char* text, uint8_t text_len, bool first_segment) { uint8_t pos = 0; if (line_size == 0) { return; } if (pos + 1 < line_size) { line[pos++] = markerFor(msg, first_segment); } if (pos + 1 < line_size) { line[pos++] = ' '; } for (uint8_t i = 0; i < text_len && pos + 1 < line_size; ++i) { line[pos++] = text[i]; } line[pos] = 0; } } UITask::UITask(SmartResponseXEBoard& board, PublicApp& app) : _board(&board), _app(&app), _mode(MODE_PUBLIC), _last_drawn_revision(0xFFFFFFFFUL), _last_diag_refresh(0), _notice_expires_at(0), _last_battery_refresh(0), _last_cursor_toggle(0), _scroll_lines_from_bottom(0), _new_messages_while_scrolled(0), _battery_millivolts(0), _last_public_recv_count(0xFFFFFFFFUL), _last_public_recv_line_count(0xFFFFFFFFUL), _cursor_visible(true), _valid_lines(0), _settings_field(0), _dirty(DIRTY_ALL) { memset(_draft, 0, sizeof(_draft)); memset(_name_edit, 0, sizeof(_name_edit)); memset(_notice, 0, sizeof(_notice)); memset(_line_cache, 0, sizeof(_line_cache)); _radio_edit = _app->getRadioSettings(); } void UITask::begin() { _board->beginDisplay(); refreshBattery(true); resetCursor(); render(true); } void UITask::redraw() { refreshBattery(true); resetCursor(); render(true); } void UITask::drawLine(uint8_t row, const char* text) { if (row >= kUiRows) { return; } char line[kUiCols + 1]; copyText(line, sizeof(line), text ? text : ""); uint32_t mask = 1UL << row; if ((_valid_lines & mask) && strcmp(_line_cache[row], line) == 0) { return; } copyText(_line_cache[row], sizeof(_line_cache[row]), line); _valid_lines |= mask; _board->writeDisplayLine(row, line); } void UITask::setNotice(const char* text, unsigned long duration_millis) { copyText(_notice, sizeof(_notice), text); _notice_expires_at = duration_millis ? millis() + duration_millis : 0; _dirty |= (_mode == MODE_PUBLIC) ? DIRTY_HEADER : DIRTY_SCREEN; } void UITask::clearNotice() { if (_notice[0] == 0) { return; } _notice[0] = 0; _notice_expires_at = 0; _dirty |= (_mode == MODE_PUBLIC) ? DIRTY_HEADER : DIRTY_SCREEN; } void UITask::expireNotice() { if (_notice[0] && _notice_expires_at && (long)(millis() - _notice_expires_at) >= 0) { clearNotice(); } } uint8_t UITask::renderWrappedMessageLine(uint8_t row, const PublicMessage& msg, uint16_t skip_lines) { const char* p = msg.text; bool first_segment = true; if (!p || !*p) { if (skip_lines == 0) { char line[kDisplayCols + 1]; buildWrappedMessageLine(line, sizeof(line), msg, "", 0, first_segment); drawLine(row, line); return row + 1; } return row; } while (*p && row < kMessageEndRow) { while (*p == ' ') { ++p; } if (!*p) { break; } uint8_t len = wrappedSegmentLen(p, PublicMessage::kWrapCols); if (len == 0) { break; } if (skip_lines > 0) { --skip_lines; } else { char line[kDisplayCols + 1]; buildWrappedMessageLine(line, sizeof(line), msg, p, len, first_segment); drawLine(row, line); ++row; } p += len; while (*p == ' ') { ++p; } first_segment = false; } return row; } void UITask::appendChar(char* dest, size_t size, char c) { size_t len = strlen(dest); if (len + 1 < size && c >= 32 && c <= 126) { dest[len] = c; dest[len + 1] = 0; } } void UITask::backspace(char* dest) { size_t len = strlen(dest); if (len > 0) { dest[len - 1] = 0; } } uint16_t UITask::maxScrollOffset() const { uint32_t total = _app->getTotalLineCount(); if (total <= kMessageRows) { return 0; } uint32_t maximum = total - kMessageRows; return (maximum > 0xFFFFUL) ? 0xFFFFU : (uint16_t)maximum; } void UITask::scrollBy(int16_t lines_from_bottom_delta) { int32_t next = (int32_t)_scroll_lines_from_bottom + lines_from_bottom_delta; uint16_t maximum = maxScrollOffset(); if (next < 0) { next = 0; } else if ((uint32_t)next > maximum) { next = maximum; } if ((uint16_t)next == _scroll_lines_from_bottom) { return; } _scroll_lines_from_bottom = (uint16_t)next; if (_scroll_lines_from_bottom == 0) { _new_messages_while_scrolled = 0; } _dirty |= DIRTY_HEADER | DIRTY_MESSAGES; } void UITask::updateHistoryState() { uint32_t recv_count = _app->getPublicRecvCount(); uint32_t recv_lines = _app->getPublicRecvLineCount(); if (_last_public_recv_count == 0xFFFFFFFFUL || _last_public_recv_line_count == 0xFFFFFFFFUL) { _last_public_recv_count = recv_count; _last_public_recv_line_count = recv_lines; return; } uint32_t new_messages = recv_count - _last_public_recv_count; uint32_t new_lines = recv_lines - _last_public_recv_line_count; _last_public_recv_count = recv_count; _last_public_recv_line_count = recv_lines; if (_scroll_lines_from_bottom == 0 || (new_messages == 0 && new_lines == 0)) { return; } uint32_t next_offset = (uint32_t)_scroll_lines_from_bottom + new_lines; uint16_t maximum = maxScrollOffset(); _scroll_lines_from_bottom = (next_offset > maximum) ? maximum : (uint16_t)next_offset; uint32_t next_count = (uint32_t)_new_messages_while_scrolled + new_messages; _new_messages_while_scrolled = (next_count > 0xFFFFUL) ? 0xFFFFU : (uint16_t)next_count; _dirty |= DIRTY_HEADER | DIRTY_MESSAGES; } void UITask::refreshBattery(bool force) { unsigned long now = millis(); if (!force && (unsigned long)(now - _last_battery_refresh) < kBatteryRefreshMillis) { return; } _last_battery_refresh = now; uint16_t millivolts = _board->getBattMilliVolts(); if (millivolts != _battery_millivolts) { _battery_millivolts = millivolts; if (_mode == MODE_PUBLIC) { _dirty |= DIRTY_HEADER; } } } void UITask::resetCursor() { _cursor_visible = true; _last_cursor_toggle = millis(); if (_mode == MODE_PUBLIC) { _dirty |= DIRTY_COMPOSER; } } void UITask::updateCursor() { if (_mode != MODE_PUBLIC) { return; } unsigned long now = millis(); if ((unsigned long)(now - _last_cursor_toggle) < kCursorBlinkMillis) { return; } _last_cursor_toggle = now; _cursor_visible = !_cursor_visible; _dirty |= DIRTY_COMPOSER; } void UITask::enterSettingsMode() { clearNotice(); _mode = MODE_SETTINGS; strncpy(_name_edit, _app->getNodeName(), sizeof(_name_edit) - 1); _name_edit[sizeof(_name_edit) - 1] = 0; _radio_edit = _app->getRadioSettings(); _settings_field = 0; } void UITask::leaveSettingsMode(bool save) { if (save) { bool radio_ok = true; if (!radioSettingsEqual(_radio_edit, _app->getRadioSettings())) { radio_ok = _app->setRadioSettings(_radio_edit, true); } bool name_ok = radio_ok && _app->setNodeName(_name_edit); if (name_ok && radio_ok) { clearNotice(); _mode = MODE_PUBLIC; resetCursor(); } else if (!radio_ok) { setNotice("Invalid radio settings"); } else { setNotice("Settings not saved"); } } else { clearNotice(); _mode = MODE_PUBLIC; resetCursor(); } } void UITask::enterDebugMode() { clearNotice(); _mode = MODE_DEBUG; } void UITask::leaveDebugMode() { clearNotice(); _mode = MODE_PUBLIC; resetCursor(); } void UITask::adjustRadioSetting(int8_t delta) { if (delta == 0 || _settings_field == 0) { return; } switch (_settings_field - 1) { case 0: { const long step_khz = 5; long next = (long)_radio_edit.freq_khz + ((long)delta * step_khz); if (next < 902000L) { next = 902000L; } else if (next > 928000L) { next = 928000L; } _radio_edit.freq_khz = (uint32_t)next; break; } case 1: { uint8_t idx = findBwIndex(_radio_edit.bw_x10); uint8_t count = sizeof(kBwOptionsX10) / sizeof(kBwOptionsX10[0]); if (delta > 0) { idx = (idx + 1) % count; } else { idx = (idx == 0) ? (count - 1) : (idx - 1); } _radio_edit.bw_x10 = kBwOptionsX10[idx]; break; } case 2: if (delta > 0 && _radio_edit.sf < 12) { ++_radio_edit.sf; } else if (delta < 0 && _radio_edit.sf > 7) { --_radio_edit.sf; } break; case 3: if (delta > 0 && _radio_edit.cr < 8) { ++_radio_edit.cr; } else if (delta < 0 && _radio_edit.cr > 5) { --_radio_edit.cr; } break; case 4: if (delta > 0 && _radio_edit.tx_power < kMaxTxPower) { ++_radio_edit.tx_power; } else if (delta < 0 && _radio_edit.tx_power > 2) { --_radio_edit.tx_power; } break; } } void UITask::handleKey(int key) { if (!key) { return; } Mode old_mode = _mode; if (_mode == MODE_DEBUG) { if (key == kKeyCancel) { leaveDebugMode(); } else if (key == 0xF3) { if (_app->resetRadio()) { setNotice("Radio reset", 2000); } else { setNotice("Radio reset failed", 5000); } } render(_mode != old_mode); return; } if (_mode == MODE_SETTINGS) { bool edited = false; if (key == 0x0D || key == kKeySoft6) { leaveSettingsMode(true); } else if (key == kKeySoft7) { leaveSettingsMode(false); } else if (key == kKeyUp) { _settings_field = (_settings_field == 0) ? (kSettingsFieldCount - 1) : (_settings_field - 1); edited = true; } else if (key == kKeyDown) { _settings_field = (_settings_field + 1) % kSettingsFieldCount; edited = true; } else if (key == kKeyLeft) { adjustRadioSetting(-1); edited = true; } else if (key == kKeyRight) { adjustRadioSetting(1); edited = true; } else if (key == 8 && _settings_field == 0) { backspace(_name_edit); edited = true; } else if (key >= 32 && key <= 126 && _settings_field == 0) { appendChar(_name_edit, sizeof(_name_edit), (char)key); edited = true; } if (edited) { clearNotice(); _dirty |= DIRTY_SCREEN; } render(_mode != old_mode); return; } if (key == kKeySoft6) { enterSettingsMode(); } else if (key == kKeySoft7) { enterDebugMode(); } else if (key == kKeyUp) { scrollBy(1); } else if (key == kKeyDown) { scrollBy(-1); } else if (key == kKeyPageUp) { scrollBy(kMessageRows - 1); } else if (key == kKeyPageDown) { scrollBy(-((int16_t)kMessageRows - 1)); } else if (key == 0x0D || key == kKeySoft10) { if (_draft[0] == 0) { return; } if (_app->sendPublic(_draft)) { _draft[0] = 0; _scroll_lines_from_bottom = 0; _new_messages_while_scrolled = 0; clearNotice(); _dirty |= DIRTY_HEADER | DIRTY_MESSAGES | DIRTY_COMPOSER; resetCursor(); } else { setNotice("Send failed - retry", 5000); } } else if (key == 8) { size_t old_length = strlen(_draft); backspace(_draft); clearNotice(); if (strlen(_draft) != old_length) { resetCursor(); } } else if (key >= 32 && key <= 126) { size_t old_length = strlen(_draft); appendChar(_draft, sizeof(_draft), (char)key); clearNotice(); if (strlen(_draft) != old_length) { resetCursor(); } } render(_mode != old_mode); } void UITask::renderSettings() { char line[kDisplayCols + 1]; drawLine(0, "Settings"); settingPrefix(line, sizeof(line), 0, _settings_field); appendText(line, sizeof(line), "Name "); appendText(line, sizeof(line), _name_edit); fitMainLine(line); drawLine(2, line); drawLine(4, "Radio"); settingPrefix(line, sizeof(line), 1, _settings_field); appendText(line, sizeof(line), "Freq "); appendFreqMHz(line, sizeof(line), _radio_edit.freq_khz); appendText(line, sizeof(line), " MHz"); fitMainLine(line); drawLine(5, line); settingPrefix(line, sizeof(line), 2, _settings_field); appendText(line, sizeof(line), "BW "); appendFixed1(line, sizeof(line), _radio_edit.bw_x10); appendText(line, sizeof(line), " kHz"); fitMainLine(line); drawLine(6, line); settingPrefix(line, sizeof(line), 3, _settings_field); appendText(line, sizeof(line), "SF "); appendUnsigned(line, sizeof(line), _radio_edit.sf); fitMainLine(line); drawLine(7, line); settingPrefix(line, sizeof(line), 4, _settings_field); appendText(line, sizeof(line), "CR "); appendUnsigned(line, sizeof(line), _radio_edit.cr); fitMainLine(line); drawLine(8, line); settingPrefix(line, sizeof(line), 5, _settings_field); appendText(line, sizeof(line), "TX "); appendSigned(line, sizeof(line), _radio_edit.tx_power); appendText(line, sizeof(line), " dBm"); fitMainLine(line); drawLine(9, line); drawLine(12, "Name: type/backspace"); drawLine(13, "Radio: left/right edit"); copyText(line, sizeof(line), _notice); fitMainLine(line); drawLine(15, line); } void UITask::renderDebug() { char line[kDisplayCols + 1]; drawLine(0, "System Diagnostics"); copyText(line, sizeof(line), "RX packets: radio "); appendUnsigned(line, sizeof(line), _app->getRawRxCount()); appendText(line, sizeof(line), " parsed "); appendUnsigned(line, sizeof(line), _app->getParsedRxCount()); appendText(line, sizeof(line), " rejected "); appendUnsigned(line, sizeof(line), _app->getRejectCount()); drawLine(1, line); copyText(line, sizeof(line), "Last RX: "); appendUnsigned(line, sizeof(line), _app->getLastRawLen()); appendText(line, sizeof(line), " bytes type "); appendUnsigned(line, sizeof(line), _app->getLastRxType()); appendText(line, sizeof(line), " RSSI "); appendSigned(line, sizeof(line), _app->getLastRSSI()); appendText(line, sizeof(line), " dBm SNR "); appendSigned(line, sizeof(line), _app->getLastSNR()); appendText(line, sizeof(line), " dB"); drawLine(2, line); copyText(line, sizeof(line), "TX packets: completed "); appendUnsigned(line, sizeof(line), _app->getTxDoneCount()); appendText(line, sizeof(line), " failed "); appendUnsigned(line, sizeof(line), _app->getTxFailCount()); drawLine(3, line); copyText(line, sizeof(line), "Rejected by type "); appendUnsigned(line, sizeof(line), _app->getRejectTypeCount()); appendText(line, sizeof(line), " wrong channel "); appendUnsigned(line, sizeof(line), _app->getRejectHashCount()); drawLine(4, line); copyText(line, sizeof(line), "Rejected as duplicate "); appendUnsigned(line, sizeof(line), _app->getRejectDupCount()); appendText(line, sizeof(line), " authentication "); appendUnsigned(line, sizeof(line), _app->getRejectMacCount()); drawLine(5, line); copyText(line, sizeof(line), "Last packet result: "); appendText(line, sizeof(line), packetResultText(_app->getLastRejectCode())); drawLine(6, line); #if defined(USE_SX1262) copyText(line, sizeof(line), "SX1262: status 0x"); appendHexByte(line, sizeof(line), radio_get_status()); appendText(line, sizeof(line), " IRQ flags 0x"); appendHexWord(line, sizeof(line), (uint16_t)radio_get_irq_flags()); drawLine(7, line); copyText(line, sizeof(line), "Radio pins: BUSY "); appendUnsigned(line, sizeof(line), radio_get_busy_level()); appendText(line, sizeof(line), " DIO1/IRQ "); appendUnsigned(line, sizeof(line), radio_get_irq_level()); appendText(line, sizeof(line), " live RSSI "); appendSigned(line, sizeof(line), radio_get_current_rssi()); appendText(line, sizeof(line), " dBm"); drawLine(8, line); #else copyText(line, sizeof(line), "SX127x: version 0x"); appendHexByte(line, sizeof(line), radio_read_reg(RADIOLIB_SX127X_REG_VERSION)); appendText(line, sizeof(line), " mode 0x"); appendHexByte(line, sizeof(line), radio_read_reg(RADIOLIB_SX127X_REG_OP_MODE)); appendText(line, sizeof(line), " IRQ 0x"); appendHexByte(line, sizeof(line), (uint8_t)radio_get_irq_flags()); drawLine(7, line); copyText(line, sizeof(line), "Modem status 0x"); appendHexByte(line, sizeof(line), radio_get_modem_status()); appendText(line, sizeof(line), " DIO0/IRQ "); appendUnsigned(line, sizeof(line), radio_get_dio0_level()); appendText(line, sizeof(line), " live RSSI "); appendSigned(line, sizeof(line), radio_get_current_rssi()); appendText(line, sizeof(line), " dBm"); drawLine(8, line); copyText(line, sizeof(line), "PA config 0x"); appendHexByte(line, sizeof(line), radio_read_reg(RADIOLIB_SX127X_REG_PA_CONFIG)); appendText(line, sizeof(line), " PA DAC 0x"); appendHexByte(line, sizeof(line), radio_read_reg(kRegPaDac)); appendText(line, sizeof(line), " OCP 0x"); appendHexByte(line, sizeof(line), radio_read_reg(RADIOLIB_SX127X_REG_OCP)); drawLine(9, line); #endif const PublicApp::RadioSettings& settings = _app->getRadioSettings(); copyText(line, sizeof(line), "Frequency: "); appendFreqMHz(line, sizeof(line), settings.freq_khz); appendText(line, sizeof(line), " MHz bandwidth "); appendFixed1(line, sizeof(line), settings.bw_x10); appendText(line, sizeof(line), " kHz"); drawLine(10, line); copyText(line, sizeof(line), "LoRa: spreading factor "); appendUnsigned(line, sizeof(line), settings.sf); appendText(line, sizeof(line), " coding rate 4/"); appendUnsigned(line, sizeof(line), settings.cr); appendText(line, sizeof(line), " TX "); appendSigned(line, sizeof(line), settings.tx_power); appendText(line, sizeof(line), " dBm"); drawLine(11, line); copyText(line, sizeof(line), "Last reset: "); appendText(line, sizeof(line), _app->getResetCauseText()); char watchdog_stage = _app->getWatchdogStage(); if (watchdog_stage != '-') { appendText(line, sizeof(line), " at "); appendText(line, sizeof(line), watchdogStageText(watchdog_stage)); appendText(line, sizeof(line), " ("); size_t len = strlen(line); if (len + 1 < sizeof(line)) { line[len] = watchdog_stage; line[len + 1] = 0; } appendText(line, sizeof(line), ")"); } drawLine(12, line); copyText(line, sizeof(line), "Stack free: now "); appendUnsigned(line, sizeof(line), MeshCoreStackFreeNow()); appendText(line, sizeof(line), " bytes minimum "); appendUnsigned(line, sizeof(line), MeshCoreStackFreeMin()); appendText(line, sizeof(line), " bytes"); drawLine(13, line); copyText(line, sizeof(line), "Message flash: "); appendUnsigned(line, sizeof(line), _app->getMessageCount()); appendText(line, sizeof(line), " records next page "); appendUnsigned(line, sizeof(line), _app->getFlashNextPage()); drawLine(14, line); copyText(line, sizeof(line), "Flash pages: "); appendUnsigned(line, sizeof(line), _app->getFlashBlankPageCount()); appendText(line, sizeof(line), " blank "); appendUnsigned(line, sizeof(line), _app->getFlashInvalidPageCount()); appendText(line, sizeof(line), " invalid "); appendUnsigned(line, sizeof(line), _app->getFlashPhysicalBreakCount()); appendText(line, sizeof(line), " layout gaps"); drawLine(15, line); copyText(line, sizeof(line), _notice); drawLine(16, line); } void UITask::renderPublicHeader() { char line[kDisplayCols + 1]; char battery[12]; if (_notice[0]) { copyText(line, sizeof(line), _notice); } else if (_scroll_lines_from_bottom > 0) { copyText(line, sizeof(line), "Public - History"); if (_new_messages_while_scrolled > 0) { appendText(line, sizeof(line), " - "); appendUnsigned(line, sizeof(line), _new_messages_while_scrolled); appendText(line, sizeof(line), " new"); } } else { copyText(line, sizeof(line), "MeshCore Public Channel"); } buildBatteryText(battery, sizeof(battery), _battery_millivolts); size_t line_length = strlen(line); size_t battery_length = strlen(battery); while (line_length + battery_length < kMainContentCols) { appendText(line, sizeof(line), " "); ++line_length; } appendText(line, sizeof(line), battery); fitMainLine(line); drawLine(0, line); } void UITask::renderPublicMessages() { uint32_t total_rows = _app->getTotalLineCount(); uint16_t visible_rows = kMessageRows; uint16_t skip_lines = 0; if (total_rows > (uint32_t)visible_rows + _scroll_lines_from_bottom) { skip_lines = (uint16_t)(total_rows - visible_rows - _scroll_lines_from_bottom); } uint16_t count = _app->getMessageCount(); PublicMessage msg; uint8_t row = kMessageFirstRow; for (uint16_t i = 0; i < count && row < kMessageEndRow; ++i) { if (!_app->getMessage(i, msg)) { continue; } uint8_t lines = wrappedLineCount(msg.text, PublicMessage::kWrapCols); if (skip_lines >= lines) { skip_lines -= lines; continue; } row = renderWrappedMessageLine(row, msg, skip_lines); skip_lines = 0; } while (row < kMessageEndRow) { drawLine(row++, ""); } } void UITask::renderComposer() { char line[kDisplayCols + 1]; copyText(line, sizeof(line), _app->getNodeName()); appendText(line, sizeof(line), "> "); size_t prompt_length = strlen(line); size_t draft_length = strlen(_draft); size_t visible_draft_columns = 0; if (prompt_length + 1 < kMainContentCols) { visible_draft_columns = kMainContentCols - prompt_length - 1; } const char* visible_draft = _draft; if (draft_length > visible_draft_columns) { visible_draft += draft_length - visible_draft_columns; } appendText(line, sizeof(line), visible_draft); appendText(line, sizeof(line), _cursor_visible ? "_" : " "); fitMainLine(line); drawLine(kMaxRows - 1, line); } void UITask::render(bool force) { uint32_t rev = _app->getRevision(); if (rev != _last_drawn_revision) { _last_drawn_revision = rev; _dirty |= (_mode == MODE_PUBLIC) ? (DIRTY_HEADER | DIRTY_MESSAGES) : DIRTY_SCREEN; } if (force) { _board->clearDisplay(); _valid_lines = 0; _dirty = DIRTY_ALL; } uint8_t dirty = _dirty; _dirty = DIRTY_NONE; if (dirty == DIRTY_NONE) { return; } if (_mode == MODE_SETTINGS) { if (dirty & DIRTY_SCREEN) { renderSettings(); } if (force) { renderSettingsSoftkeyLabels(_board); } return; } if (_mode == MODE_DEBUG) { if (dirty & DIRTY_SCREEN) { renderDebug(); _last_diag_refresh = millis(); } if (force) { renderDebugSoftkeyLabels(_board); } return; } if (dirty & DIRTY_HEADER) { renderPublicHeader(); } if (dirty & DIRTY_MESSAGES) { renderPublicMessages(); } if (dirty & DIRTY_COMPOSER) { renderComposer(); } if (force) { renderMainSoftkeyLabels(_board); } } void UITask::loop() { expireNotice(); updateHistoryState(); refreshBattery(false); handleKey(_board->readKey()); updateCursor(); if (_mode == MODE_DEBUG && (unsigned long)(millis() - _last_diag_refresh) >= kDiagRefreshMillis) { _dirty |= DIRTY_SCREEN; } render(false); }