mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
1628 lines
59 KiB
C++
1628 lines
59 KiB
C++
#include "cardboy/backend/esp/time_sync_service.hpp"
|
|
|
|
#include "cardboy/backend/esp/fs_helper.hpp"
|
|
#include "sdkconfig.h"
|
|
|
|
#include <sys/time.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/queue.h"
|
|
#include "freertos/task.h"
|
|
#include "host/ble_att.h"
|
|
#include "host/ble_gap.h"
|
|
#include "host/ble_gatt.h"
|
|
#include "host/ble_hs.h"
|
|
#include "host/ble_hs_mbuf.h"
|
|
#include "host/ble_store.h"
|
|
#include "host/util/util.h"
|
|
#include "nimble/nimble_port.h"
|
|
#include "nimble/nimble_port_freertos.h"
|
|
#include "os/os_mbuf.h"
|
|
#include "services/gap/ble_svc_gap.h"
|
|
#include "services/gatt/ble_svc_gatt.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <dirent.h>
|
|
#include <esp_bt.h>
|
|
#include <esp_err.h>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <sys/stat.h>
|
|
#include <vector>
|
|
|
|
#include "cardboy/sdk/services.hpp"
|
|
|
|
extern "C" void ble_store_config_init(void);
|
|
|
|
namespace cardboy::backend::esp {
|
|
|
|
namespace {
|
|
|
|
constexpr char kLogTag[] = "TimeSyncBLE";
|
|
constexpr char kDeviceName[] = "Cardboy";
|
|
|
|
constexpr std::uint16_t kPreferredConnIntervalMin = BLE_GAP_CONN_ITVL_MS(200); // 80 ms
|
|
constexpr std::uint16_t kPreferredConnIntervalMax = BLE_GAP_CONN_ITVL_MS(300); // 150 ms
|
|
constexpr std::uint16_t kPreferredConnLatency = 3;
|
|
constexpr std::uint16_t kPreferredSupervisionTimeout = BLE_GAP_SUPERVISION_TIMEOUT_MS(5000); // 5 s
|
|
|
|
constexpr float connIntervalUnitsToMs(std::uint16_t units) { return static_cast<float>(units) * 1.25f; }
|
|
|
|
constexpr float supervisionUnitsToMs(std::uint16_t units) { return static_cast<float>(units) * 10.0f; }
|
|
|
|
// 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);
|
|
|
|
static const ble_uuid128_t kFileServiceUuid = BLE_UUID128_INIT(0x30, 0xF2, 0xD3, 0xF4, 0xC3, 0x10, 0xA6, 0xB5, 0xFD,
|
|
0x4E, 0x7B, 0xCA, 0x10, 0x00, 0x00, 0x00);
|
|
static const ble_uuid128_t kFileCommandCharUuid = BLE_UUID128_INIT(0x31, 0xF2, 0xD3, 0xF4, 0xC3, 0x10, 0xA6, 0xB5, 0xFD,
|
|
0x4E, 0x7B, 0xCA, 0x11, 0x00, 0x00, 0x00);
|
|
static const ble_uuid128_t kFileResponseCharUuid = BLE_UUID128_INIT(0x32, 0xF2, 0xD3, 0xF4, 0xC3, 0x10, 0xA6, 0xB5,
|
|
0xFD, 0x4E, 0x7B, 0xCA, 0x12, 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;
|
|
static uint16_t g_activeConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
|
static cardboy::sdk::INotificationCenter* g_notificationCenter = nullptr;
|
|
static bool g_securityRequested = false;
|
|
|
|
struct ResponseMessage {
|
|
uint8_t opcode;
|
|
uint8_t status;
|
|
uint16_t length;
|
|
uint8_t* data;
|
|
bool streamDownload;
|
|
};
|
|
|
|
static QueueHandle_t g_responseQueue = nullptr;
|
|
static TaskHandle_t g_notifyTaskHandle = nullptr;
|
|
constexpr uint8_t kResponseOpcodeShutdown = 0xFF;
|
|
|
|
static uint16_t g_fileCommandValueHandle = 0;
|
|
static uint16_t g_fileResponseValueHandle = 0;
|
|
|
|
struct FileUploadContext {
|
|
FILE* file = nullptr;
|
|
std::string path;
|
|
std::size_t remaining = 0;
|
|
bool active = false;
|
|
};
|
|
|
|
struct FileDownloadContext {
|
|
FILE* file = nullptr;
|
|
std::size_t remaining = 0;
|
|
bool active = false;
|
|
bool chunkScheduled = false;
|
|
};
|
|
|
|
static FileUploadContext g_uploadCtx{};
|
|
static FileDownloadContext g_downloadCtx{};
|
|
|
|
static const ble_uuid128_t kAncsServiceUuid = BLE_UUID128_INIT(0xD0, 0x00, 0x2D, 0x12, 0x1E, 0x4B, 0x0F, 0xA4, 0x99,
|
|
0x4E, 0xCE, 0xB5, 0x31, 0xF4, 0x05, 0x79);
|
|
static const ble_uuid128_t kAncsNotificationSourceUuid = BLE_UUID128_INIT(
|
|
0xBD, 0x1D, 0xA2, 0x99, 0xE6, 0x25, 0x58, 0x8C, 0xD9, 0x42, 0x01, 0x63, 0x0D, 0x12, 0xBF, 0x9F);
|
|
static const ble_uuid128_t kAncsDataSourceUuid = BLE_UUID128_INIT(0xFB, 0x7B, 0x7C, 0xCE, 0x6A, 0xB3, 0x44, 0xBE, 0xB5,
|
|
0x4B, 0xD6, 0x24, 0xE9, 0xC6, 0xEA, 0x22);
|
|
static const ble_uuid128_t kAncsControlPointUuid = BLE_UUID128_INIT(0xD9, 0xD9, 0xAA, 0xFD, 0xBD, 0x9B, 0x21, 0x98,
|
|
0xA8, 0x49, 0xE1, 0x45, 0xF3, 0xD8, 0xD1, 0x69);
|
|
|
|
static uint16_t g_ancsServiceEndHandle = 0;
|
|
static uint16_t g_ancsNotificationSourceHandle = 0;
|
|
static uint16_t g_ancsDataSourceHandle = 0;
|
|
static uint16_t g_ancsControlPointHandle = 0;
|
|
static uint16_t g_mtuSize = 23;
|
|
|
|
struct PendingNotification {
|
|
uint32_t uid = 0;
|
|
uint8_t category = 0;
|
|
uint8_t flags = 0;
|
|
std::string appIdentifier;
|
|
std::string title;
|
|
std::string message;
|
|
};
|
|
|
|
static std::vector<PendingNotification> g_pendingNotifications;
|
|
static std::vector<uint8_t> g_dataSourceBuffer;
|
|
static const ble_uuid16_t kClientConfigUuid = BLE_UUID16_INIT(BLE_GATT_DSC_CLT_CFG_UUID16);
|
|
|
|
void resetAncsState() {
|
|
g_ancsServiceEndHandle = 0;
|
|
g_ancsNotificationSourceHandle = 0;
|
|
g_ancsDataSourceHandle = 0;
|
|
g_ancsControlPointHandle = 0;
|
|
g_mtuSize = 23;
|
|
g_dataSourceBuffer.clear();
|
|
g_pendingNotifications.clear();
|
|
}
|
|
|
|
void clearDeliveredNotifications() {
|
|
if (g_notificationCenter)
|
|
g_notificationCenter->clear();
|
|
}
|
|
|
|
PendingNotification* findPending(uint32_t uid) {
|
|
for (auto& entry: g_pendingNotifications) {
|
|
if (entry.uid == uid)
|
|
return &entry;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
PendingNotification& ensurePending(uint32_t uid) {
|
|
if (auto* existing = findPending(uid))
|
|
return *existing;
|
|
g_pendingNotifications.push_back({});
|
|
auto& pending = g_pendingNotifications.back();
|
|
pending.uid = uid;
|
|
return pending;
|
|
}
|
|
|
|
void discardPending(uint32_t uid) {
|
|
for (auto it = g_pendingNotifications.begin(); it != g_pendingNotifications.end(); ++it) {
|
|
if (it->uid == uid) {
|
|
g_pendingNotifications.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void finalizePending(uint32_t uid) {
|
|
if (!g_notificationCenter)
|
|
return;
|
|
for (auto it = g_pendingNotifications.begin(); it != g_pendingNotifications.end(); ++it) {
|
|
if (it->uid != uid)
|
|
continue;
|
|
|
|
cardboy::sdk::INotificationCenter::Notification note{};
|
|
note.timestamp = static_cast<std::uint64_t>(time(nullptr));
|
|
note.externalId = uid;
|
|
if (!it->title.empty()) {
|
|
note.title = it->title;
|
|
} else if (!it->appIdentifier.empty()) {
|
|
note.title = it->appIdentifier;
|
|
} else {
|
|
note.title = "Notification";
|
|
}
|
|
note.body = it->message;
|
|
g_notificationCenter->pushNotification(std::move(note));
|
|
ESP_LOGI(kLogTag, "Stored notification uid=%" PRIu32 " title='%s' body='%s'", uid, it->title.c_str(),
|
|
it->message.c_str());
|
|
g_pendingNotifications.erase(it);
|
|
break;
|
|
}
|
|
}
|
|
|
|
enum class FileCommandCode : uint8_t {
|
|
ListDirectory = 0x01,
|
|
UploadBegin = 0x02,
|
|
UploadChunk = 0x03,
|
|
UploadEnd = 0x04,
|
|
DownloadRequest = 0x05,
|
|
DeleteFile = 0x06,
|
|
CreateDirectory = 0x07,
|
|
DeleteDirectory = 0x08,
|
|
RenamePath = 0x09,
|
|
};
|
|
|
|
constexpr uint8_t kResponseFlagComplete = 0x80;
|
|
|
|
struct PacketHeader {
|
|
uint8_t opcode;
|
|
uint8_t status;
|
|
uint16_t length;
|
|
} __attribute__((packed));
|
|
|
|
int gapEventHandler(struct ble_gap_event* event, void* arg);
|
|
void startAdvertising();
|
|
int timeSyncWriteAccess(uint16_t connHandle, uint16_t attrHandle, ble_gatt_access_ctxt* ctxt, void* arg);
|
|
int fileCommandAccess(uint16_t connHandle, uint16_t attrHandle, ble_gatt_access_ctxt* ctxt, void* arg);
|
|
int fileResponseAccess(uint16_t connHandle, uint16_t attrHandle, ble_gatt_access_ctxt* ctxt, void* arg);
|
|
void handleGattsRegister(ble_gatt_register_ctxt* ctxt, void* arg);
|
|
bool sendFileResponse(uint8_t opcode, uint8_t status, const uint8_t* data, std::size_t length);
|
|
bool sendFileError(uint8_t opcode, int err, const char* message = nullptr);
|
|
bool sanitizePath(std::string_view input, std::string& absoluteOut);
|
|
void resetUploadContext();
|
|
void resetDownloadContext();
|
|
void handleListDirectory(const uint8_t* payload, std::size_t length);
|
|
void handleUploadBegin(const uint8_t* payload, std::size_t length);
|
|
void handleUploadChunk(const uint8_t* payload, std::size_t length);
|
|
void handleUploadEnd();
|
|
void handleDownloadRequest(const uint8_t* payload, std::size_t length);
|
|
void handleDeletePath(const uint8_t* payload, std::size_t length, bool directory);
|
|
void handleCreateDirectory(const uint8_t* payload, std::size_t length);
|
|
void handleRename(const uint8_t* payload, std::size_t length);
|
|
bool enqueueFileResponse(uint8_t opcode, uint8_t status, const uint8_t* data, std::size_t length);
|
|
bool sendFileResponseNow(const ResponseMessage& msg);
|
|
void notificationTask(void* param);
|
|
bool scheduleDownloadChunk();
|
|
void processDownloadChunk();
|
|
void handleAncsNotificationSource(uint16_t connHandle, const uint8_t* data, uint16_t length);
|
|
bool handleAncsDataSource(const uint8_t* data, uint16_t length);
|
|
void requestAncsAttributes(uint16_t connHandle, uint32_t uid);
|
|
void applyPreferredConnectionParams(uint16_t connHandle);
|
|
int ancsServiceDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_svc* svc, void* arg);
|
|
int ancsCharacteristicDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_chr* chr,
|
|
void* arg);
|
|
int ancsDescriptorDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, uint16_t chrValHandle,
|
|
const ble_gatt_dsc* dsc, void* arg);
|
|
|
|
static const ble_gatt_chr_def kTimeServiceCharacteristics[] = {
|
|
{
|
|
.uuid = &kTimeWriteCharUuid.u,
|
|
.access_cb = timeSyncWriteAccess,
|
|
.arg = nullptr,
|
|
.descriptors = nullptr,
|
|
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
|
|
.min_key_size = 0,
|
|
.val_handle = nullptr,
|
|
.cpfd = nullptr,
|
|
},
|
|
{
|
|
0,
|
|
},
|
|
};
|
|
|
|
static const ble_gatt_chr_def kFileServiceCharacteristics[] = {
|
|
{
|
|
.uuid = &kFileCommandCharUuid.u,
|
|
.access_cb = fileCommandAccess,
|
|
.arg = nullptr,
|
|
.descriptors = nullptr,
|
|
.flags = BLE_GATT_CHR_F_WRITE | BLE_GATT_CHR_F_WRITE_NO_RSP,
|
|
.min_key_size = 0,
|
|
.val_handle = &g_fileCommandValueHandle,
|
|
.cpfd = nullptr,
|
|
},
|
|
{
|
|
.uuid = &kFileResponseCharUuid.u,
|
|
.access_cb = fileResponseAccess,
|
|
.arg = nullptr,
|
|
.descriptors = nullptr,
|
|
.flags = BLE_GATT_CHR_F_NOTIFY,
|
|
.min_key_size = 0,
|
|
.val_handle = &g_fileResponseValueHandle,
|
|
.cpfd = nullptr,
|
|
},
|
|
{
|
|
0,
|
|
},
|
|
};
|
|
|
|
static const ble_gatt_svc_def kGattServices[] = {
|
|
{
|
|
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
|
.uuid = &kTimeServiceUuid.u,
|
|
.includes = nullptr,
|
|
.characteristics = kTimeServiceCharacteristics,
|
|
},
|
|
{
|
|
.type = BLE_GATT_SVC_TYPE_PRIMARY,
|
|
.uuid = &kFileServiceUuid.u,
|
|
.includes = nullptr,
|
|
.characteristics = kFileServiceCharacteristics,
|
|
},
|
|
{
|
|
0,
|
|
},
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
bool sanitizePath(std::string_view input, std::string& absoluteOut) {
|
|
std::string path(input);
|
|
if (path.empty())
|
|
path = "/";
|
|
|
|
if (path.front() != '/')
|
|
path.insert(path.begin(), '/');
|
|
|
|
// Collapse multiple slashes
|
|
std::string cleaned;
|
|
cleaned.reserve(path.size());
|
|
char prev = '\0';
|
|
for (char ch: path) {
|
|
if (ch == '/' && prev == '/')
|
|
continue;
|
|
cleaned.push_back(ch);
|
|
prev = ch;
|
|
}
|
|
|
|
if (cleaned.size() > 1 && cleaned.back() == '/')
|
|
cleaned.pop_back();
|
|
|
|
if (cleaned.find("..") != std::string::npos)
|
|
return false;
|
|
|
|
absoluteOut.assign(FsHelper::get().basePath());
|
|
absoluteOut.append(cleaned);
|
|
return true;
|
|
}
|
|
|
|
bool enqueueFileResponse(uint8_t opcode, uint8_t status, const uint8_t* data, std::size_t length) {
|
|
if (!g_responseQueue || g_fileResponseValueHandle == 0 || g_activeConnHandle == BLE_HS_CONN_HANDLE_NONE)
|
|
return false;
|
|
|
|
ResponseMessage msg{};
|
|
msg.opcode = opcode;
|
|
msg.status = status;
|
|
msg.length = static_cast<uint16_t>(std::min<std::size_t>(length, 0xFFFF));
|
|
if (msg.length > 0 && data != nullptr) {
|
|
msg.data = static_cast<uint8_t*>(pvPortMalloc(msg.length));
|
|
if (!msg.data) {
|
|
ESP_LOGW(kLogTag, "Failed to allocate buffer for queued response (len=%u)", msg.length);
|
|
return false;
|
|
}
|
|
std::memcpy(msg.data, data, msg.length);
|
|
} else {
|
|
msg.data = nullptr;
|
|
}
|
|
|
|
if (xQueueSend(g_responseQueue, &msg, pdMS_TO_TICKS(20)) != pdPASS) {
|
|
ESP_LOGW(kLogTag, "Response queue full; dropping packet opcode=0x%02x", opcode);
|
|
if (msg.data)
|
|
vPortFree(msg.data);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool sendFileResponseNow(const ResponseMessage& msg) {
|
|
if (g_fileResponseValueHandle == 0 || g_activeConnHandle == BLE_HS_CONN_HANDLE_NONE)
|
|
return false;
|
|
|
|
const std::size_t totalLength = sizeof(PacketHeader) + msg.length;
|
|
if (totalLength > 0xFFFF + sizeof(PacketHeader)) {
|
|
ESP_LOGW(kLogTag, "File response payload too large (%zu bytes)", totalLength);
|
|
return false;
|
|
}
|
|
|
|
PacketHeader header{.opcode = msg.opcode, .status = msg.status, .length = msg.length};
|
|
|
|
constexpr int kMaxAttempts = 20;
|
|
for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
|
|
os_mbuf* om = os_msys_get_pkthdr(totalLength, 0);
|
|
if (om == nullptr) {
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
continue;
|
|
}
|
|
|
|
if (os_mbuf_append(om, &header, sizeof(header)) != 0 ||
|
|
(msg.length > 0 && msg.data != nullptr && os_mbuf_append(om, msg.data, msg.length) != 0)) {
|
|
ESP_LOGW(kLogTag, "Failed to populate mbuf for file response");
|
|
os_mbuf_free_chain(om);
|
|
return false;
|
|
}
|
|
|
|
int rc = ble_gatts_notify_custom(g_activeConnHandle, g_fileResponseValueHandle, om);
|
|
if (rc == 0) {
|
|
return true;
|
|
}
|
|
|
|
os_mbuf_free_chain(om);
|
|
|
|
if (rc != BLE_HS_ENOMEM && rc != BLE_HS_EBUSY) {
|
|
ESP_LOGW(kLogTag, "ble_gatts_notify_custom failed: %d", rc);
|
|
return false;
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
}
|
|
|
|
ESP_LOGW(kLogTag, "Failed to send file response opcode=0x%02x after %d attempts", msg.opcode, kMaxAttempts);
|
|
return false;
|
|
}
|
|
|
|
bool sendFileResponse(uint8_t opcode, uint8_t status, const uint8_t* data, std::size_t length) {
|
|
return enqueueFileResponse(opcode, status, data, length);
|
|
}
|
|
|
|
bool scheduleDownloadChunk() {
|
|
if (!g_downloadCtx.active || !g_responseQueue || g_activeConnHandle == BLE_HS_CONN_HANDLE_NONE)
|
|
return false;
|
|
|
|
if (g_downloadCtx.chunkScheduled)
|
|
return true;
|
|
|
|
ResponseMessage msg{};
|
|
msg.opcode = static_cast<uint8_t>(FileCommandCode::DownloadRequest);
|
|
msg.status = 0;
|
|
msg.length = 0;
|
|
msg.data = nullptr;
|
|
msg.streamDownload = true;
|
|
|
|
if (xQueueSend(g_responseQueue, &msg, pdMS_TO_TICKS(20)) != pdPASS) {
|
|
ESP_LOGW(kLogTag, "Failed to schedule download chunk; response queue full");
|
|
return false;
|
|
}
|
|
|
|
g_downloadCtx.chunkScheduled = true;
|
|
return true;
|
|
}
|
|
|
|
bool sendFileError(uint8_t opcode, int err, const char* message) {
|
|
const uint8_t status = static_cast<uint8_t>(std::min(err, 0x7F)) | kResponseFlagComplete;
|
|
if (message && *message != '\0') {
|
|
const std::size_t len = std::strlen(message);
|
|
return enqueueFileResponse(opcode, status, reinterpret_cast<const uint8_t*>(message), len);
|
|
}
|
|
return enqueueFileResponse(opcode, status, nullptr, 0);
|
|
}
|
|
|
|
void resetUploadContext() {
|
|
if (g_uploadCtx.file) {
|
|
std::fclose(g_uploadCtx.file);
|
|
g_uploadCtx.file = nullptr;
|
|
}
|
|
g_uploadCtx.path.clear();
|
|
g_uploadCtx.remaining = 0;
|
|
g_uploadCtx.active = false;
|
|
}
|
|
|
|
void resetDownloadContext() {
|
|
if (g_downloadCtx.file) {
|
|
std::fclose(g_downloadCtx.file);
|
|
g_downloadCtx.file = nullptr;
|
|
}
|
|
g_downloadCtx.remaining = 0;
|
|
g_downloadCtx.active = false;
|
|
g_downloadCtx.chunkScheduled = false;
|
|
}
|
|
|
|
void processDownloadChunk() {
|
|
if (!g_downloadCtx.active || !g_downloadCtx.file) {
|
|
return;
|
|
}
|
|
|
|
if (g_activeConnHandle == BLE_HS_CONN_HANDLE_NONE) {
|
|
resetDownloadContext();
|
|
return;
|
|
}
|
|
|
|
constexpr std::size_t kMaxChunkBuffer = 244;
|
|
std::array<uint8_t, kMaxChunkBuffer> buffer{};
|
|
|
|
std::size_t maxPayload = buffer.size();
|
|
if (g_activeConnHandle != BLE_HS_CONN_HANDLE_NONE) {
|
|
const uint16_t mtu = ble_att_mtu(g_activeConnHandle);
|
|
if (mtu > sizeof(PacketHeader)) {
|
|
maxPayload = std::min<std::size_t>(buffer.size(), static_cast<std::size_t>(mtu - sizeof(PacketHeader)));
|
|
} else {
|
|
maxPayload = std::min<std::size_t>(buffer.size(), static_cast<std::size_t>(20));
|
|
}
|
|
}
|
|
|
|
const std::size_t toRead = std::min<std::size_t>(maxPayload, g_downloadCtx.remaining);
|
|
if (toRead == 0) {
|
|
resetDownloadContext();
|
|
return;
|
|
}
|
|
|
|
const std::size_t read = std::fread(buffer.data(), 1, toRead, g_downloadCtx.file);
|
|
if (read == 0) {
|
|
const int err = ferror(g_downloadCtx.file) ? errno : EIO;
|
|
resetDownloadContext();
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), err, "Read failed");
|
|
return;
|
|
}
|
|
|
|
g_downloadCtx.remaining -= read;
|
|
|
|
ResponseMessage chunk{};
|
|
chunk.opcode = static_cast<uint8_t>(FileCommandCode::DownloadRequest);
|
|
chunk.status = (g_downloadCtx.remaining == 0) ? kResponseFlagComplete : 0;
|
|
chunk.length = static_cast<uint16_t>(read);
|
|
chunk.data = buffer.data();
|
|
|
|
bool sent = false;
|
|
while (g_activeConnHandle != BLE_HS_CONN_HANDLE_NONE) {
|
|
if (sendFileResponseNow(chunk)) {
|
|
sent = true;
|
|
break;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
}
|
|
if (!sent) {
|
|
resetDownloadContext();
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), EIO, "Notify failed");
|
|
return;
|
|
}
|
|
|
|
if (g_downloadCtx.remaining == 0) {
|
|
resetDownloadContext();
|
|
return;
|
|
}
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(3));
|
|
if (!scheduleDownloadChunk()) {
|
|
resetDownloadContext();
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), EAGAIN, "Queue busy");
|
|
}
|
|
}
|
|
|
|
void handleListDirectory(const uint8_t* payload, std::size_t length) {
|
|
if (length < sizeof(uint16_t)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::ListDirectory), EINVAL, "Invalid list payload");
|
|
return;
|
|
}
|
|
const uint16_t pathLen = static_cast<uint16_t>(payload[0] | (payload[1] << 8));
|
|
if (length < sizeof(uint16_t) + pathLen) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::ListDirectory), EINVAL, "Malformed list payload");
|
|
return;
|
|
}
|
|
std::string relative(reinterpret_cast<const char*>(payload + sizeof(uint16_t)), pathLen);
|
|
std::string absolute;
|
|
if (!sanitizePath(relative, absolute)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::ListDirectory), EINVAL, "Invalid path");
|
|
return;
|
|
}
|
|
|
|
DIR* dir = opendir(absolute.c_str());
|
|
if (!dir) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::ListDirectory), errno, "opendir failed");
|
|
return;
|
|
}
|
|
|
|
std::vector<uint8_t> buffer;
|
|
buffer.reserve(196);
|
|
struct dirent* entry = nullptr;
|
|
|
|
while ((entry = readdir(dir)) != nullptr) {
|
|
const char* name = entry->d_name;
|
|
if (std::strcmp(name, ".") == 0 || std::strcmp(name, "..") == 0)
|
|
continue;
|
|
|
|
std::string fullPath = absolute;
|
|
fullPath.push_back('/');
|
|
fullPath.append(name);
|
|
|
|
struct stat st{};
|
|
if (stat(fullPath.c_str(), &st) != 0)
|
|
continue;
|
|
|
|
const std::size_t nameLen = std::strlen(name);
|
|
if (nameLen > 0xFFFF)
|
|
continue;
|
|
|
|
const std::size_t recordSize = sizeof(uint8_t) * 2 + sizeof(uint16_t) + sizeof(uint32_t) + nameLen;
|
|
if (buffer.size() + recordSize > 180) {
|
|
if (!sendFileResponse(static_cast<uint8_t>(FileCommandCode::ListDirectory), 0, buffer.data(),
|
|
buffer.size())) {
|
|
closedir(dir);
|
|
return;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
buffer.clear();
|
|
}
|
|
|
|
const uint8_t type = S_ISDIR(st.st_mode) ? 1 : (S_ISREG(st.st_mode) ? 0 : 2);
|
|
const uint32_t size = S_ISREG(st.st_mode) ? static_cast<uint32_t>(st.st_size) : 0;
|
|
|
|
buffer.push_back(type);
|
|
buffer.push_back(0);
|
|
buffer.push_back(static_cast<uint8_t>(nameLen & 0xFF));
|
|
buffer.push_back(static_cast<uint8_t>((nameLen >> 8) & 0xFF));
|
|
|
|
buffer.push_back(static_cast<uint8_t>(size & 0xFF));
|
|
buffer.push_back(static_cast<uint8_t>((size >> 8) & 0xFF));
|
|
buffer.push_back(static_cast<uint8_t>((size >> 16) & 0xFF));
|
|
buffer.push_back(static_cast<uint8_t>((size >> 24) & 0xFF));
|
|
|
|
buffer.insert(buffer.end(), name, name + nameLen);
|
|
}
|
|
|
|
closedir(dir);
|
|
|
|
const uint8_t opcode = static_cast<uint8_t>(FileCommandCode::ListDirectory);
|
|
if (!buffer.empty()) {
|
|
if (!sendFileResponse(opcode, kResponseFlagComplete, buffer.data(), buffer.size()))
|
|
return;
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
} else {
|
|
sendFileResponse(opcode, kResponseFlagComplete, nullptr, 0);
|
|
}
|
|
}
|
|
|
|
void handleUploadBegin(const uint8_t* payload, std::size_t length) {
|
|
if (length < sizeof(uint16_t) + sizeof(uint32_t)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::UploadBegin), EINVAL, "Invalid upload header");
|
|
return;
|
|
}
|
|
|
|
const uint16_t pathLen = static_cast<uint16_t>(payload[0] | (payload[1] << 8));
|
|
if (length < sizeof(uint16_t) + pathLen + sizeof(uint32_t)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::UploadBegin), EINVAL, "Malformed upload header");
|
|
return;
|
|
}
|
|
|
|
const uint8_t* ptr = payload + sizeof(uint16_t);
|
|
std::string relative(reinterpret_cast<const char*>(ptr), pathLen);
|
|
ptr += pathLen;
|
|
|
|
const uint32_t fileSize = static_cast<uint32_t>(ptr[0] | (ptr[1] << 8) | (ptr[2] << 16) | (ptr[3] << 24));
|
|
|
|
std::string absolute;
|
|
if (!sanitizePath(relative, absolute)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::UploadBegin), EINVAL, "Invalid path");
|
|
return;
|
|
}
|
|
|
|
resetUploadContext();
|
|
|
|
FILE* file = std::fopen(absolute.c_str(), "wb");
|
|
if (!file) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::UploadBegin), errno, "Failed to open file");
|
|
return;
|
|
}
|
|
|
|
g_uploadCtx.file = file;
|
|
g_uploadCtx.path = std::move(absolute);
|
|
g_uploadCtx.remaining = fileSize;
|
|
g_uploadCtx.active = true;
|
|
|
|
if (!sendFileResponse(static_cast<uint8_t>(FileCommandCode::UploadBegin), kResponseFlagComplete, nullptr, 0)) {
|
|
resetUploadContext();
|
|
}
|
|
}
|
|
|
|
void handleUploadChunk(const uint8_t* payload, std::size_t length) {
|
|
if (!g_uploadCtx.active || g_uploadCtx.file == nullptr) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::UploadChunk), EBADF, "No active upload");
|
|
return;
|
|
}
|
|
|
|
if (length == 0) {
|
|
if (!sendFileResponse(static_cast<uint8_t>(FileCommandCode::UploadChunk), kResponseFlagComplete, nullptr, 0)) {
|
|
resetUploadContext();
|
|
return;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (g_uploadCtx.remaining >= length) {
|
|
g_uploadCtx.remaining -= length;
|
|
}
|
|
|
|
const size_t written = std::fwrite(payload, 1, length, g_uploadCtx.file);
|
|
if (written != length) {
|
|
const int err = ferror(g_uploadCtx.file) ? errno : EIO;
|
|
resetUploadContext();
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::UploadChunk), err, "Write failed");
|
|
return;
|
|
}
|
|
|
|
if (!sendFileResponse(static_cast<uint8_t>(FileCommandCode::UploadChunk), kResponseFlagComplete, nullptr, 0)) {
|
|
resetUploadContext();
|
|
}
|
|
}
|
|
|
|
void handleUploadEnd() {
|
|
if (!g_uploadCtx.active || g_uploadCtx.file == nullptr) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::UploadEnd), EBADF, "No active upload");
|
|
return;
|
|
}
|
|
|
|
std::fflush(g_uploadCtx.file);
|
|
resetUploadContext();
|
|
sendFileResponse(static_cast<uint8_t>(FileCommandCode::UploadEnd), kResponseFlagComplete, nullptr, 0);
|
|
}
|
|
|
|
void handleDownloadRequest(const uint8_t* payload, std::size_t length) {
|
|
if (length < sizeof(uint16_t)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), EINVAL, "Invalid download payload");
|
|
return;
|
|
}
|
|
|
|
const uint16_t pathLen = static_cast<uint16_t>(payload[0] | (payload[1] << 8));
|
|
if (length < sizeof(uint16_t) + pathLen) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), EINVAL, "Malformed path");
|
|
return;
|
|
}
|
|
|
|
std::string relative(reinterpret_cast<const char*>(payload + sizeof(uint16_t)), pathLen);
|
|
std::string absolute;
|
|
if (!sanitizePath(relative, absolute)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), EINVAL, "Invalid path");
|
|
return;
|
|
}
|
|
|
|
resetDownloadContext();
|
|
|
|
FILE* file = std::fopen(absolute.c_str(), "rb");
|
|
if (!file) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), errno, "Failed to open file");
|
|
return;
|
|
}
|
|
|
|
struct stat st{};
|
|
if (stat(absolute.c_str(), &st) != 0) {
|
|
std::fclose(file);
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::DownloadRequest), errno, "stat failed");
|
|
return;
|
|
}
|
|
|
|
g_downloadCtx.file = file;
|
|
g_downloadCtx.remaining = static_cast<std::size_t>(st.st_size);
|
|
g_downloadCtx.active = true;
|
|
|
|
uint8_t sizePayload[4];
|
|
const uint32_t size = static_cast<uint32_t>(st.st_size);
|
|
sizePayload[0] = static_cast<uint8_t>(size & 0xFF);
|
|
sizePayload[1] = static_cast<uint8_t>((size >> 8) & 0xFF);
|
|
sizePayload[2] = static_cast<uint8_t>((size >> 16) & 0xFF);
|
|
sizePayload[3] = static_cast<uint8_t>((size >> 24) & 0xFF);
|
|
|
|
const uint8_t opcode = static_cast<uint8_t>(FileCommandCode::DownloadRequest);
|
|
if (!sendFileResponse(opcode, 0, sizePayload, sizeof(sizePayload))) {
|
|
resetDownloadContext();
|
|
return;
|
|
}
|
|
|
|
if (g_downloadCtx.remaining == 0) {
|
|
sendFileResponse(opcode, kResponseFlagComplete, nullptr, 0);
|
|
resetDownloadContext();
|
|
return;
|
|
}
|
|
|
|
if (!scheduleDownloadChunk()) {
|
|
sendFileError(opcode, EAGAIN, "Queue busy");
|
|
resetDownloadContext();
|
|
}
|
|
}
|
|
|
|
void handleDeletePath(const uint8_t* payload, std::size_t length, bool directory) {
|
|
if (length < sizeof(uint16_t)) {
|
|
sendFileError(static_cast<uint8_t>(directory ? FileCommandCode::DeleteDirectory : FileCommandCode::DeleteFile),
|
|
EINVAL, "Invalid payload");
|
|
return;
|
|
}
|
|
const uint16_t pathLen = static_cast<uint16_t>(payload[0] | (payload[1] << 8));
|
|
if (length < sizeof(uint16_t) + pathLen) {
|
|
sendFileError(static_cast<uint8_t>(directory ? FileCommandCode::DeleteDirectory : FileCommandCode::DeleteFile),
|
|
EINVAL, "Malformed path");
|
|
return;
|
|
}
|
|
std::string relative(reinterpret_cast<const char*>(payload + sizeof(uint16_t)), pathLen);
|
|
std::string absolute;
|
|
if (!sanitizePath(relative, absolute)) {
|
|
sendFileError(static_cast<uint8_t>(directory ? FileCommandCode::DeleteDirectory : FileCommandCode::DeleteFile),
|
|
EINVAL, "Invalid path");
|
|
return;
|
|
}
|
|
|
|
int result = 0;
|
|
if (directory) {
|
|
result = rmdir(absolute.c_str());
|
|
} else {
|
|
result = std::remove(absolute.c_str());
|
|
}
|
|
|
|
const uint8_t opcode =
|
|
static_cast<uint8_t>(directory ? FileCommandCode::DeleteDirectory : FileCommandCode::DeleteFile);
|
|
if (result != 0) {
|
|
sendFileError(opcode, errno, "Remove failed");
|
|
} else {
|
|
sendFileResponse(opcode, kResponseFlagComplete, nullptr, 0);
|
|
}
|
|
}
|
|
|
|
void handleCreateDirectory(const uint8_t* payload, std::size_t length) {
|
|
if (length < sizeof(uint16_t)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::CreateDirectory), EINVAL, "Invalid payload");
|
|
return;
|
|
}
|
|
const uint16_t pathLen = static_cast<uint16_t>(payload[0] | (payload[1] << 8));
|
|
if (length < sizeof(uint16_t) + pathLen) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::CreateDirectory), EINVAL, "Malformed path");
|
|
return;
|
|
}
|
|
std::string relative(reinterpret_cast<const char*>(payload + sizeof(uint16_t)), pathLen);
|
|
std::string absolute;
|
|
if (!sanitizePath(relative, absolute)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::CreateDirectory), EINVAL, "Invalid path");
|
|
return;
|
|
}
|
|
|
|
if (mkdir(absolute.c_str(), 0755) != 0) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::CreateDirectory), errno, "mkdir failed");
|
|
} else {
|
|
sendFileResponse(static_cast<uint8_t>(FileCommandCode::CreateDirectory), kResponseFlagComplete, nullptr, 0);
|
|
}
|
|
}
|
|
|
|
void handleRename(const uint8_t* payload, std::size_t length) {
|
|
if (length < sizeof(uint16_t) * 2) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::RenamePath), EINVAL, "Invalid rename payload");
|
|
return;
|
|
}
|
|
|
|
const uint16_t srcLen = static_cast<uint16_t>(payload[0] | (payload[1] << 8));
|
|
if (length < sizeof(uint16_t) + srcLen + sizeof(uint16_t)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::RenamePath), EINVAL, "Malformed source path");
|
|
return;
|
|
}
|
|
|
|
const uint8_t* ptr = payload + sizeof(uint16_t);
|
|
std::string srcRel(reinterpret_cast<const char*>(ptr), srcLen);
|
|
ptr += srcLen;
|
|
|
|
const uint16_t dstLen = static_cast<uint16_t>(ptr[0] | (ptr[1] << 8));
|
|
ptr += sizeof(uint16_t);
|
|
if (length < sizeof(uint16_t) * 2 + srcLen + dstLen) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::RenamePath), EINVAL, "Malformed destination path");
|
|
return;
|
|
}
|
|
|
|
std::string dstRel(reinterpret_cast<const char*>(ptr), dstLen);
|
|
|
|
std::string srcAbs;
|
|
std::string dstAbs;
|
|
if (!sanitizePath(srcRel, srcAbs) || !sanitizePath(dstRel, dstAbs)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::RenamePath), EINVAL, "Invalid path");
|
|
return;
|
|
}
|
|
|
|
if (std::rename(srcAbs.c_str(), dstAbs.c_str()) != 0) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::RenamePath), errno, "rename failed");
|
|
} else {
|
|
sendFileResponse(static_cast<uint8_t>(FileCommandCode::RenamePath), kResponseFlagComplete, nullptr, 0);
|
|
}
|
|
}
|
|
|
|
void requestAncsAttributes(uint16_t connHandle, uint32_t uid) {
|
|
if (!g_notificationCenter || g_ancsControlPointHandle == 0)
|
|
return;
|
|
|
|
static constexpr uint16_t kMaxTitle = 96;
|
|
static constexpr uint16_t kMaxMessage = 256;
|
|
|
|
uint8_t buffer[32];
|
|
std::size_t index = 0;
|
|
buffer[index++] = 0x00; // CommandIDGetNotificationAttributes
|
|
buffer[index++] = static_cast<uint8_t>(uid & 0xFF);
|
|
buffer[index++] = static_cast<uint8_t>((uid >> 8) & 0xFF);
|
|
buffer[index++] = static_cast<uint8_t>((uid >> 16) & 0xFF);
|
|
buffer[index++] = static_cast<uint8_t>((uid >> 24) & 0xFF);
|
|
|
|
buffer[index++] = 0x00; // App Identifier
|
|
|
|
buffer[index++] = 0x01; // Title
|
|
buffer[index++] = static_cast<uint8_t>(kMaxTitle & 0xFF);
|
|
buffer[index++] = static_cast<uint8_t>((kMaxTitle >> 8) & 0xFF);
|
|
|
|
buffer[index++] = 0x03; // Message
|
|
buffer[index++] = static_cast<uint8_t>(kMaxMessage & 0xFF);
|
|
buffer[index++] = static_cast<uint8_t>((kMaxMessage >> 8) & 0xFF);
|
|
|
|
const int rc = ble_gattc_write_flat(connHandle, g_ancsControlPointHandle, buffer, index, nullptr, nullptr);
|
|
if (rc != 0) {
|
|
ESP_LOGW(kLogTag, "ANCS attribute request failed: rc=%d uid=%" PRIu32, rc, uid);
|
|
} else {
|
|
ESP_LOGI(kLogTag, "Requested ANCS attributes for uid=%" PRIu32, uid);
|
|
}
|
|
}
|
|
|
|
void applyPreferredConnectionParams(uint16_t connHandle) {
|
|
ble_gap_upd_params params{
|
|
.itvl_min = kPreferredConnIntervalMin,
|
|
.itvl_max = kPreferredConnIntervalMax,
|
|
.latency = kPreferredConnLatency,
|
|
.supervision_timeout = kPreferredSupervisionTimeout,
|
|
.min_ce_len = 0,
|
|
.max_ce_len = 0,
|
|
};
|
|
|
|
const int rc = ble_gap_update_params(connHandle, ¶ms);
|
|
if (rc != 0) {
|
|
ESP_LOGW(kLogTag, "ble_gap_update_params failed (rc=%d)", rc);
|
|
} else {
|
|
ESP_LOGI(kLogTag, "Requested conn params: %.1f-%.1f ms latency %u timeout %.0f ms",
|
|
connIntervalUnitsToMs(params.itvl_min), connIntervalUnitsToMs(params.itvl_max), params.latency,
|
|
supervisionUnitsToMs(params.supervision_timeout));
|
|
}
|
|
}
|
|
|
|
void handleAncsNotificationSource(uint16_t connHandle, const uint8_t* data, uint16_t length) {
|
|
if (!g_notificationCenter || !data || length < 8)
|
|
return;
|
|
|
|
const uint8_t eventId = data[0];
|
|
const uint8_t eventFlags = data[1];
|
|
const uint8_t category = data[2];
|
|
const uint32_t uid = static_cast<uint32_t>(data[4]) | (static_cast<uint32_t>(data[5]) << 8) |
|
|
(static_cast<uint32_t>(data[6]) << 16) | (static_cast<uint32_t>(data[7]) << 24);
|
|
|
|
ESP_LOGI(kLogTag, "ANCS notification event=%u flags=0x%02x category=%u uid=%" PRIu32, eventId, eventFlags, category,
|
|
uid);
|
|
|
|
if (eventId == 2) { // Removed
|
|
discardPending(uid);
|
|
g_notificationCenter->removeByExternalId(uid);
|
|
ESP_LOGI(kLogTag, "Cleared notification uid=%" PRIu32, uid);
|
|
return;
|
|
}
|
|
|
|
auto& pending = ensurePending(uid);
|
|
pending.flags = eventFlags;
|
|
pending.category = category;
|
|
|
|
requestAncsAttributes(connHandle, uid);
|
|
}
|
|
|
|
bool handleAncsDataSource(const uint8_t* data, uint16_t length) {
|
|
if (!g_notificationCenter || !data || length == 0)
|
|
return false;
|
|
|
|
g_dataSourceBuffer.insert(g_dataSourceBuffer.end(), data, data + length);
|
|
if (g_dataSourceBuffer.size() > 2048) {
|
|
ESP_LOGW(kLogTag, "Dropping oversized ANCS data buffer (%u bytes)",
|
|
static_cast<unsigned>(g_dataSourceBuffer.size()));
|
|
g_dataSourceBuffer.clear();
|
|
return false;
|
|
}
|
|
const uint8_t* buffer = g_dataSourceBuffer.data();
|
|
const uint16_t total = static_cast<uint16_t>(g_dataSourceBuffer.size());
|
|
|
|
if (total < 5)
|
|
return false;
|
|
|
|
if (buffer[0] != 0x00)
|
|
return false;
|
|
|
|
const uint32_t uid = static_cast<uint32_t>(buffer[1]) | (static_cast<uint32_t>(buffer[2]) << 8) |
|
|
(static_cast<uint32_t>(buffer[3]) << 16) | (static_cast<uint32_t>(buffer[4]) << 24);
|
|
|
|
PendingNotification* pending = findPending(uid);
|
|
if (!pending)
|
|
pending = &ensurePending(uid);
|
|
|
|
std::size_t offset = 5;
|
|
while (offset + 3 <= total) {
|
|
const uint8_t attrId = buffer[offset];
|
|
const uint16_t attrLen =
|
|
static_cast<uint16_t>(buffer[offset + 1]) | (static_cast<uint16_t>(buffer[offset + 2]) << 8);
|
|
offset += 3;
|
|
if (offset + attrLen > total)
|
|
return false;
|
|
|
|
const char* valuePtr = reinterpret_cast<const char*>(buffer + offset);
|
|
const std::string value(valuePtr, valuePtr + attrLen);
|
|
switch (attrId) {
|
|
case 0x00:
|
|
pending->appIdentifier = value;
|
|
ESP_LOGD(kLogTag, "ANCS uid=%" PRIu32 " appId=%.*s", uid, static_cast<int>(attrLen), valuePtr);
|
|
break;
|
|
case 0x01:
|
|
pending->title = value;
|
|
ESP_LOGD(kLogTag, "ANCS uid=%" PRIu32 " title=%.*s", uid, static_cast<int>(attrLen), valuePtr);
|
|
break;
|
|
case 0x03:
|
|
pending->message = value;
|
|
ESP_LOGD(kLogTag, "ANCS uid=%" PRIu32 " message=%.*s", uid, static_cast<int>(attrLen), valuePtr);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
offset += attrLen;
|
|
}
|
|
|
|
if (offset != total)
|
|
return false;
|
|
|
|
ESP_LOGI(kLogTag, "ANCS data complete uid=%" PRIu32, uid);
|
|
finalizePending(uid);
|
|
g_dataSourceBuffer.clear();
|
|
return true;
|
|
}
|
|
|
|
int ancsDescriptorDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, uint16_t /*chr_val_handle*/,
|
|
const ble_gatt_dsc* dsc, void* /*arg*/) {
|
|
if (error->status == 0 && dsc) {
|
|
if (ble_uuid_cmp(&dsc->uuid.u, &kClientConfigUuid.u) == 0) {
|
|
const uint8_t enable[2] = {0x01, 0x00};
|
|
const int rc = ble_gattc_write_flat(connHandle, dsc->handle, enable, sizeof(enable), nullptr, nullptr);
|
|
if (rc != 0)
|
|
ESP_LOGW(kLogTag, "Failed to enable ANCS notifications (rc=%d) handle=%u", rc, dsc->handle);
|
|
else
|
|
ESP_LOGI(kLogTag, "Subscribed ANCS descriptor handle=%u", dsc->handle);
|
|
}
|
|
return 0;
|
|
}
|
|
if (error->status == BLE_HS_EDONE)
|
|
return 0;
|
|
return error->status;
|
|
}
|
|
|
|
int ancsCharacteristicDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_chr* chr,
|
|
void* /*arg*/) {
|
|
if (error->status == BLE_HS_EDONE)
|
|
return 0;
|
|
if (error->status != 0)
|
|
return error->status;
|
|
|
|
if (!chr)
|
|
return 0;
|
|
|
|
if ((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) &&
|
|
ble_uuid_cmp(&chr->uuid.u, &kAncsNotificationSourceUuid.u) == 0) {
|
|
g_ancsNotificationSourceHandle = chr->val_handle;
|
|
ESP_LOGI(kLogTag, "ANCS notification source handle=%u", g_ancsNotificationSourceHandle);
|
|
ble_gattc_disc_all_dscs(connHandle, chr->val_handle, g_ancsServiceEndHandle, ancsDescriptorDiscoveredCb,
|
|
nullptr);
|
|
} else if ((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) &&
|
|
ble_uuid_cmp(&chr->uuid.u, &kAncsDataSourceUuid.u) == 0) {
|
|
g_ancsDataSourceHandle = chr->val_handle;
|
|
ESP_LOGI(kLogTag, "ANCS data source handle=%u", g_ancsDataSourceHandle);
|
|
ble_gattc_disc_all_dscs(connHandle, chr->val_handle, g_ancsServiceEndHandle, ancsDescriptorDiscoveredCb,
|
|
nullptr);
|
|
} else if ((chr->properties & BLE_GATT_CHR_PROP_WRITE) &&
|
|
ble_uuid_cmp(&chr->uuid.u, &kAncsControlPointUuid.u) == 0) {
|
|
g_ancsControlPointHandle = chr->val_handle;
|
|
ESP_LOGI(kLogTag, "ANCS control point handle=%u", g_ancsControlPointHandle);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int ancsServiceDiscoveredCb(uint16_t connHandle, const ble_gatt_error* error, const ble_gatt_svc* svc, void* /*arg*/) {
|
|
if (error->status == BLE_HS_EDONE)
|
|
return 0;
|
|
if (error->status != 0)
|
|
return error->status;
|
|
if (!svc) {
|
|
ESP_LOGW(kLogTag, "ANCS service missing");
|
|
return 0;
|
|
}
|
|
|
|
g_ancsServiceEndHandle = svc->end_handle;
|
|
ESP_LOGI(kLogTag, "ANCS service discovered: start=%u end=%u", svc->start_handle, svc->end_handle);
|
|
return ble_gattc_disc_all_chrs(connHandle, svc->start_handle, svc->end_handle, ancsCharacteristicDiscoveredCb,
|
|
nullptr);
|
|
}
|
|
|
|
void handleGattsRegister(ble_gatt_register_ctxt* ctxt, void* /*arg*/) {
|
|
if (ctxt->op == BLE_GATT_REGISTER_OP_CHR) {
|
|
if (ble_uuid_cmp(ctxt->chr.chr_def->uuid, &kFileCommandCharUuid.u) == 0) {
|
|
g_fileCommandValueHandle = ctxt->chr.val_handle;
|
|
ESP_LOGI(kLogTag, "File command characteristic handle=%u", g_fileCommandValueHandle);
|
|
} else if (ble_uuid_cmp(ctxt->chr.chr_def->uuid, &kFileResponseCharUuid.u) == 0) {
|
|
g_fileResponseValueHandle = ctxt->chr.val_handle;
|
|
ESP_LOGI(kLogTag, "File response characteristic handle=%u", g_fileResponseValueHandle);
|
|
}
|
|
}
|
|
}
|
|
|
|
int fileCommandAccess(uint16_t connHandle, uint16_t /*attrHandle*/, ble_gatt_access_ctxt* ctxt, void* /*arg*/) {
|
|
if (ctxt->op != BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
|
}
|
|
|
|
const uint16_t incomingLen = OS_MBUF_PKTLEN(ctxt->om);
|
|
if (incomingLen < sizeof(PacketHeader)) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::ListDirectory), EINVAL, "Command too short");
|
|
return 0;
|
|
}
|
|
|
|
std::vector<uint8_t> buffer(incomingLen);
|
|
const int rc = os_mbuf_copydata(ctxt->om, 0, incomingLen, buffer.data());
|
|
if (rc != 0) {
|
|
sendFileError(static_cast<uint8_t>(FileCommandCode::ListDirectory), EIO, "Read failed");
|
|
return 0;
|
|
}
|
|
|
|
const auto* header = reinterpret_cast<const PacketHeader*>(buffer.data());
|
|
const uint16_t payloadLen = header->length;
|
|
if (payloadLen + sizeof(PacketHeader) != incomingLen) {
|
|
sendFileError(header->opcode, EINVAL, "Length mismatch");
|
|
return 0;
|
|
}
|
|
|
|
const uint8_t* payload = buffer.data() + sizeof(PacketHeader);
|
|
|
|
g_activeConnHandle = connHandle;
|
|
|
|
switch (static_cast<FileCommandCode>(header->opcode)) {
|
|
case FileCommandCode::ListDirectory:
|
|
handleListDirectory(payload, payloadLen);
|
|
break;
|
|
case FileCommandCode::UploadBegin:
|
|
handleUploadBegin(payload, payloadLen);
|
|
break;
|
|
case FileCommandCode::UploadChunk:
|
|
handleUploadChunk(payload, payloadLen);
|
|
break;
|
|
case FileCommandCode::UploadEnd:
|
|
handleUploadEnd();
|
|
break;
|
|
case FileCommandCode::DownloadRequest:
|
|
handleDownloadRequest(payload, payloadLen);
|
|
break;
|
|
case FileCommandCode::DeleteFile:
|
|
handleDeletePath(payload, payloadLen, false);
|
|
break;
|
|
case FileCommandCode::CreateDirectory:
|
|
handleCreateDirectory(payload, payloadLen);
|
|
break;
|
|
case FileCommandCode::DeleteDirectory:
|
|
handleDeletePath(payload, payloadLen, true);
|
|
break;
|
|
case FileCommandCode::RenamePath:
|
|
handleRename(payload, payloadLen);
|
|
break;
|
|
default:
|
|
sendFileError(header->opcode, EINVAL, "Unknown opcode");
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int fileResponseAccess(uint16_t /*connHandle*/, uint16_t /*attrHandle*/, ble_gatt_access_ctxt* ctxt, void* /*arg*/) {
|
|
if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) {
|
|
return BLE_ATT_ERR_WRITE_NOT_PERMITTED;
|
|
}
|
|
return BLE_ATT_ERR_READ_NOT_PERMITTED;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void notificationTask(void* /*param*/) {
|
|
ResponseMessage msg{};
|
|
while (xQueueReceive(g_responseQueue, &msg, portMAX_DELAY) == pdTRUE) {
|
|
if (msg.opcode == kResponseOpcodeShutdown && msg.length == 0 && !msg.streamDownload) {
|
|
if (msg.data)
|
|
vPortFree(msg.data);
|
|
break;
|
|
}
|
|
|
|
if (msg.streamDownload) {
|
|
g_downloadCtx.chunkScheduled = false;
|
|
processDownloadChunk();
|
|
if (msg.data)
|
|
vPortFree(msg.data);
|
|
continue;
|
|
}
|
|
|
|
bool sent = false;
|
|
while (g_activeConnHandle != BLE_HS_CONN_HANDLE_NONE) {
|
|
if (sendFileResponseNow(msg)) {
|
|
sent = true;
|
|
break;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
}
|
|
|
|
if (!sent) {
|
|
ESP_LOGW(kLogTag, "Notification delivery failed for opcode=0x%02x", msg.opcode);
|
|
}
|
|
|
|
if (msg.data)
|
|
vPortFree(msg.data);
|
|
}
|
|
|
|
g_notifyTaskHandle = nullptr;
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
void logConnectionParams(uint16_t connHandle, const char* context) {
|
|
ble_gap_conn_desc desc{};
|
|
if (ble_gap_conn_find(connHandle, &desc) != 0) {
|
|
ESP_LOGW(kLogTag, "%s: unable to read conn params for handle=%u", context, static_cast<unsigned>(connHandle));
|
|
return;
|
|
}
|
|
|
|
const float intervalMs = connIntervalUnitsToMs(desc.conn_itvl);
|
|
const float timeoutMs = supervisionUnitsToMs(desc.supervision_timeout);
|
|
|
|
ESP_LOGI(kLogTag, "%s params: interval=%.1f ms latency=%u supervision=%.0f ms", context, intervalMs,
|
|
static_cast<unsigned>(desc.conn_latency), timeoutMs);
|
|
}
|
|
|
|
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));
|
|
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;
|
|
const uint16_t advIntervalMin = BLE_GAP_ADV_ITVL_MS(1000);
|
|
advParams.itvl_min = advIntervalMin;
|
|
advParams.itvl_max = BLE_GAP_ADV_ITVL_MS(1200);
|
|
|
|
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]);
|
|
}
|
|
|
|
rc = ble_gap_set_prefered_default_le_phy(BLE_HCI_LE_PHY_1M_PREF_MASK, BLE_HCI_LE_PHY_1M_PREF_MASK);
|
|
if (rc != 0) {
|
|
ESP_LOGW(kLogTag, "Failed to set preferred PHY (rc=%d)", rc);
|
|
}
|
|
|
|
if (esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_N12) != ESP_OK) {
|
|
ESP_LOGW(kLogTag, "Failed to set default TX power level");
|
|
}
|
|
if (esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_N12) != ESP_OK) {
|
|
ESP_LOGW(kLogTag, "Failed to set advertising TX power level");
|
|
}
|
|
if (esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_N12) != ESP_OK) {
|
|
ESP_LOGW(kLogTag, "Failed to set scan TX power level");
|
|
}
|
|
|
|
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);
|
|
g_activeConnHandle = event->connect.conn_handle;
|
|
logConnectionParams(event->connect.conn_handle, "Initial");
|
|
|
|
ble_gap_conn_desc desc{};
|
|
if (ble_gap_conn_find(event->connect.conn_handle, &desc) == 0) {
|
|
ESP_LOGI(kLogTag, "Security state on connect: bonded=%d encrypted=%d authenticated=%d key_size=%u",
|
|
desc.sec_state.bonded, desc.sec_state.encrypted, desc.sec_state.authenticated,
|
|
static_cast<unsigned>(desc.sec_state.key_size));
|
|
if (!desc.sec_state.encrypted && !desc.sec_state.bonded && !g_securityRequested) {
|
|
const int src = ble_gap_security_initiate(event->connect.conn_handle);
|
|
if (src == 0) {
|
|
g_securityRequested = true;
|
|
ESP_LOGI(kLogTag, "Security procedure initiated");
|
|
} else {
|
|
ESP_LOGW(kLogTag, "Failed to initiate security (rc=%d)", src);
|
|
}
|
|
}
|
|
}
|
|
} 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);
|
|
g_activeConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
|
g_securityRequested = false;
|
|
resetAncsState();
|
|
resetUploadContext();
|
|
resetDownloadContext();
|
|
if (g_responseQueue)
|
|
xQueueReset(g_responseQueue);
|
|
startAdvertising();
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_ENC_CHANGE:
|
|
if (event->enc_change.status == 0) {
|
|
ESP_LOGI(kLogTag, "Link encrypted; discovering ANCS");
|
|
resetAncsState();
|
|
g_securityRequested = false;
|
|
ble_gattc_disc_svc_by_uuid(event->enc_change.conn_handle, &kAncsServiceUuid.u, ancsServiceDiscoveredCb,
|
|
nullptr);
|
|
applyPreferredConnectionParams(event->enc_change.conn_handle);
|
|
} else {
|
|
ESP_LOGW(kLogTag, "Encryption change failed; status=%d", event->enc_change.status);
|
|
g_securityRequested = false;
|
|
}
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_ADV_COMPLETE:
|
|
ESP_LOGI(kLogTag, "Advertising complete; restarting");
|
|
startAdvertising();
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_CONN_UPDATE:
|
|
if (event->conn_update.status == 0) {
|
|
logConnectionParams(event->conn_update.conn_handle, "Updated");
|
|
} else {
|
|
ESP_LOGW(kLogTag, "Connection update failed; status=%d handle=%u", event->conn_update.status,
|
|
static_cast<unsigned>(event->conn_update.conn_handle));
|
|
}
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_CONN_UPDATE_REQ:
|
|
if (event->conn_update_req.self_params) {
|
|
const auto& params = *event->conn_update_req.self_params;
|
|
ESP_LOGI(kLogTag, "Peer update request -> interval %.1f-%.1f ms latency %u timeout %.0f ms",
|
|
connIntervalUnitsToMs(params.itvl_min), connIntervalUnitsToMs(params.itvl_max), params.latency,
|
|
supervisionUnitsToMs(params.supervision_timeout));
|
|
}
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_NOTIFY_RX:
|
|
if (event->notify_rx.attr_handle == g_ancsNotificationSourceHandle) {
|
|
handleAncsNotificationSource(event->notify_rx.conn_handle, event->notify_rx.om->om_data,
|
|
event->notify_rx.om->om_len);
|
|
} else if (event->notify_rx.attr_handle == g_ancsDataSourceHandle) {
|
|
const uint16_t len = event->notify_rx.om->om_len;
|
|
ESP_LOGD(kLogTag, "ANCS data chunk len=%u", static_cast<unsigned>(len));
|
|
handleAncsDataSource(event->notify_rx.om->om_data, len);
|
|
}
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_MTU:
|
|
g_mtuSize = event->mtu.value;
|
|
ESP_LOGI(kLogTag, "MTU updated to %u", g_mtuSize);
|
|
break;
|
|
|
|
case BLE_GAP_EVENT_REPEAT_PAIRING: {
|
|
ble_gap_conn_desc desc{};
|
|
const int findRc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc);
|
|
if (findRc != 0) {
|
|
ESP_LOGW(kLogTag, "Repeat pairing but failed to fetch connection descriptor (rc=%d)", findRc);
|
|
} else {
|
|
ESP_LOGI(kLogTag,
|
|
"Repeat pairing requested by %02X:%02X:%02X:%02X:%02X:%02X; deleting existing bond to re-pair",
|
|
desc.peer_id_addr.val[0], desc.peer_id_addr.val[1], desc.peer_id_addr.val[2],
|
|
desc.peer_id_addr.val[3], desc.peer_id_addr.val[4], desc.peer_id_addr.val[5]);
|
|
const int deleteRc = ble_store_util_delete_peer(&desc.peer_id_addr);
|
|
if (deleteRc != 0) {
|
|
ESP_LOGW(kLogTag, "Failed to delete existing bond (rc=%d)", deleteRc);
|
|
}
|
|
}
|
|
resetAncsState();
|
|
clearDeliveredNotifications();
|
|
g_securityRequested = false;
|
|
return BLE_GAP_REPEAT_PAIRING_RETRY;
|
|
}
|
|
|
|
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 = handleGattsRegister;
|
|
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 = 1;
|
|
ble_hs_cfg.sm_mitm = 0;
|
|
ble_hs_cfg.sm_sc = 0;
|
|
ble_hs_cfg.sm_our_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
|
ble_hs_cfg.sm_their_key_dist = BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID;
|
|
ESP_ERROR_CHECK(nimble_port_init());
|
|
|
|
configureGap();
|
|
ble_store_config_init();
|
|
|
|
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;
|
|
}
|
|
|
|
resetAncsState();
|
|
|
|
if (!initController()) {
|
|
ESP_LOGE(kLogTag, "Unable to initialise BLE time sync service");
|
|
return;
|
|
}
|
|
|
|
if (!g_responseQueue) {
|
|
g_responseQueue = xQueueCreate(256, sizeof(ResponseMessage));
|
|
if (!g_responseQueue) {
|
|
ESP_LOGE(kLogTag, "Failed to create response queue");
|
|
nimble_port_deinit();
|
|
esp_bt_controller_disable();
|
|
esp_bt_controller_deinit();
|
|
return;
|
|
}
|
|
} else {
|
|
xQueueReset(g_responseQueue);
|
|
}
|
|
|
|
if (!g_notifyTaskHandle) {
|
|
if (xTaskCreate(notificationTask, "BleNotify", 4096, nullptr, 5, &g_notifyTaskHandle) != pdPASS) {
|
|
ESP_LOGE(kLogTag, "Failed to start notification task");
|
|
vQueueDelete(g_responseQueue);
|
|
g_responseQueue = nullptr;
|
|
nimble_port_deinit();
|
|
esp_bt_controller_disable();
|
|
esp_bt_controller_deinit();
|
|
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");
|
|
|
|
if (g_responseQueue) {
|
|
xQueueReset(g_responseQueue);
|
|
ResponseMessage stop{};
|
|
stop.opcode = kResponseOpcodeShutdown;
|
|
stop.status = 0;
|
|
stop.length = 0;
|
|
stop.data = nullptr;
|
|
xQueueSend(g_responseQueue, &stop, pdMS_TO_TICKS(20));
|
|
|
|
while (g_notifyTaskHandle != nullptr) {
|
|
vTaskDelay(pdMS_TO_TICKS(5));
|
|
}
|
|
|
|
vQueueDelete(g_responseQueue);
|
|
g_responseQueue = nullptr;
|
|
}
|
|
|
|
resetAncsState();
|
|
}
|
|
|
|
void set_notification_center(cardboy::sdk::INotificationCenter* center) { g_notificationCenter = center; }
|
|
|
|
} // namespace cardboy::backend::esp
|