diff --git a/src/graphics/QrEncode.cpp b/src/graphics/QrEncode.cpp new file mode 100644 index 0000000000..a367f9c252 --- /dev/null +++ b/src/graphics/QrEncode.cpp @@ -0,0 +1,343 @@ +// Stripped-down fork of ricmoo/QRCode (https://github.com/ricmoo/QRCode), itself based on +// Project Nayuki's QR Code generator (https://www.nayuki.io/page/qr-code-generator-library). +// +// The MIT License (MIT) +// Copyright (c) 2017 Richard Moore +// Copyright (c) Project Nayuki +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +// Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "QrEncode.h" + +#include + +// ECC level M, versions 1-6 (index = version - 1). Every block in these versions has the same length. +static const uint8_t TOTAL_CODEWORDS[6] = {26, 44, 70, 100, 134, 172}; +static const uint8_t ECC_PER_BLOCK[6] = {10, 16, 26, 18, 24, 16}; +static const uint8_t NUM_BLOCKS[6] = {1, 1, 1, 2, 2, 4}; + +// 15-bit format words for ECC level M with masks 0-7 +static const uint16_t FORMAT_BITS[8] = {0x5412, 0x5125, 0x5E7C, 0x5B4B, 0x45F9, 0x40CE, 0x4F97, 0x4AA0}; + +#define MAX_CODEWORDS 172 +#define MAX_ECC_PER_BLOCK 26 + +static bool getBit(const uint8_t *m, uint8_t size, uint8_t x, uint8_t y) +{ + uint16_t i = y * size + x; + return (m[i >> 3] >> (7 - (i & 7))) & 1; +} + +static void setBit(uint8_t *m, uint8_t size, uint8_t x, uint8_t y, bool on) +{ + uint16_t i = y * size + x; + uint8_t b = 0x80 >> (i & 7); + if (on) + m[i >> 3] |= b; + else + m[i >> 3] &= ~b; +} + +// Computed rather than tracked in a second grid; versions 1-6 have no version-info area and one alignment pattern. +static bool isFunction(uint8_t size, uint8_t x, uint8_t y) +{ + if (x == 6 || y == 6) + return true; + if (x < 9 && (y < 9 || y >= size - 8)) + return true; + if (y < 9 && x >= size - 8) + return true; + return size > 21 && (uint8_t)(x - (size - 9)) < 5 && (uint8_t)(y - (size - 9)) < 5; +} + +static void drawFormatBits(uint8_t *m, uint8_t size, uint8_t mask) +{ + uint16_t bits = FORMAT_BITS[mask]; + for (uint8_t i = 0; i < 15; i++) { + bool on = (bits >> i) & 1; + if (i < 6) + setBit(m, size, 8, i, on); + else if (i < 8) + setBit(m, size, 8, i + 1, on); + else if (i == 8) + setBit(m, size, 7, 8, on); + else + setBit(m, size, 14 - i, 8, on); + + if (i < 8) + setBit(m, size, size - 1 - i, 8, on); + else + setBit(m, size, 8, size - 15 + i, on); + } + setBit(m, size, 8, size - 8, true); // Dark module +} + +static void drawFunctionPatterns(uint8_t *m, uint8_t size) +{ + for (uint8_t i = 8; i < size - 8; i += 2) { + setBit(m, size, 6, i, true); + setBit(m, size, i, 6, true); + } + + // Symmetric row bitmaps; separators are already light + static const uint8_t FINDER[7] = {0x7F, 0x41, 0x5D, 0x5D, 0x5D, 0x41, 0x7F}; + static const uint8_t ALIGN[5] = {0x1F, 0x11, 0x15, 0x11, 0x1F}; + uint8_t edge = size - 7; + for (uint8_t y = 0; y < 7; y++) { + for (uint8_t x = 0; x < 7; x++) { + if ((FINDER[y] >> x) & 1) { + setBit(m, size, x, y, true); + setBit(m, size, edge + x, y, true); + setBit(m, size, x, edge + y, true); + } + if (size > 21 && x < 5 && y < 5 && ((ALIGN[y] >> x) & 1)) + setBit(m, size, size - 9 + x, size - 9 + y, true); + } + } +} + +static void applyMask(uint8_t *m, uint8_t size, uint8_t mask) +{ + for (uint8_t y = 0; y < size; y++) { + for (uint8_t x = 0; x < size; x++) { + if (isFunction(size, x, y)) + continue; + bool invert; + switch (mask) { + case 0: + invert = (x + y) % 2 == 0; + break; + case 1: + invert = y % 2 == 0; + break; + case 2: + invert = x % 3 == 0; + break; + case 3: + invert = (x + y) % 3 == 0; + break; + case 4: + invert = (x / 3 + y / 2) % 2 == 0; + break; + case 5: + invert = x * y % 2 + x * y % 3 == 0; + break; + case 6: + invert = (x * y % 2 + x * y % 3) % 2 == 0; + break; + default: + invert = ((x + y) % 2 + x * y % 3) % 2 == 0; + break; + } + if (invert) { + uint16_t i = y * size + x; + m[i >> 3] ^= 0x80 >> (i & 7); + } + } + } +} + +static uint32_t getPenaltyScore(const uint8_t *m, uint8_t size) +{ + uint32_t result = 0; + uint16_t dark = 0; + + for (uint8_t y = 0; y < size; y++) { + uint8_t runRow = 0, runCol = 0; + bool lastRow = false, lastCol = false; + uint16_t bitsRow = 0, bitsCol = 0; + + for (uint8_t x = 0; x < size; x++) { + bool cRow = getBit(m, size, x, y); + bool cCol = getBit(m, size, y, x); + + if (x == 0 || cRow != lastRow) { + lastRow = cRow; + runRow = 1; + } else if (++runRow == 5) { + result += 3; + } else if (runRow > 5) { + result++; + } + if (x == 0 || cCol != lastCol) { + lastCol = cCol; + runCol = 1; + } else if (++runCol == 5) { + result += 3; + } else if (runCol > 5) { + result++; + } + + if (x > 0 && y > 0 && cRow == getBit(m, size, x - 1, y - 1) && cRow == getBit(m, size, x, y - 1) && + cRow == getBit(m, size, x - 1, y)) + result += 3; + + // Finder-like 1:1:3:1:1 runs + bitsRow = ((bitsRow << 1) & 0x7FF) | cRow; + bitsCol = ((bitsCol << 1) & 0x7FF) | cCol; + if (x >= 10) { + if (bitsRow == 0x05D || bitsRow == 0x5D0) + result += 40; + if (bitsCol == 0x05D || bitsCol == 0x5D0) + result += 40; + } + + dark += cRow; + } + } + + uint16_t total = size * size; + for (uint16_t k = 0; dark * 20 < (9 - k) * total || dark * 20 > (11 + k) * total; k++) + result += 10; + return result; +} + +// Reed-Solomon over GF(2^8 / 0x11D) +static uint8_t rsMultiply(uint8_t x, uint8_t y) +{ + uint16_t z = 0; + for (int8_t i = 7; i >= 0; i--) { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + return z; +} + +static void rsInit(uint8_t degree, uint8_t *coeff) +{ + memset(coeff, 0, degree); + coeff[degree - 1] = 1; + uint8_t root = 1; + for (uint8_t i = 0; i < degree; i++) { + for (uint8_t j = 0; j < degree; j++) { + coeff[j] = rsMultiply(coeff[j], root); + if (j + 1 < degree) + coeff[j] ^= coeff[j + 1]; + } + root = rsMultiply(root, 0x02); + } +} + +// result must be zeroed +static void rsRemainder(uint8_t degree, const uint8_t *coeff, const uint8_t *data, uint8_t length, uint8_t *result) +{ + for (uint8_t i = 0; i < length; i++) { + uint8_t factor = data[i] ^ result[0]; + memmove(result, result + 1, degree - 1); + result[degree - 1] = 0; + for (uint8_t j = 0; j < degree; j++) + result[j] ^= rsMultiply(coeff[j], factor); + } +} + +static void appendBits(uint8_t *buf, uint16_t *offset, uint8_t val, uint8_t length) +{ + for (int8_t i = length - 1; i >= 0; i--, (*offset)++) + buf[*offset >> 3] |= ((val >> i) & 1) << (7 - (*offset & 7)); +} + +bool qrEncodeText(QrEncode *qr, uint8_t *modules, const char *text) +{ + size_t length = strlen(text); + uint8_t v = 1; + // 4-bit mode + 8-bit count + data bytes + while (length + 2 > (size_t)(TOTAL_CODEWORDS[v - 1] - ECC_PER_BLOCK[v - 1] * NUM_BLOCKS[v - 1])) { + if (++v > QR_ENCODE_MAX_VERSION) + return false; + } + + uint8_t size = QR_ENCODE_SIZE(v); + uint8_t total = TOTAL_CODEWORDS[v - 1]; + uint8_t eccLen = ECC_PER_BLOCK[v - 1]; + uint8_t blocks = NUM_BLOCKS[v - 1]; + uint8_t dataCap = total - eccLen * blocks; + uint8_t blockLen = dataCap / blocks; + + qr->version = v; + qr->size = size; + qr->modules = modules; + + // Data blocks then ECC blocks, each contiguous; interleaving happens during placement + uint8_t cw[MAX_CODEWORDS]; + memset(cw, 0, total); + uint16_t bit = 0; + appendBits(cw, &bit, 0x4, 4); // Byte mode + appendBits(cw, &bit, length, 8); + for (size_t i = 0; i < length; i++) + appendBits(cw, &bit, (uint8_t)text[i], 8); + // Terminator and byte-alignment bits are already zero + for (uint8_t i = (bit + 7) >> 3, pad = 0xEC; i < dataCap; i++, pad ^= 0xEC ^ 0x11) + cw[i] = pad; + + uint8_t coeff[MAX_ECC_PER_BLOCK]; + rsInit(eccLen, coeff); + for (uint8_t b = 0; b < blocks; b++) + rsRemainder(eccLen, coeff, cw + b * blockLen, blockLen, cw + dataCap + b * eccLen); + + memset(modules, 0, QR_ENCODE_BUFFER_SIZE(v)); + drawFunctionPatterns(modules, size); + + uint16_t i = 0; + uint8_t cur = 0; + for (int8_t right = size - 1; right >= 1; right -= 2) { + if (right == 6) + right = 5; + for (uint8_t vert = 0; vert < size; vert++) { + for (uint8_t j = 0; j < 2; j++) { + uint8_t x = right - j; + bool upwards = ((right & 2) == 0) ^ (x < 6); + uint8_t y = upwards ? size - 1 - vert : vert; + if (isFunction(size, x, y) || i >= total * 8) + continue; // Remainder bits stay light + if ((i & 7) == 0) { + uint8_t k = i >> 3; + if (k < dataCap) { + cur = cw[(k % blocks) * blockLen + k / blocks]; + } else { + k -= dataCap; + cur = cw[dataCap + (k % blocks) * eccLen + k / blocks]; + } + } + if (cur & (0x80 >> (i & 7))) + setBit(modules, size, x, y, true); + i++; + } + } + } + + uint8_t mask = 0; + uint32_t minPenalty = UINT32_MAX; + for (uint8_t m = 0; m < 8; m++) { + drawFormatBits(modules, size, m); + applyMask(modules, size, m); + uint32_t penalty = getPenaltyScore(modules, size); + if (penalty < minPenalty) { + mask = m; + minPenalty = penalty; + } + applyMask(modules, size, m); // XOR again to undo + } + + qr->mask = mask; + drawFormatBits(modules, size, mask); + applyMask(modules, size, mask); + return true; +} + +bool qrGetModule(const QrEncode *qr, uint8_t x, uint8_t y) +{ + if (x >= qr->size || y >= qr->size) + return false; + return getBit(qr->modules, qr->size, x, y); +} diff --git a/src/graphics/QrEncode.h b/src/graphics/QrEncode.h new file mode 100644 index 0000000000..9057074ca1 --- /dev/null +++ b/src/graphics/QrEncode.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +// Minimal QR encoder: ECC level M, versions 1-6 (21-41 modules, up to 106 bytes), byte mode only. + +#define QR_ENCODE_MAX_VERSION 6 +#define QR_ENCODE_SIZE(version) (4 * (version) + 17) +#define QR_ENCODE_BUFFER_SIZE(version) ((QR_ENCODE_SIZE(version) * QR_ENCODE_SIZE(version) + 7) / 8) + +struct QrEncode { + uint8_t version; + uint8_t size; + uint8_t mask; + uint8_t *modules; +}; + +// modules must hold QR_ENCODE_BUFFER_SIZE(QR_ENCODE_MAX_VERSION) bytes; picks the smallest version that fits. +bool qrEncodeText(QrEncode *qr, uint8_t *modules, const char *text); + +// true for a dark module; false outside the symbol +bool qrGetModule(const QrEncode *qr, uint8_t x, uint8_t y); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0350405e08..d3a9da2a56 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1542,6 +1542,14 @@ void Screen::setFrames(FrameFocus focus) } #endif +#if HAS_BLUETOOTH + if (config.bluetooth.enabled) { + normalFrames[numframes++] = graphics::DebugRenderer::drawFrameBluetoothPair; + indicatorIcons.push_back(icon_qr); + PUSH_FRAME_TITLE("Pair"); + } +#endif + // Beware of what changes you make in this code! // We pass numframes into GetMeshModulesWithUIFrames() which is highly important! // Inside of that callback, goes over to MeshModule.cpp and we run diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index da03cb0e99..63fa9c87d6 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -8,6 +8,7 @@ #include "UIRenderer.h" #include "airtime.h" #include "gps/RTC.h" +#include "graphics/QrEncode.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/TFTColorRegions.h" @@ -15,6 +16,7 @@ #include "graphics/TimeFormatters.h" #include "graphics/images.h" #include "main.h" +#include "meshUtils.h" #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" @@ -648,6 +650,84 @@ void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int1 display->drawString(textX, getTextPositions(display)[line++], "World!"); } +// The PIN pairing will ask for: the fixed one, or the random one picked when Bluetooth started +static bool getPairingPin(uint32_t *pin) +{ +#if defined(ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_BLUETOOTH + return nimbleBluetooth && nimbleBluetooth->getPasskey(pin); +#elif defined(ARCH_NRF52) + return nrf52Bluetooth && nrf52Bluetooth->getPasskey(pin); +#else + (void)pin; + return false; +#endif +} + +void drawFrameBluetoothPair(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + graphics::drawCommonHeader(display, x, y, "Pair"); + + uint32_t pin = 0; + const bool hasPin = getPairingPin(&pin); + char mac[18] = ""; + uint8_t addr[6]; + if (getBluetoothAddr(addr)) + formatMacAddr(addr, mac, sizeof(mac)); + + // Re-encode only when the link changes, not on every redraw + static char cachedLink[96]; + static uint8_t modules[QR_ENCODE_BUFFER_SIZE(QR_ENCODE_MAX_VERSION)]; + static QrEncode qr; + static bool valid = false; + char link[sizeof(cachedLink)]; + if (hasPin) + snprintf(link, sizeof(link), "meshtastic://meshtastic/connections?address=x%s&pin=%06u", mac, (unsigned)pin); + else + snprintf(link, sizeof(link), "meshtastic://meshtastic/connections?address=x%s", mac); + if (!mac[0]) { + valid = false; + cachedLink[0] = '\0'; // Re-encode once the address is back, even if unchanged + } else if (strcmp(link, cachedLink) != 0) { + strcpy(cachedLink, link); + valid = qrEncodeText(&qr, modules, link); + } + + const int top = getTextPositions(display)[1] + y; + if (!valid) { + display->drawString(x + BASEUI_BODY_LR_MARGIN, top, "No BLE address"); + return; + } + + // 2-module quiet zone; largest whole-pixel scale that fits below the header + const int span = qr.size + 4; + const int availH = display->getHeight() - (top - y); + const int availW = display->getWidth(); + int scale = (availH < availW ? availH : availW) / span; + if (scale < 1) { + display->drawString(x + BASEUI_BODY_LR_MARGIN, top, "Screen too small"); + return; + } + const int qrPx = span * scale; + const int qrX = x + (availW - qrPx) / 2; + const int qrY = top + (availH - qrPx) / 2; + + display->setColor(WHITE); + display->fillRect(qrX, qrY, qrPx, qrPx); + display->setColor(BLACK); + for (uint8_t my = 0; my < qr.size; my++) + for (uint8_t mx = 0; mx < qr.size; mx++) + if (qrGetModule(&qr, mx, my)) + display->fillRect(qrX + (mx + 2) * scale, qrY + (my + 2) * scale, scale, scale); + display->setColor(WHITE); +#if GRAPHICS_TFT_COLORING_ENABLED + // Keep dark-on-light whatever the theme, so phone scanners read it + graphics::registerTFTColorRegionDirect(qrX, qrY, qrPx, qrPx, graphics::TFTPalette::White, graphics::TFTPalette::Black); +#endif +} + } // namespace DebugRenderer } // namespace graphics #endif diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index 41b4c2bce6..0151ea0e23 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -27,6 +27,8 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); // Chirpy screen display void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +// QR code with the app's BLE connect link +void drawFrameBluetoothPair(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/images.h b/src/graphics/images.h index a4ae77e5f0..fc155a6092 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -121,6 +121,9 @@ const uint8_t icon_system[] PROGMEM = { 0x24 // Row 7: ..#..#.. }; +// QR code (BLE pairing) +const uint8_t icon_qr[] PROGMEM = {0xE7, 0xA5, 0xE7, 0x18, 0x18, 0x27, 0x25, 0xA7}; + // 🌐 Wi-Fi const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000, 0b00011000, 0b00000000}; diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index e01c32c72d..22dca9e303 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -101,6 +101,8 @@ BLECharacteristic *logRadioCharacteristic; BLEServer *bleServer; static bool passkeyShowing; +static uint32_t configuredPasskey; +static bool passkeyConfigured; static std::atomic nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection" // Set by onDisconnect to defer (re)starting advertising to the main task. A stale-bond reconnect @@ -876,6 +878,14 @@ bool NimbleBluetooth::isActive() return bleServer != nullptr; } +bool NimbleBluetooth::getPasskey(uint32_t *pin) +{ + if (!passkeyConfigured) + return false; + *pin = configuredPasskey; + return true; +} + bool NimbleBluetooth::isConnected() { return nimbleBluetoothConnHandle.load() != BLE_HS_CONN_HANDLE_NONE; @@ -940,17 +950,19 @@ void NimbleBluetooth::setup() // Set the passkey if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) { LOG_INFO("Use random passkey"); - security.setPassKey(false); // generate a random passkey + configuredPasskey = security.setPassKey(false); // generate a random passkey } else { LOG_INFO("Use fixed passkey"); - security.setPassKey(true, config.bluetooth.fixed_pin); + configuredPasskey = security.setPassKey(true, config.bluetooth.fixed_pin); } + passkeyConfigured = true; // Enable authorization requirements: // - bonding: true (for persistent storage of the keys) // - MITM: true (enables Man-In-The-Middle protection for password prompts) // - secure connection: true (enables secure connection for encryption) security.setAuthenticationMode(true, true, true); } else { + passkeyConfigured = false; // No IO capability for no PIN mode security.setCapability(ESP_IO_CAP_NONE); // No PIN mode: no MITM protection diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h index dad0a8c985..9af5fd9624 100644 --- a/src/nimble/NimbleBluetooth.h +++ b/src/nimble/NimbleBluetooth.h @@ -11,6 +11,8 @@ class NimbleBluetooth : BluetoothApi bool isActive(); bool isConnected(); int getRssi(); + // The PIN pairing asks for; false in NO_PIN mode or before setup() + bool getPasskey(uint32_t *pin); void sendLog(const uint8_t *logMessage, size_t length); void startAdvertising(); bool isDeInit = false; diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 76eaae0789..af3e14a3d3 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -338,6 +338,16 @@ void setupMeshService(void) logRadio.begin(); } static uint32_t configuredPasskey; +static bool passkeyConfigured; + +bool NRF52Bluetooth::getPasskey(uint32_t *pin) +{ + if (!passkeyConfigured) + return false; + *pin = configuredPasskey; + return true; +} + void NRF52Bluetooth::shutdown() { // Shutdown bluetooth for minimum power draw @@ -404,6 +414,7 @@ void NRF52Bluetooth::setup() HardwareRNG::fill(reinterpret_cast(&hwrand), sizeof(hwrand)); configuredPasskey = hwrand % 900000u + 100000u; } + passkeyConfigured = true; auto pinString = std::to_string(configuredPasskey); LOG_INFO("Bluetooth pin set to '%i'", configuredPasskey); Bluefruit.Security.setPIN(pinString.c_str()); @@ -413,6 +424,7 @@ void NRF52Bluetooth::setup() Bluefruit.Security.setSecuredCallback(NRF52Bluetooth::onConnectionSecured); meshBleService.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM); } else { + passkeyConfigured = false; Bluefruit.Security.setIOCaps(false, false, false); meshBleService.setPermission(SECMODE_OPEN, SECMODE_OPEN); } diff --git a/src/platform/nrf52/NRF52Bluetooth.h b/src/platform/nrf52/NRF52Bluetooth.h index 630ab05bc8..c82905feb2 100644 --- a/src/platform/nrf52/NRF52Bluetooth.h +++ b/src/platform/nrf52/NRF52Bluetooth.h @@ -13,6 +13,8 @@ class NRF52Bluetooth : BluetoothApi void clearBonds(); bool isConnected(); int getRssi(); + // The PIN pairing asks for; false in NO_PIN mode or before setup() + bool getPasskey(uint32_t *pin); void sendLog(const uint8_t *logMessage, size_t length); private: diff --git a/test/test_qr_encode/test_main.cpp b/test/test_qr_encode/test_main.cpp new file mode 100644 index 0000000000..da9e39425b --- /dev/null +++ b/test/test_qr_encode/test_main.cpp @@ -0,0 +1,106 @@ +// Unit tests for qrEncodeText() / qrGetModule() in src/graphics/QrEncode.cpp - the encoder behind the +// BLE pairing QR frame (DebugRenderer::drawFrameBluetoothPair). +// +// The frame encodes the app's connect link, meshtastic://meshtastic/connections?address=x&pin=, +// which a phone camera must read back byte for byte or the app opens on the wrong device or PIN. The +// encoder is a cut-down ricmoo/QRCode fixed at ECC level M, versions 1-6 and byte mode, so a symbol is +// only correct if every module matches what a full encoder produces for the same version and mask. +// +// Regressions guarded: +// - any change to placement, interleaving, Reed-Solomon, masking or format bits that moves a module. +// The reference matrix below is Project Nayuki's encoder output for the 73-character link (version 5, +// mask 2, which both encoders choose), and it decodes with zxing; +// - picking a larger version than needed (a coarser code on 128x64 panels) or a smaller one that the +// data overflows, at each version's byte-mode capacity boundary; +// - writing past the module buffer instead of rejecting data longer than version 6 holds. +#include "TestUtil.h" +#include "graphics/QrEncode.h" +#include +#include +#include +#include + +void setUp(void) {} +void tearDown(void) {} + +static const char *const kLink = "meshtastic://meshtastic/connections?address=xAC:27:6E:CD:5A:99&pin=317338"; + +static const char *const kLinkModules[] = { + "1111111000101001111011111001101111111", "1000001000101101010100000101101000001", "1011101010100110101111100011101011101", + "1011101011000100110100111111101011101", "1011101011001001000111010000101011101", "1000001011100010001010000001101000001", + "1111111010101010101010101010101111111", "0000000010001000100111110011100000000", "1011111001000011001110010101101111100", + "0011010001110101100000111110010001110", "1000111101001010011010101001000011011", "1111000110101110100001110000011010011", + "0000101001100100111000111110011011011", "1001010101100010110111011110100101010", "1010111010100111110011000101111000111", + "0101000011011001111001111001100100011", "1101101011001111100100001100111111110", "1010100110001101011010110010001001000", + "1110001001011101110100100011001110011", "0111010000110111000111001001110100011", "0010011011010111010100111111001111010", + "0100110101101110101111010100110101110", "0011101000101001001000100001111111001", "0111010000111100000011101010111000010", + "0101001101111101001000011100001110101", "1110010110110111100000110011110001000", "1011001100000100001000101001100010111", + "1000110011111100100001001010000001011", "1010001010011010110110101111111110100", "0000000010001100111111010111100010010", + "1111111000001111010010001000101011011", "1000001010101101111101100011100010010", "1011101011001001100010000101111111101", + "1011101011000001001101110000101111101", "1011101011001011100011101100100101111", "1000001000000111001101110011000101001", + "1111111011011101010000100110000000111"}; + +static uint8_t modules[QR_ENCODE_BUFFER_SIZE(QR_ENCODE_MAX_VERSION)]; + +static void test_connect_link_matches_reference_symbol() +{ + QrEncode qr; + TEST_ASSERT_TRUE(qrEncodeText(&qr, modules, kLink)); + TEST_ASSERT_EQUAL_UINT8(5, qr.version); + TEST_ASSERT_EQUAL_UINT8(37, qr.size); + TEST_ASSERT_EQUAL_UINT8(2, qr.mask); + for (uint8_t y = 0; y < qr.size; y++) { + char row[38]; + for (uint8_t x = 0; x < qr.size; x++) + row[x] = qrGetModule(&qr, x, y) ? '1' : '0'; + row[qr.size] = '\0'; + TEST_ASSERT_EQUAL_STRING_MESSAGE(kLinkModules[y], row, "module row differs from reference"); + } +} + +// Byte-mode capacity at ECC M: 14, 26, 42, 62, 84, 106 characters for versions 1-6. +static void test_smallest_version_is_chosen_at_each_capacity_boundary() +{ + static const uint16_t capacity[6] = {14, 26, 42, 62, 84, 106}; + for (uint8_t v = 1; v <= 6; v++) { + QrEncode qr; + std::string fits(capacity[v - 1], 'a'); + TEST_ASSERT_TRUE(qrEncodeText(&qr, modules, fits.c_str())); + TEST_ASSERT_EQUAL_UINT8(v, qr.version); + TEST_ASSERT_EQUAL_UINT8(4 * v + 17, qr.size); + if (v < 6) { + std::string over(capacity[v - 1] + 1, 'a'); + TEST_ASSERT_TRUE(qrEncodeText(&qr, modules, over.c_str())); + TEST_ASSERT_EQUAL_UINT8(v + 1, qr.version); + } + } +} + +static void test_data_beyond_version_6_is_rejected() +{ + QrEncode qr; + std::string tooLong(107, 'a'); + TEST_ASSERT_FALSE(qrEncodeText(&qr, modules, tooLong.c_str())); +} + +static void test_getModule_outside_symbol_is_light() +{ + QrEncode qr; + TEST_ASSERT_TRUE(qrEncodeText(&qr, modules, "x")); + TEST_ASSERT_TRUE(qrGetModule(&qr, 0, 0)); // finder corner + TEST_ASSERT_FALSE(qrGetModule(&qr, qr.size, 0)); + TEST_ASSERT_FALSE(qrGetModule(&qr, 0, qr.size)); +} + +void setup() +{ + initializeTestEnvironment(); + UNITY_BEGIN(); + RUN_TEST(test_connect_link_matches_reference_symbol); + RUN_TEST(test_smallest_version_is_chosen_at_each_capacity_boundary); + RUN_TEST(test_data_beyond_version_6_is_rejected); + RUN_TEST(test_getModule_outside_symbol_is_light); + exit(UNITY_END()); +} + +void loop() {}