Позвонить Telegram Viber
График работы: пн–пт 9:00–18:00

Бездротовий монітор BMS Jiabaida (JBD) на ESP32 з OLED‑дисплеєм (корпус + код)

RizneVdim

Час збірки: 60 хв

Складність: низька

Компоненти

  • ESP32-C3
  • display 0.96" 128×64
  • case

 Якщо у тебе стоїть LiFePO₄‑акумулятор — у резервного живлення, сонячної станції або просто в майстерні — ця стаття буде корисною. Щоб дізнатися стан батареї через БМС з Bluetooth, доводиться щоразу брати телефон, запускати застосунок, чекати з'єднання… І так знову і знову. А якщо акумулятор взагалі в сусідньому приміщенні? Ходити туди з телефоном — задоволення сумнівне.

 Я зібрав пристрій, який вирішує цю проблему раз і назавжди. Компактний Bluetooth‑монітор БМС акумулятора з невеликим дисплеєм відображає мінімально необхідну інформацію про стан БМС та акумулятора. Жодного телефону. Жодних застосунків. Просто підключив до звичайної зарядки Type‑C — і одразу все видно.

Наприкінці статті — код скетчу для прошивки esp32 в Arduino IDE!

У підсумку ви отримаєте

  • Готовий монітор BMS з екраном
  • Автоматичне підключення через Bluetooth
  • Відображення всіх ключових параметрів
  • Компактний корпус для друку
  • Готовий скетч

Що знадобиться (компоненти)

ESP32-C3 Super Mini — 1 шт  

OLED SSD1306 128×64 — 1 шт  

BMS Jiabaida (JBD) з BLE — 1 шт  >> Джерело даних (протестовано на JBD DP04S007)

OLED дисплей 0.96" 128×64 (SSD1306)

Двокольоровий дисплей:

  • жовта зона — статус
  • синя — дані

Корпус (3D)

Для проєкту розроблено компактний корпус з 3 частин.

  • Лицьова частина із засувками під дисплей
  • Тильна частина
  • Середня частина із засувкою під встановлення ESP32

Підходить для друку на будь-якому FDM‑принтері.

Збирання

  • Припаяти гребінку до ESP32 C3 Super Mini
  • Припаяти дроти до дисплея, довжина ~5 см
  • Завантажити скетч через Arduino IDE (код — наприкінці)
  • Закріпити дисплей у лицьовій панелі — засувки без клею
  • Встановити ESP32 у середню частину корпусу — засувки без клею
  • З'єднати дроти і закрити кришку
  • Подати живлення — на роз'єм Type‑C мікроконтролера (5В)

Схема підключення

OLED підключається до ESP32, представлена на картинці

Поради

  • перевіряй живлення OLED (часто помиляються)
  • не плутай SPI та I2C версії дисплея
  • тримай ESP32 ближче до БМС
  • скетч — основа, можна змінювати для інших дисплеїв

Бездротовий монітор БМС працює за простою схемою:

  • ESP32 запускається
  • сканує BLE
  • знаходить BMS
  • підключається
  • отримує дані
  • виводить їх на екран

Стани системи:

  • START
  • CONNECTING
  • CONNECTED
  • DATA
  • REBOOT

Налаштування та перевірка

Після прошивки:

  • пристрій автоматично шукає BMS
  • підключається
  • починає відображати дані

Можна змінювати:

  • MAC‑адресу BMS
  • параметри відображення
  • інтервали оновлення

Що показує монітор

Екран розділений на 2 зони:

Верхня (жовта)

  • CHARGE
  • DISCHARGE
  • IDLE

Нижня (синя)

  • напруга
  • потужність
  • час до заряду/розряду

Можливі проблеми

Не підключається до BMS

- неправильний MAC

- слабкий сигнал

Порожній екран

- помилка підключення дисплея

- неправильна бібліотека

Глючить відображення

- проблеми з живленням

- помилки в парсингу пакета

