#include #include #include #include namespace { const NimBLEUUID CTS_SERVICE_UUID((uint16_t)0x1805); const NimBLEUUID CTS_CHARACTERISTIC_UUID((uint16_t)0x2A2B); constexpr uint32_t CTS_READ_INTERVAL_MS = 10000; // 10 seconds const NimBLEAdvertisedDevice* g_targetDevice = nullptr; NimBLEClient* g_client = nullptr; NimBLERemoteCharacteristic* g_ctsChar = nullptr; bool g_doConnect = false; uint32_t g_lastReadMillis = 0; constexpr uint8_t OLED_WIDTH = 128; constexpr uint8_t OLED_HEIGHT = 64; constexpr uint8_t OLED_RESET_PIN = -1; constexpr uint8_t OLED_I2C_ADDR = 0x3C; constexpr uint8_t OLED_LINE_HEIGHT = 10; constexpr uint8_t OLED_TOP_MARGIN = 12; Adafruit_SSD1306 g_display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET_PIN); struct CurrentTime { uint16_t year; uint8_t month; uint8_t day; uint8_t hours; uint8_t minutes; uint8_t seconds; uint8_t dayOfWeek; uint8_t fractions256; uint8_t adjustReason; }; void printCurrentTime(const CurrentTime& time) { static const char* kDays[] = {"Unknown", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; const char* dow = (time.dayOfWeek >= 1 && time.dayOfWeek <= 7) ? kDays[time.dayOfWeek] : kDays[0]; Serial.printf("[CTS] %04u-%02u-%02u %02u:%02u:%02u (%s) adj=0x%02X frac=%u\n", time.year, time.month, time.day, time.hours, time.minutes, time.seconds, dow, time.adjustReason, time.fractions256); } bool parseCurrentTime(const uint8_t* buffer, size_t length, CurrentTime& out) { if (length < 10) { Serial.printf("[CTS] Expected 10 bytes, got %u\n", static_cast(length)); return false; } out.year = static_cast(buffer[0] | (buffer[1] << 8)); out.month = buffer[2]; out.day = buffer[3]; out.hours = buffer[4]; out.minutes = buffer[5]; out.seconds = buffer[6]; out.dayOfWeek = buffer[7]; out.fractions256 = buffer[8]; out.adjustReason = buffer[9]; return true; } void displayStatusMessage(const char* message) { g_display.clearDisplay(); g_display.setCursor(0, OLED_TOP_MARGIN); g_display.println(message); g_display.display(); } void displayCurrentTime(const CurrentTime& time) { g_display.clearDisplay(); g_display.setCursor(0, OLED_TOP_MARGIN); char line[24]; snprintf(line, sizeof(line), "%04u-%02u-%02u", time.year, time.month, time.day); g_display.println(line); snprintf(line, sizeof(line), "%02u:%02u:%02u", time.hours, time.minutes, time.seconds); g_display.println(line); static const char* kDays[] = {"", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; const char* dow = (time.dayOfWeek >= 1 && time.dayOfWeek <= 7) ? kDays[time.dayOfWeek] : ""; g_display.println(dow); g_display.display(); } class ClientCallbacks : public NimBLEClientCallbacks { void onConnect(NimBLEClient* client) override { Serial.printf("[BLE] Connected to %s\n", client->getPeerAddress().toString().c_str()); displayStatusMessage("Connected"); } void onDisconnect(NimBLEClient* client, int reason) override { Serial.printf("[BLE] Disconnected (reason %d), restarting scan\n", reason); g_ctsChar = nullptr; g_lastReadMillis = 0; NimBLEDevice::deleteClient(client); g_client = nullptr; displayStatusMessage("Disconnected"); NimBLEDevice::getScan()->start(0, false, true); } }; class ScanCallbacks : public NimBLEScanCallbacks { void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override { if (!advertisedDevice->isAdvertisingService(CTS_SERVICE_UUID)) { return; } Serial.printf("[BLE] Found CTS peripheral: %s\n", advertisedDevice->toString().c_str()); NimBLEDevice::getScan()->stop(); g_targetDevice = advertisedDevice; g_doConnect = true; } void onScanEnd(const NimBLEScanResults& results, int reason) override { Serial.printf("[BLE] Scan ended (reason %d, %d devices)\n", reason, results.getCount()); if (!g_doConnect && g_ctsChar == nullptr) { NimBLEDevice::getScan()->start(0, false, true); } } }; ClientCallbacks g_clientCallbacks; ScanCallbacks g_scanCallbacks; void startScan() { NimBLEScan* scan = NimBLEDevice::getScan(); scan->setScanCallbacks(&g_scanCallbacks, false); scan->setActiveScan(true); scan->setInterval(160); scan->setWindow(120); scan->start(0, false, true); Serial.println("[BLE] Scanning for Current Time Service peripherals..."); displayStatusMessage("Scanning..."); } bool connectToCurrentTimeService() { if (!g_doConnect || g_targetDevice == nullptr) { return false; } g_doConnect = false; if (g_client == nullptr) { g_client = NimBLEDevice::createClient(); g_client->setClientCallbacks(&g_clientCallbacks, false); g_client->setConnectionParams(12, 12, 0, 200); g_client->setConnectTimeout(5000); } Serial.println("[BLE] Connecting to peripheral..."); if (!g_client->connect(g_targetDevice)) { Serial.println("[BLE] Connection failed, restarting scan"); NimBLEDevice::deleteClient(g_client); g_client = nullptr; g_ctsChar = nullptr; g_targetDevice = nullptr; NimBLEDevice::getScan()->start(0, false, true); return false; } NimBLERemoteService* service = g_client->getService(CTS_SERVICE_UUID); if (service == nullptr) { Serial.println("[BLE] Current Time Service not found on peripheral"); g_client->disconnect(); NimBLEDevice::deleteClient(g_client); g_client = nullptr; g_ctsChar = nullptr; g_targetDevice = nullptr; NimBLEDevice::getScan()->start(0, false, true); return false; } NimBLERemoteCharacteristic* characteristic = service->getCharacteristic(CTS_CHARACTERISTIC_UUID); if (characteristic == nullptr) { Serial.println("[BLE] Current Time characteristic missing"); g_client->disconnect(); NimBLEDevice::deleteClient(g_client); g_client = nullptr; g_ctsChar = nullptr; g_targetDevice = nullptr; NimBLEDevice::getScan()->start(0, false, true); return false; } if (!characteristic->canRead()) { Serial.println("[BLE] Current Time characteristic is not readable"); g_client->disconnect(); NimBLEDevice::deleteClient(g_client); g_client = nullptr; g_ctsChar = nullptr; g_targetDevice = nullptr; NimBLEDevice::getScan()->start(0, false, true); return false; } g_ctsChar = characteristic; g_lastReadMillis = 0; g_targetDevice = nullptr; Serial.println("[BLE] Ready to read current time"); displayStatusMessage("CTS ready"); return true; } void readAndPrintCurrentTime() { if (g_ctsChar == nullptr || g_client == nullptr) { return; } if (!g_client->isConnected()) { return; } const uint32_t now = millis(); if (now - g_lastReadMillis < CTS_READ_INTERVAL_MS) { return; } g_lastReadMillis = now; Serial.println("[CTS] Reading current time..."); NimBLEAttValue value = g_ctsChar->readValue(); if (value.length() == 0) { Serial.println("[CTS] Read returned empty value"); return; } CurrentTime time{}; if (!parseCurrentTime(value.data(), value.length(), time)) { return; } printCurrentTime(time); displayCurrentTime(time); } } // namespace void setup() { Serial.begin(115200); while (!Serial) { delay(10); } Serial.println(); Serial.println("[BOOT] BLE Current Time Service client"); NimBLEDevice::init("ESP32C6-CTS-Client"); NimBLEDevice::setPower(ESP_PWR_LVL_P9); NimBLEDevice::setSecurityAuth(false, false, true); Wire.begin(21, 22); if (!g_display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) { Serial.println("[OLED] SSD1306 allocation failed"); while (true) { delay(1000); } } g_display.clearDisplay(); g_display.setTextColor(SSD1306_WHITE); g_display.setTextSize(2); g_display.setCursor(0, OLED_TOP_MARGIN); g_display.println("BLE CTS Client"); g_display.display(); startScan(); } void loop() { connectToCurrentTimeService(); readAndPrintCurrentTime(); delay(100); }