mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
time sync
This commit is contained in:
@@ -10,6 +10,7 @@ idf_component_register(
|
||||
"src/i2c_global.cpp"
|
||||
"src/shutdowner.cpp"
|
||||
"src/spi_global.cpp"
|
||||
"src/time_sync_service.cpp"
|
||||
INCLUDE_DIRS
|
||||
"include"
|
||||
PRIV_REQUIRES
|
||||
@@ -19,6 +20,7 @@ idf_component_register(
|
||||
esp_driver_spi
|
||||
littlefs
|
||||
nvs_flash
|
||||
bt
|
||||
)
|
||||
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/utils" cardboy_utils_esp)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
/**
|
||||
* Ensure the BLE time synchronisation service is running.
|
||||
*
|
||||
* Safe to call multiple times; subsequent calls become no-ops once the
|
||||
* service has been started successfully.
|
||||
*/
|
||||
void ensure_time_sync_service_started();
|
||||
|
||||
/**
|
||||
* Stop the BLE time synchronisation service if it is running.
|
||||
* A no-op on platforms that do not support the BLE implementation.
|
||||
*/
|
||||
void shutdown_time_sync_service();
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/shutdowner.hpp"
|
||||
#include "cardboy/backend/esp/spi_global.hpp"
|
||||
#include "cardboy/backend/esp/time_sync_service.hpp"
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
@@ -150,7 +151,7 @@ EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
|
||||
Buttons::get().setEventBus(eventBus.get());
|
||||
}
|
||||
|
||||
EspRuntime::~EspRuntime() = default;
|
||||
EspRuntime::~EspRuntime() { shutdown_time_sync_service(); }
|
||||
|
||||
cardboy::sdk::Services& EspRuntime::serviceRegistry() { return services; }
|
||||
|
||||
@@ -180,6 +181,8 @@ void EspRuntime::initializeHardware() {
|
||||
Buzzer::get().init();
|
||||
|
||||
FsHelper::get().mount();
|
||||
|
||||
ensure_time_sync_service_started();
|
||||
}
|
||||
|
||||
void EspFramebuffer::clear_impl(bool on) {
|
||||
|
||||
304
Firmware/components/backend-esp/src/time_sync_service.cpp
Normal file
304
Firmware/components/backend-esp/src/time_sync_service.cpp
Normal file
@@ -0,0 +1,304 @@
|
||||
#include "cardboy/backend/esp/time_sync_service.hpp"
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include <sys/time.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "host/ble_gatt.h"
|
||||
#include "host/ble_hs.h"
|
||||
#include "host/util/util.h"
|
||||
#include "nimble/nimble_port.h"
|
||||
#include "nimble/nimble_port_freertos.h"
|
||||
#include "services/gap/ble_svc_gap.h"
|
||||
#include "services/gatt/ble_svc_gatt.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <esp_bt.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr char kLogTag[] = "TimeSyncBLE";
|
||||
constexpr char kDeviceName[] = "Cardboy";
|
||||
|
||||
// 128-bit UUIDs (little-endian order for NimBLE macros)
|
||||
static const ble_uuid128_t kTimeServiceUuid = BLE_UUID128_INIT(0x30, 0xF2, 0xD3, 0xF4, 0xC3, 0x10, 0xA6, 0xB5, 0xFD,
|
||||
0x4E, 0x7B, 0xCA, 0x01, 0x00, 0x00, 0x00);
|
||||
static const ble_uuid128_t kTimeWriteCharUuid = BLE_UUID128_INIT(0x31, 0xF2, 0xD3, 0xF4, 0xC3, 0x10, 0xA6, 0xB5, 0xFD,
|
||||
0x4E, 0x7B, 0xCA, 0x02, 0x00, 0x00, 0x00);
|
||||
|
||||
struct [[gnu::packed]] TimeSyncPayload {
|
||||
std::uint64_t epochSeconds; // Unix time in seconds (UTC)
|
||||
std::int16_t timezoneOffsetMinutes; // Minutes east of UTC
|
||||
std::uint8_t daylightSavingActive; // Non-zero if DST active at source
|
||||
std::uint8_t reserved; // Reserved for alignment / future use
|
||||
};
|
||||
|
||||
static_assert(sizeof(TimeSyncPayload) == 12, "Unexpected payload size");
|
||||
|
||||
static bool g_started = false;
|
||||
static uint8_t g_ownAddrType = BLE_OWN_ADDR_PUBLIC;
|
||||
static TaskHandle_t g_hostTaskHandle = nullptr;
|
||||
|
||||
int gapEventHandler(struct ble_gap_event* event, void* arg);
|
||||
void startAdvertising();
|
||||
|
||||
void setSystemTimeFromPayload(const TimeSyncPayload& payload) {
|
||||
timeval tv{};
|
||||
tv.tv_sec = static_cast<time_t>(payload.epochSeconds);
|
||||
tv.tv_usec = 0;
|
||||
|
||||
if (settimeofday(&tv, nullptr) != 0) {
|
||||
ESP_LOGW(kLogTag, "Failed to set system time (errno=%d)", errno);
|
||||
} else {
|
||||
ESP_LOGI(kLogTag, "Wall time updated: epoch=%llu dst=%u offset=%dmin",
|
||||
static_cast<unsigned long long>(payload.epochSeconds), payload.daylightSavingActive,
|
||||
static_cast<int>(payload.timezoneOffsetMinutes));
|
||||
}
|
||||
|
||||
// Apply timezone offset by updating TZ environment variable.
|
||||
// POSIX TZ strings invert the sign relative to the offset from UTC.
|
||||
const int offsetMin = static_cast<int>(payload.timezoneOffsetMinutes);
|
||||
const int absOffset = std::abs(offsetMin);
|
||||
const int hours = absOffset / 60;
|
||||
const int minutes = absOffset % 60;
|
||||
|
||||
char tzString[16];
|
||||
const char signChar = (offsetMin >= 0) ? '-' : '+';
|
||||
if (minutes == 0) {
|
||||
std::snprintf(tzString, sizeof(tzString), "GMT%c%d", signChar, hours);
|
||||
} else {
|
||||
std::snprintf(tzString, sizeof(tzString), "GMT%c%d:%02d", signChar, hours, minutes);
|
||||
}
|
||||
|
||||
setenv("TZ", tzString, 1);
|
||||
tzset();
|
||||
ESP_LOGI(kLogTag, "Timezone updated to %s", tzString);
|
||||
}
|
||||
|
||||
int timeSyncWriteAccess(uint16_t /*conn_handle*/, uint16_t /*attr_handle*/, ble_gatt_access_ctxt* ctxt, void* /*arg*/) {
|
||||
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
||||
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
||||
}
|
||||
|
||||
const std::uint16_t incomingLen = OS_MBUF_PKTLEN(ctxt->om);
|
||||
if (incomingLen != sizeof(TimeSyncPayload)) {
|
||||
ESP_LOGW(kLogTag, "Invalid payload length: %u", incomingLen);
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
TimeSyncPayload payload{};
|
||||
const int rc = os_mbuf_copydata(ctxt->om, 0, sizeof(TimeSyncPayload), &payload);
|
||||
if (rc != 0) {
|
||||
ESP_LOGW(kLogTag, "Failed to read payload (rc=%d)", rc);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
setSystemTimeFromPayload(payload);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ble_gatt_svc_def kGattServices[] = {
|
||||
{
|
||||
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
||||
.uuid = &kTimeServiceUuid.u,
|
||||
.characteristics =
|
||||
(ble_gatt_chr_def[]) {
|
||||
{
|
||||
.uuid = &kTimeWriteCharUuid.u,
|
||||
.access_cb = timeSyncWriteAccess,
|
||||
.flags = static_cast<uint8_t>(BLE_GATT_CHR_F_WRITE |
|
||||
BLE_GATT_CHR_F_WRITE_NO_RSP),
|
||||
},
|
||||
{
|
||||
0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
0,
|
||||
},
|
||||
};
|
||||
|
||||
void startAdvertising() {
|
||||
ble_hs_adv_fields fields{};
|
||||
std::memset(&fields, 0, sizeof(fields));
|
||||
|
||||
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
|
||||
|
||||
const char* name = ble_svc_gap_device_name();
|
||||
fields.name = reinterpret_cast<const std::uint8_t*>(name);
|
||||
fields.name_len = static_cast<std::uint8_t>(std::strlen(name));
|
||||
fields.name_is_complete = 1;
|
||||
fields.uuids128 = const_cast<ble_uuid128_t*>(&kTimeServiceUuid);
|
||||
fields.num_uuids128 = 1;
|
||||
fields.uuids128_is_complete = 1;
|
||||
|
||||
int rc = ble_gap_adv_set_fields(&fields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(kLogTag, "ble_gap_adv_set_fields failed: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_hs_adv_fields rspFields{};
|
||||
std::memset(&rspFields, 0, sizeof(rspFields));
|
||||
rspFields.tx_pwr_lvl_is_present = 1;
|
||||
rspFields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
|
||||
rc = ble_gap_adv_rsp_set_fields(&rspFields);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(kLogTag, "ble_gap_adv_rsp_set_fields failed: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
ble_gap_adv_params advParams{};
|
||||
std::memset(&advParams, 0, sizeof(advParams));
|
||||
advParams.conn_mode = BLE_GAP_CONN_MODE_UND;
|
||||
advParams.disc_mode = BLE_GAP_DISC_MODE_GEN;
|
||||
|
||||
rc = ble_gap_adv_start(g_ownAddrType, nullptr, BLE_HS_FOREVER, &advParams, gapEventHandler, nullptr);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(kLogTag, "ble_gap_adv_start failed: %d", rc);
|
||||
} else {
|
||||
ESP_LOGI(kLogTag, "Advertising started");
|
||||
}
|
||||
}
|
||||
|
||||
void onReset(int reason) { ESP_LOGW(kLogTag, "Resetting state; reason=%d", reason); }
|
||||
|
||||
void onSync() {
|
||||
int rc = ble_hs_id_infer_auto(0, &g_ownAddrType);
|
||||
if (rc != 0) {
|
||||
ESP_LOGE(kLogTag, "ble_hs_id_infer_auto failed: %d", rc);
|
||||
return;
|
||||
}
|
||||
|
||||
std::uint8_t addrVal[6];
|
||||
rc = ble_hs_id_copy_addr(g_ownAddrType, addrVal, nullptr);
|
||||
if (rc == 0) {
|
||||
ESP_LOGI(kLogTag, "Device address: %02X:%02X:%02X:%02X:%02X:%02X", addrVal[5], addrVal[4], addrVal[3],
|
||||
addrVal[2], addrVal[1], addrVal[0]);
|
||||
}
|
||||
|
||||
startAdvertising();
|
||||
}
|
||||
|
||||
int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) {
|
||||
switch (event->type) {
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
if (event->connect.status == 0) {
|
||||
ESP_LOGI(kLogTag, "Connected; handle=%d", event->connect.conn_handle);
|
||||
} else {
|
||||
ESP_LOGW(kLogTag, "Connection attempt failed; status=%d", event->connect.status);
|
||||
startAdvertising();
|
||||
}
|
||||
break;
|
||||
|
||||
case BLE_GAP_EVENT_DISCONNECT:
|
||||
ESP_LOGI(kLogTag, "Disconnected; reason=%d", event->disconnect.reason);
|
||||
startAdvertising();
|
||||
break;
|
||||
|
||||
case BLE_GAP_EVENT_ADV_COMPLETE:
|
||||
ESP_LOGI(kLogTag, "Advertising complete; restarting");
|
||||
startAdvertising();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void hostTask(void* /*param*/) {
|
||||
g_hostTaskHandle = xTaskGetCurrentTaskHandle();
|
||||
nimble_port_run(); // This call blocks until NimBLE stops
|
||||
nimble_port_freertos_deinit();
|
||||
g_hostTaskHandle = nullptr;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
void configureGap() {
|
||||
ble_svc_gap_init();
|
||||
ble_svc_gap_device_name_set(kDeviceName);
|
||||
ble_svc_gatt_init();
|
||||
}
|
||||
|
||||
bool initController() {
|
||||
ble_hs_cfg.reset_cb = onReset;
|
||||
ble_hs_cfg.sync_cb = onSync;
|
||||
ble_hs_cfg.gatts_register_cb = nullptr;
|
||||
ble_hs_cfg.store_status_cb = ble_store_util_status_rr;
|
||||
ble_hs_cfg.sm_io_cap = BLE_HS_IO_NO_INPUT_OUTPUT;
|
||||
ble_hs_cfg.sm_bonding = 0;
|
||||
ble_hs_cfg.sm_mitm = 0;
|
||||
ble_hs_cfg.sm_sc = 0;
|
||||
ESP_ERROR_CHECK(nimble_port_init());
|
||||
|
||||
configureGap();
|
||||
|
||||
int gattRc = ble_gatts_count_cfg(kGattServices);
|
||||
if (gattRc != 0) {
|
||||
ESP_LOGE(kLogTag, "ble_gatts_count_cfg failed (rc=%d)", gattRc);
|
||||
return false;
|
||||
}
|
||||
|
||||
gattRc = ble_gatts_add_svcs(kGattServices);
|
||||
if (gattRc != 0) {
|
||||
ESP_LOGE(kLogTag, "ble_gatts_add_svcs failed (rc=%d)", gattRc);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ensure_time_sync_service_started() {
|
||||
if (g_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!initController()) {
|
||||
ESP_LOGE(kLogTag, "Unable to initialise BLE time sync service");
|
||||
return;
|
||||
}
|
||||
|
||||
nimble_port_freertos_init(hostTask);
|
||||
g_started = true;
|
||||
ESP_LOGI(kLogTag, "BLE time sync service initialised");
|
||||
}
|
||||
|
||||
void shutdown_time_sync_service() {
|
||||
if (!g_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
int rc = nimble_port_stop();
|
||||
if (rc == 0) {
|
||||
// Wait for host task to exit
|
||||
while (g_hostTaskHandle != nullptr) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
|
||||
nimble_port_deinit();
|
||||
// esp_nimble_hci_and_controller_deinit();
|
||||
esp_bt_controller_disable();
|
||||
esp_bt_controller_deinit();
|
||||
|
||||
g_started = false;
|
||||
ESP_LOGI(kLogTag, "BLE time sync service stopped");
|
||||
}
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
Reference in New Issue
Block a user