Завантажити та використовувати

Повний скетч

Бібліотеки

STL корпус

Підсумок

Вийшов простий і корисний девайс:

  • працює автономно
  • не потребує смартфона
  • відображає стан БМС та акумулятора
  • І головне — збирається за вечір без болю і страждань (ну майже).

FAQ

Чи підходить для будь-якої JBD?

Теоретично так, якщо є Bluetooth. 100% протестовано на JBD DP04S007.

Чи можна інший дисплей?

Так, SSD1306 128×64 (SPI або I2C).

Чи потрібен застосунок?

Ні. Все працює автономно.

Код ниже полностью готов — просто измените MAC адрес на свой и прошейте ESP32
/*
 * ESP32-C3 Mini BMS Monitor with two-color SSD1306 (SPI)
 * 
 * Display connection (SPI) – full pin comments:
 *   OLED_MOSI (D1)  -> GPIO7
 *   OLED_CLK  (D0)  -> GPIO6
 *   OLED_DC   (DC)  -> GPIO2
 *   OLED_CS   (CS)  -> GPIO10
 *   OLED_RESET(RES) -> GPIO5
 *   VCC             -> 3.3V
 *   GND             -> GND
 * 
 * Hardware:
 * - ESP32-C3 Super Mini
 * - SSD1306 128x64 SPI (two-color: yellow 16px, blue 48px)
 * - LED on pin 8
 * 
 * LED behavior:
 * - On startup: always on
 * - During connection (5 attempts): blinking 500/500 ms
 * - On receiving data: short flash every 3 seconds
 */

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <NimBLEDevice.h>
#include "esp_task_wdt.h"

/* ================= ДИСПЛЕЙ (SPI) ================= */
#define OLED_MOSI  7   // D1
#define OLED_CLK   6   // D0
#define OLED_DC    2   // DC
#define OLED_CS    10  // CS
#define OLED_RESET 5   // RES

Adafruit_SSD1306 display(128, 64, OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);

/* ================= LED ================= */
#define LED_PIN 8

/* ================= BLE ================= */
#define BMS_MAC "a5:c2:37:2a:a2:f0"
static NimBLEAddress deviceAddress(BMS_MAC, 0);
static NimBLEClient* connectedClient = nullptr;

static uint8_t notifyBuffer[256];
static size_t notifyLen = 0;

/* ================= ТАЙМЕРЫ ================= */
unsigned long nowMs;
unsigned long lastRequestMs = 0;
unsigned long lastDataMs    = 0;
unsigned long lastLedMs     = 0;
unsigned long rebootAtMs    = 0;
unsigned long lastWatchdogFeedMs = 0;

/* ================= КОНСТАНТЫ ================= */
const unsigned long REQUEST_INTERVAL = 3000;
const unsigned long DATA_TIMEOUT     = 15000;
const unsigned long WATCHDOG_FEED_INTERVAL = 1000;
const unsigned long WATCHDOG_TIMEOUT = 30;
const unsigned long LED_BLINK_INTERVAL = 500;
const unsigned long LED_FLASH_INTERVAL = 3000;
const unsigned long LED_FLASH_DURATION = 50;

const uint8_t CMD_BASIC[] = {0xDD,0xA5,0x03,0x00,0xFF,0xFD,0x77};

/* ===== БАТАРЕЯ ===== */
const float BAT_CAPACITY_AH = 100.0f;

/* ===== КОНСТАНТЫ ПАРСЕРА ===== */
const size_t PACKET_MIN_LEN = 41;
const float VOLTAGE_MAX = 100.0f;
const uint8_t SOC_MAX = 100;
const float CURRENT_THRESHOLD = 0.05f;
const float CURRENT_THRESHOLD_ETA = 0.1f;
const float CURRENT_MIN_FOR_DIVISION = 0.001f;

