somewhat working file sync

This commit is contained in:
2025-10-19 23:07:20 +02:00
parent b4f11851d7
commit 3ab2a7bf26
12 changed files with 5934 additions and 67 deletions

View File

@@ -1,5 +1,6 @@
#include "cardboy/backend/esp/time_sync_service.hpp"
#include "cardboy/backend/esp/fs_helper.hpp"
#include "sdkconfig.h"
#include <sys/time.h>
@@ -7,23 +8,33 @@
#include <unistd.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "host/ble_gap.h"
#include "host/ble_gatt.h"
#include "host/ble_hs.h"
#include "host/ble_hs_mbuf.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 <dirent.h>
#include <esp_bt.h>
#include <esp_err.h>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <vector>
namespace cardboy::backend::esp {
@@ -38,6 +49,13 @@ static const ble_uuid128_t kTimeServiceUuid = BLE_UUID128_INIT(0x30, 0xF2, 0xD
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
@@ -47,12 +65,143 @@ struct [[gnu::packed]] TimeSyncPayload {
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 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;
struct ResponseMessage {
uint8_t opcode;
uint8_t status;
uint16_t length;
uint8_t* data;
};
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;
};
static FileUploadContext g_uploadCtx{};
static FileDownloadContext g_downloadCtx{};
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);
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{};
@@ -87,6 +236,574 @@ void setSystemTimeFromPayload(const TimeSyncPayload& payload) {
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 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;
}
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;
}
std::array<uint8_t, 128> chunk{};
while (g_downloadCtx.remaining > 0) {
const std::size_t toRead = std::min<std::size_t>(chunk.size(), g_downloadCtx.remaining);
const std::size_t read = std::fread(chunk.data(), 1, toRead, g_downloadCtx.file);
if (read == 0) {
const int err = ferror(g_downloadCtx.file) ? errno : EIO;
resetDownloadContext();
sendFileError(opcode, err, "Read failed");
return;
}
g_downloadCtx.remaining -= read;
const uint8_t status = (g_downloadCtx.remaining == 0) ? kResponseFlagComplete : 0;
if (!sendFileResponse(opcode, status, chunk.data(), read)) {
resetDownloadContext();
return;
}
if (g_downloadCtx.remaining > 0)
vTaskDelay(pdMS_TO_TICKS(5));
}
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 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;
@@ -109,27 +826,35 @@ int timeSyncWriteAccess(uint16_t /*conn_handle*/, uint16_t /*attr_handle*/, ble_
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 notificationTask(void* /*param*/) {
ResponseMessage msg{};
while (xQueueReceive(g_responseQueue, &msg, portMAX_DELAY) == pdTRUE) {
if (msg.opcode == kResponseOpcodeShutdown && msg.length == 0) {
if (msg.data)
vPortFree(msg.data);
break;
}
bool sent = false;
for (uint8_t attempt = 0; attempt < 20; ++attempt) {
if (sendFileResponseNow(msg)) {
sent = true;
break;
}
vTaskDelay(pdMS_TO_TICKS(5));
}
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 startAdvertising() {
ble_hs_adv_fields fields{};
@@ -214,6 +939,7 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) {
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;
} else {
ESP_LOGW(kLogTag, "Connection attempt failed; status=%d", event->connect.status);
startAdvertising();
@@ -222,6 +948,11 @@ int gapEventHandler(struct ble_gap_event* event, void* /*arg*/) {
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(kLogTag, "Disconnected; reason=%d", event->disconnect.reason);
g_activeConnHandle = BLE_HS_CONN_HANDLE_NONE;
resetUploadContext();
resetDownloadContext();
if (g_responseQueue)
xQueueReset(g_responseQueue);
startAdvertising();
break;
@@ -254,7 +985,7 @@ void configureGap() {
bool initController() {
ble_hs_cfg.reset_cb = onReset;
ble_hs_cfg.sync_cb = onSync;
ble_hs_cfg.gatts_register_cb = nullptr;
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 = 0;
@@ -291,6 +1022,31 @@ void ensure_time_sync_service_started() {
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");
@@ -316,6 +1072,23 @@ void shutdown_time_sync_service() {
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;
}
}
} // namespace cardboy::backend::esp