/* ================= ДАННЫЕ BMS ================= */
struct BMSData {
  float voltage;
  float current;
  float power;
  uint8_t soc;
  float temp;               // температура BMS (только для логов)
  bool isValid = false;
} bms, prevBms;

/* ================= СОСТОЯНИЯ ================= */
enum State {
  ST_START,
  ST_CONNECTING,
  ST_CONNECTED,
  ST_DATA,
  ST_REBOOT
};
State state = ST_START;
State prevState = ST_START;

uint8_t connectAttempts = 0;
const uint8_t MAX_CONNECT_ATTEMPTS = 5;

/* ================= КЭШ ДИСПЛЕЯ (4 строки) ================= */
static char lastLine0[32] = "";  // Режим (жёлтая зона, шрифт 2)
static char lastLine1[32] = "";  // SOC и напряжение (шрифт 1)
static char lastLine2[32] = "";  // Мощность (шрифт 1)
static char lastLine3[32] = "";  // ETA (шрифт 1)

/* ================= LED ================= */
bool ledBlinkState = false;
bool ledFlashActive = false;
unsigned long ledFlashStartMs = 0;

/* ================= ЛОГИРОВАНИЕ ================= */
void logInfo(const char* msg) {
  Serial.printf("[INFO] %lu: %s\n", millis(), msg);
}
void logDebug(const char* msg) {
  Serial.printf("[DEBUG] %lu: %s\n", millis(), msg);
}
void logError(const char* msg) {
  Serial.printf("[ERROR] %lu: %s\n", millis(), msg);
}
void logStateChange(State oldState, State newState) {
  const char* stateNames[] = {"ST_START", "ST_CONNECTING", "ST_CONNECTED", "ST_DATA", "ST_REBOOT"};
  Serial.printf("[STATE] %lu: %s -> %s\n", millis(), stateNames[oldState], stateNames[newState]);
}

/* ================= ДИСПЛЕЙ (SSD1306, двухцветный) ================= */
// Геометрия: жёлтая область 16px, синяя 48px
static const int YELLOW_HEIGHT = 16;
static const int BLUE_START = YELLOW_HEIGHT;

// Размеры шрифтов
static const uint8_t TEXT_SIZE_BIG = 2;    // для жёлтой строки и заставки
static const uint8_t TEXT_SIZE_SMALL = 1;  // для синих строк и статусных сообщений

// Высоты строк
static const int LINE_H_BIG = 16;           // высота жёлтой строки
static const int LINE_H_SMALL = 12;         // высота каждой синей строки (шрифт 8 + отступ 4)

// Координаты Y для строк (с учётом центрирования внутри своих зон)
static const int LINE_Y0 = 0;                                  // жёлтая строка (занимает всю жёлтую зону)
static const int LINE_Y1 = BLUE_START + 6;                     // первая синяя строка (отступ 6 сверху)
static const int LINE_Y2 = LINE_Y1 + LINE_H_SMALL;             // вторая синяя строка
static const int LINE_Y3 = LINE_Y2 + LINE_H_SMALL;             // третья синяя строка

// Отрисовка конкретной строки с автоматическим центрированием
void drawLine(uint8_t line, const char* text) {
  int yTop;
  char* lastLine;
  uint8_t textSize;
  int lineHeight;

  switch (line) {
    case 0:
      yTop = LINE_Y0;
      lastLine = lastLine0;
      textSize = TEXT_SIZE_BIG;
      lineHeight = LINE_H_BIG;
      break;
    case 1:
      yTop = LINE_Y1;
      lastLine = lastLine1;
      textSize = TEXT_SIZE_SMALL;
      lineHeight = LINE_H_SMALL;
      break;
    case 2:
      yTop = LINE_Y2;
      lastLine = lastLine2;
      textSize = TEXT_SIZE_SMALL;
      lineHeight = LINE_H_SMALL;
      break;
    case 3:
      yTop = LINE_Y3;
      lastLine = lastLine3;
      textSize = TEXT_SIZE_SMALL;
      lineHeight = LINE_H_SMALL;
      break;
    default: return;
  }

  // Если текст не изменился – ничего не делаем
  if (strcmp(lastLine, text) == 0) return;

  strncpy(lastLine, text, 31);
  lastLine[31] = '\0';

  // Очищаем область строки
  display.fillRect(0, yTop, 128, lineHeight, SSD1306_BLACK);

  // Центрируем текст по горизонтали и вертикали внутри строки
  display.setTextSize(textSize);
  display.setTextColor(SSD1306_WHITE);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
  int x = (128 - w) / 2;
  if (x < 0) x = 0;
  // Вертикальное центрирование внутри строки высотой lineHeight
  int y = yTop + (lineHeight - h) / 2;

  display.setCursor(x, y);
  display.print(text);
  display.display();
}

// Полная отрисовка экрана с данными (при переходе в ST_DATA)
void drawDataScreen() {
  display.clearDisplay();
  display.setTextColor(SSD1306_WHITE);

  // Сброс кэша
  lastLine0[0] = '\0';
  lastLine1[0] = '\0';
  lastLine2[0] = '\0';
  lastLine3[0] = '\0';

  if (bms.isValid) {
    // Строка 0 (жёлтая): режим
    char mode[16];
    if (bms.current > CURRENT_THRESHOLD) strcpy(mode, "CHARGE");
    else if (bms.current < -CURRENT_THRESHOLD) strcpy(mode, "DISCHARGE");
    else strcpy(mode, "IDLE");
    drawLine(0, mode);

    // Строка 1: SOC и напряжение (компактно, 1 знак после запятой)
    char line1[32];
    snprintf(line1, sizeof(line1), "%d%%%.1fV", bms.soc, bms.voltage);
    drawLine(1, line1);

    // Строка 2: мощность
    char line2[32];
    snprintf(line2, sizeof(line2), "%dW", (int)bms.power);
    drawLine(2, line2);

    // Строка 3: ETA
    char line3[32];
    if (fabs(bms.current) > CURRENT_THRESHOLD_ETA && fabs(bms.current) >= CURRENT_MIN_FOR_DIVISION) {
      float socFrac = bms.soc / 100.0f;
      float hours;
      if (bms.current > 0)
        hours = (BAT_CAPACITY_AH * (1.0f - socFrac)) / bms.current;
      else
        hours = (BAT_CAPACITY_AH * socFrac) / fabs(bms.current);
      int h = (int)hours;
      int m = (int)((hours - h) * 60);
      snprintf(line3, sizeof(line3), "%d:%02d", h, m);
    } else {
      strcpy(line3, "00:00");
    }
    drawLine(3, line3);
  }
  display.display();
}

// Показать сообщение по центру всего экрана (заставка, статусы)
void showCenteredMessage(const char* msg, bool bigFont = false) {
  display.clearDisplay();
  display.setTextSize(bigFont ? TEXT_SIZE_BIG : TEXT_SIZE_SMALL);
  display.setTextColor(SSD1306_WHITE);
  int16_t x1, y1;
  uint16_t w, h;
  display.getTextBounds(msg, 0, 0, &x1, &y1, &w, &h);
  int x = (128 - w) / 2;
  int y = (64 - h) / 2;
  display.setCursor(x, y);
  display.print(msg);
  display.display();

  // Сброс кэша строк
  lastLine0[0] = '\0';
  lastLine1[0] = '\0';
  lastLine2[0] = '\0';
  lastLine3[0] = '\0';
}

/* ================= LED ================= */
void updateLed() {
  if (state == ST_START) {
    digitalWrite(LED_PIN, HIGH);
    ledFlashActive = false;
  } else if (state == ST_CONNECTING) {
    if (nowMs - lastLedMs >= LED_BLINK_INTERVAL) {
      ledBlinkState = !ledBlinkState;
      digitalWrite(LED_PIN, ledBlinkState ? HIGH : LOW);
      lastLedMs = nowMs;
    }
    ledFlashActive = false;
  } else if (state == ST_DATA) {
    if (!ledFlashActive) {
      digitalWrite(LED_PIN, LOW);  // обычно выключен
      if (nowMs - lastLedMs >= LED_FLASH_INTERVAL) {
        ledFlashActive = true;
        ledFlashStartMs = nowMs;
        digitalWrite(LED_PIN, HIGH);
      }
    } else if (nowMs - ledFlashStartMs >= LED_FLASH_DURATION) {
      ledFlashActive = false;
      digitalWrite(LED_PIN, LOW);
      lastLedMs = nowMs;
    }
  } else {
    digitalWrite(LED_PIN, HIGH);
    ledFlashActive = false;
  }
}

/* ================= ПАРСЕР ================= */
void parseBasicData(const uint8_t* d, size_t len) {
  if (len < PACKET_MIN_LEN || d[0] != 0xDD || d[1] != 0x03 || d[len - 1] != 0x77) {
    logError("Invalid packet format");
    return;
  }

  prevBms = bms;
  const uint8_t* p = d + 4;

  float voltage = ((p[0] << 8) | p[1]) / 100.0f;
  float current = (int16_t)((p[2] << 8) | p[3]) / 100.0f;
  uint8_t soc = p[19];
  int16_t rawTemp = (int16_t)(((uint16_t)p[23] << 8) | p[24]);
  float temp = (rawTemp - 2731) / 10.0f;

  if (voltage < 0 || voltage > VOLTAGE_MAX || soc > SOC_MAX) {
    logError("Invalid data values");
    return;
  }

  bms.voltage = voltage;
  bms.current = current;
  bms.soc = soc;
  bms.power = bms.voltage * fabs(bms.current);
  bms.temp = temp;
  bms.isValid = true;
  lastDataMs = millis();

  if (state != ST_DATA) {
    State oldState = state;
    state = ST_DATA;
    logStateChange(oldState, state);
    drawDataScreen();  // полная отрисовка при первом получении данных
    logDebug("State changed to ST_DATA, full redraw");
  } else {
    // Частичное обновление строк
    char mode[16];
    if (bms.current > CURRENT_THRESHOLD) strcpy(mode, "CHARGE");
    else if (bms.current < -CURRENT_THRESHOLD) strcpy(mode, "DISCHARGE");
    else strcpy(mode, "IDLE");
    drawLine(0, mode);

    char line1[32];
    snprintf(line1, sizeof(line1), "%d%%%.1fV", bms.soc, bms.voltage);
    drawLine(1, line1);

    char line2[32];
    snprintf(line2, sizeof(line2), "%dW", (int)bms.power);
    drawLine(2, line2);

    char line3[32];
    if (fabs(bms.current) > CURRENT_THRESHOLD_ETA && fabs(bms.current) >= CURRENT_MIN_FOR_DIVISION) {
      float socFrac = bms.soc / 100.0f;
      float hours = (bms.current > 0) ? (BAT_CAPACITY_AH * (1.0f - socFrac)) / bms.current
                                      : (BAT_CAPACITY_AH * socFrac) / fabs(bms.current);
      int h = (int)hours;
      int m = (int)((hours - h) * 60);
      snprintf(line3, sizeof(line3), "%d:%02d", h, m);
    } else {
      strcpy(line3, "00:00");
    }
    drawLine(3, line3);
  }
}

/* ================= NOTIFY ================= */
void notifyCallback(NimBLERemoteCharacteristic*, uint8_t* data, size_t len, bool) {
  for (size_t i = 0; i < len; i++) {
    uint8_t b = data[i];
    if (b == 0xDD) {
      notifyLen = 0;
      notifyBuffer[notifyLen++] = b;
      continue;
    }
    if (notifyLen < sizeof(notifyBuffer)) {
      notifyBuffer[notifyLen++] = b;
    } else {
      notifyLen = 0;
      continue;
    }
    if (b == 0x77 && notifyLen >= 6) {
      if (notifyBuffer[1] == 0x03) parseBasicData(notifyBuffer, notifyLen);
      notifyLen = 0;
    }
  }
}

/* ================= СКАНИРОВАНИЕ ================= */
class ScanCallbacks : public NimBLEScanCallbacks {
  void onResult(const NimBLEAdvertisedDevice* dev) override {
    if (dev->getAddress().equals(deviceAddress)) {
      NimBLEDevice::getScan()->stop();
      logInfo("BMS device found in scan");
    }
  }
};

/* ================= УПРАВЛЕНИЕ BLE ================= */
void cleanupBleResources() {
  logInfo("Cleaning up BLE resources");
  if (connectedClient) {
    if (connectedClient->isConnected()) connectedClient->disconnect();
    NimBLEDevice::deleteClient(connectedClient);
    connectedClient = nullptr;
  }
  notifyLen = 0;
  bms.isValid = false;
}

/* ================= ПОДКЛЮЧЕНИЕ С ПОПЫТКАМИ ================= */
bool tryConnectWithAttempts() {
  State oldState = state;
  state = ST_CONNECTING;
  logStateChange(oldState, state);
  showCenteredMessage("CONNECT", false); // мелкий шрифт
  logInfo("Starting connection attempts");

  auto scan = NimBLEDevice::getScan();
  if (scan) scan->stop();

  connectAttempts = 0;
  esp_task_wdt_reset();

  for (uint8_t attempt = 1; attempt <= MAX_CONNECT_ATTEMPTS; ++attempt) {
    esp_task_wdt_reset();
    connectAttempts = attempt;
    char msg[32];
    snprintf(msg, sizeof(msg), "Connect %d/%d", attempt, MAX_CONNECT_ATTEMPTS);
    showCenteredMessage(msg, false); // мелкий шрифт
    logDebug(msg);

    NimBLEClient* c = NimBLEDevice::createClient();
    if (!c) { delay(1000); continue; }

    if (!c->connect(deviceAddress)) {
      NimBLEDevice::deleteClient(c);
      delay(1000);
      continue;
    }

    auto s = c->getService("ff00");
    if (!s) { c->disconnect(); NimBLEDevice::deleteClient(c); delay(1000); continue; }

    auto n = s->getCharacteristic("ff01");
    if (n && n->canNotify()) n->subscribe(true, notifyCallback);

    auto w = s->getCharacteristic("ff02");
    if (!w) { c->disconnect(); NimBLEDevice::deleteClient(c); delay(1000); continue; }

    w->writeValue(CMD_BASIC, sizeof(CMD_BASIC), false);

    connectedClient = c;
    lastRequestMs = millis();

    oldState = state;
    state = ST_CONNECTED;
    logStateChange(oldState, state);
    showCenteredMessage("CONNECTED", false); // мелкий шрифт
    logInfo("Connected - waiting for data");
    digitalWrite(LED_PIN, HIGH);
    return true;
  }

  oldState = state;
  state = ST_REBOOT;
  logStateChange(oldState, state);
  showCenteredMessage("REBOOT", false); // мелкий шрифт
  rebootAtMs = millis() + 3000;
  return false;
}

void checkConnectionStatus() {
  if (connectedClient && !connectedClient->isConnected()) {
    logError("BLE connection lost");
    cleanupBleResources();
    tryConnectWithAttempts();
  }
}

/* ================= WATCHDOG ================= */
void feedWatchdog() {
  if (nowMs - lastWatchdogFeedMs >= WATCHDOG_FEED_INTERVAL) {
    esp_task_wdt_reset();
    lastWatchdogFeedMs = nowMs;
  }
}

/* ================= SETUP ================= */
void setup() {
  Serial.begin(115200);
  delay(200);
  logInfo("=== BMS Monitor SSD1306 SPI ===");

  esp_task_wdt_config_t wdt_config;
  wdt_config.timeout_ms = WATCHDOG_TIMEOUT * 1000;
  wdt_config.idle_core_mask = 0;
  wdt_config.trigger_panic = true;
  esp_task_wdt_init(&wdt_config);
  esp_task_wdt_add(NULL);

  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
  state = ST_START;

  // Инициализация SPI и дисплея
  SPI.begin(OLED_CLK, -1, OLED_MOSI, OLED_CS);
  if (!display.begin(SSD1306_SWITCHCAPVCC)) {
    logError("SSD1306 allocation failed");
    while (true);
  }
  display.setTextColor(SSD1306_WHITE);
  display.clearDisplay();

  // Заставка (большой шрифт)
  showCenteredMessage("CUB v1.0", true);
  delay(2000);

  logInfo("Initializing BLE");
  NimBLEDevice::init("");
  NimBLEDevice::setPower(ESP_PWR_LVL_P6);
  NimBLEDevice::setMTU(64);

  auto scan = NimBLEDevice::getScan();
  scan->setScanCallbacks(new ScanCallbacks());
  scan->setActiveScan(true);
  scan->setInterval(100);
  scan->setWindow(99);
  scan->start(0, false);
  logInfo("BLE scan started");

  tryConnectWithAttempts();
}

/* ================= LOOP ================= */
void loop() {
  nowMs = millis();

  feedWatchdog();
  updateLed();
  checkConnectionStatus();

  if (state == ST_REBOOT && nowMs >= rebootAtMs) {
    logInfo("Rebooting...");
    cleanupBleResources();
    NimBLEDevice::deinit(true);
    delay(100);
    ESP.restart();
  }

  // Если подключены и прошло 3 секунды – запрашиваем данные
  if (connectedClient && connectedClient->isConnected() && nowMs - lastRequestMs >= REQUEST_INTERVAL) {
    lastRequestMs = nowMs;
    auto s = connectedClient->getService("ff00");
    if (s) {
      auto c = s->getCharacteristic("ff02");
      if (c) {
        c->writeValue(CMD_BASIC, sizeof(CMD_BASIC), false);
        logDebug("Data request sent");
      }
    }
  }

  // Таймаут данных в состоянии CONNECTED
  if (state == ST_CONNECTED && (nowMs - lastDataMs > DATA_TIMEOUT) && !bms.isValid) {
    showCenteredMessage("NO DATA", false); // мелкий шрифт
    logError("Data timeout");
    delay(1500);
    cleanupBleResources();
    tryConnectWithAttempts();
  }

  delay(10);
}

З цим купують:

  • ESP32-C3 Super Mini — компактная плата разработки с USB-C для Arduino

    ESP32-C3 SuperMini — компактная плата разработки с USB-C для Arduino
    160 грн
    ДЕТАЛЬНІШЕ
  • Кабель DuPont мама-мама 20 см (2.54 мм) для Arduino та макетних плат

    Кабель DuPont мама-мама 20 см (2.54 мм) для Arduino та макетних плат
    2 грн
    ДЕТАЛЬНІШЕ
  • Кабель DuPont тато-тато 20 см (2.54 мм) для Arduino та макетних плат

    Кабель DuPont тато-тато 20 см (2.54 мм) для Arduino та макетних плат
    2 грн
    ДЕТАЛЬНІШЕ
  • Недорогий паяльник 60W з чорною пластиковою ручкою – надійний та доступний

    Недорогий електричний паяльник 60 Вт з чорною пластиковою ручкою — купити паяльник набір для пайки
    120 грн
    ДЕТАЛЬНІШЕ
  • Акумулятор 18650 з контактами для пайки

    Акумулятор 18650 з контактами для пайки
    120 грн
    ДЕТАЛЬНІШЕ

Коментарі до статті

Поки що немає коментарів. Будьте першим!

Додати коментар