mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 23:27:38 +03:00
Merge 8e38e1bd49 into da4d3b5ea5
This commit is contained in:
commit
9b9d45b56c
@ -48,6 +48,7 @@ lib_deps =
|
|||||||
bblanchon/ArduinoJson @ 7.4.2
|
bblanchon/ArduinoJson @ 7.4.2
|
||||||
ricmoo/QRCode @ 0.0.1
|
ricmoo/QRCode @ 0.0.1
|
||||||
links2004/WebSockets @ 2.7.3
|
links2004/WebSockets @ 2.7.3
|
||||||
|
h2zero/NimBLE-Arduino @ 2.3.7
|
||||||
|
|
||||||
[env:default]
|
[env:default]
|
||||||
extends = base
|
extends = base
|
||||||
|
|||||||
337
src/activities/bluetooth/BluetoothActivity.cpp
Normal file
337
src/activities/bluetooth/BluetoothActivity.cpp
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
#include "BluetoothActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
|
#define DEVICE_NAME "EPaper"
|
||||||
|
#define SERVICE_UUID "4ae29d01-499a-480a-8c41-a82192105125"
|
||||||
|
#define REQUEST_CHARACTERISTIC_UUID "a00e530d-b48b-48c8-aadb-d062a1b91792"
|
||||||
|
#define RESPONSE_CHARACTERISTIC_UUID "0c656023-dee6-47c5-9afb-e601dfbdaa1d"
|
||||||
|
|
||||||
|
#define OUTPUT_DIRECTORY "/bt"
|
||||||
|
#define MAX_FILENAME_LENGTH 200
|
||||||
|
|
||||||
|
#define PROTOCOL_ASSERT(cond, fmt, ...) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
snprintf(errorMessage, sizeof(errorMessage), fmt, ##__VA_ARGS__); \
|
||||||
|
intoState(STATE_ERROR); \
|
||||||
|
return; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
void BluetoothActivity::displayTaskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<BluetoothActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::reportTaskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<BluetoothActivity*>(param);
|
||||||
|
self->report();
|
||||||
|
vTaskDelete(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::report() {
|
||||||
|
if (state != STATE_DONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onFileReceived(OUTPUT_DIRECTORY "/" + filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::startAdvertising() { NimBLEDevice::startAdvertising(); }
|
||||||
|
|
||||||
|
void BluetoothActivity::stopAdvertising() { NimBLEDevice::stopAdvertising(); }
|
||||||
|
|
||||||
|
void BluetoothActivity::onEnter() {
|
||||||
|
Activity::onEnter();
|
||||||
|
|
||||||
|
NimBLEDevice::init(DEVICE_NAME);
|
||||||
|
NimBLEServer* pServer = NimBLEDevice::createServer();
|
||||||
|
pServer->setCallbacks(&serverCallbacks, false);
|
||||||
|
NimBLEService* pService = pServer->createService(SERVICE_UUID);
|
||||||
|
NimBLECharacteristic* pRequestChar =
|
||||||
|
pService->createCharacteristic(REQUEST_CHARACTERISTIC_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR);
|
||||||
|
pRequestChar->setCallbacks(&requestCallbacks);
|
||||||
|
pResponseChar = pService->createCharacteristic(RESPONSE_CHARACTERISTIC_UUID, NIMBLE_PROPERTY::INDICATE);
|
||||||
|
pService->start();
|
||||||
|
|
||||||
|
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
|
||||||
|
pAdvertising->setName(DEVICE_NAME);
|
||||||
|
pAdvertising->addServiceUUID(pService->getUUID());
|
||||||
|
pAdvertising->enableScanResponse(true);
|
||||||
|
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
state = STATE_INITIALIZING;
|
||||||
|
intoState(STATE_WAITING);
|
||||||
|
|
||||||
|
xTaskCreate(&BluetoothActivity::displayTaskTrampoline, "BluetoothTask",
|
||||||
|
// TODO: figure out how much stack we actually need
|
||||||
|
4096, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::intoState(State newState) {
|
||||||
|
if (state == newState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (newState) {
|
||||||
|
case STATE_WAITING:
|
||||||
|
file.close();
|
||||||
|
startAdvertising();
|
||||||
|
txnId = 0;
|
||||||
|
break;
|
||||||
|
case STATE_OFFERED:
|
||||||
|
// caller sets filename, totalBytes, file, txnId
|
||||||
|
receivedBytes = 0;
|
||||||
|
break;
|
||||||
|
case STATE_DONE:
|
||||||
|
// we cannot call onFileReceived here directly because it might cause onExit to be called,
|
||||||
|
// which calls NimBLEDevice::deinit, which cannot be called from inside a NimBLE callback.
|
||||||
|
xTaskCreate(&BluetoothActivity::reportTaskTrampoline, "BluetoothReportTask",
|
||||||
|
2048, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority,
|
||||||
|
nullptr);
|
||||||
|
break;
|
||||||
|
case STATE_ERROR: {
|
||||||
|
// caller sets errorMessage
|
||||||
|
file.close();
|
||||||
|
NimBLEServer* pServer = NimBLEDevice::getServer();
|
||||||
|
if (pServer != nullptr && pServer->getConnectedCount() > 0) {
|
||||||
|
// TODO: send back a response over BLE?
|
||||||
|
pServer->disconnect(pServer->getPeerInfo(0));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = newState;
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::onExit() {
|
||||||
|
Activity::onExit();
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
stopAdvertising();
|
||||||
|
|
||||||
|
NimBLEDevice::deinit(true);
|
||||||
|
|
||||||
|
// Wait until not rendering to delete task
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::loop() {
|
||||||
|
// Handle back button - cancel
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == STATE_ERROR || state == STATE_DONE) {
|
||||||
|
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
|
||||||
|
// restart
|
||||||
|
intoState(STATE_WAITING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
render();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::render() const {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Bluetooth", true, EpdFontFamily::BOLD);
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, 50, "Use the Longform app to transfer files.");
|
||||||
|
|
||||||
|
std::string stateText;
|
||||||
|
switch (state) {
|
||||||
|
case STATE_WAITING:
|
||||||
|
stateText = "Waiting for a connection.";
|
||||||
|
break;
|
||||||
|
case STATE_CONNECTED:
|
||||||
|
stateText = "Connected.";
|
||||||
|
break;
|
||||||
|
case STATE_OFFERED:
|
||||||
|
stateText = "Ready to receive.";
|
||||||
|
break;
|
||||||
|
case STATE_RECEIVING:
|
||||||
|
stateText = "Receiving.";
|
||||||
|
break;
|
||||||
|
case STATE_DONE:
|
||||||
|
stateText = "Transfer complete.";
|
||||||
|
break;
|
||||||
|
case STATE_ERROR:
|
||||||
|
stateText = "An error occurred.";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
stateText = "UNKNOWN STATE.";
|
||||||
|
}
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, 75, stateText.c_str());
|
||||||
|
|
||||||
|
if (state == STATE_OFFERED || state == STATE_RECEIVING || state == STATE_DONE) {
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 110, filename.c_str());
|
||||||
|
} else if (state == STATE_ERROR) {
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, 110, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == STATE_RECEIVING) {
|
||||||
|
const int percent = (totalBytes > 0) ? (receivedBytes * 100) / totalBytes : 0;
|
||||||
|
|
||||||
|
const int barWidth = renderer.getScreenWidth() * 3 / 4;
|
||||||
|
const int barHeight = 20;
|
||||||
|
const int boxX = (renderer.getScreenWidth() - barWidth) / 2;
|
||||||
|
const int boxY = 160;
|
||||||
|
renderer.drawRect(boxX, boxY, barWidth, barHeight);
|
||||||
|
const int fillWidth = (barWidth - 2) * percent / 100;
|
||||||
|
renderer.fillRect(boxX + 1, boxY + 1, fillWidth, barHeight - 2);
|
||||||
|
|
||||||
|
char dynamicText[64];
|
||||||
|
snprintf(dynamicText, sizeof(dynamicText), "Received %zu / %zu bytes (%d%%)", receivedBytes, totalBytes, percent);
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, 200, dynamicText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw help text at bottom
|
||||||
|
const auto labels =
|
||||||
|
mappedInput.mapLabels("« Back", (state == STATE_ERROR || state == STATE_DONE) ? "Restart" : "", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::ServerCallbacks::onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo) {
|
||||||
|
Serial.printf("[%lu] [BT] connected\n", millis());
|
||||||
|
activity->onConnected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::ServerCallbacks::onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason) {
|
||||||
|
Serial.printf("[%lu] [BT] disconnected\n", millis());
|
||||||
|
activity->onConnected(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::onConnected(bool isConnected) {
|
||||||
|
if (state == STATE_ERROR || state == STATE_DONE) {
|
||||||
|
// stay in error state so the user can read the error message even after disconnect.
|
||||||
|
// stay in done state so the user can see the transfer complete message.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
intoState(isConnected ? STATE_CONNECTED : STATE_WAITING);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::onRequest(const lfbt_message* msg, size_t msg_len) {
|
||||||
|
if (state == STATE_ERROR) {
|
||||||
|
// ignore further messages in error state
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PROTOCOL_ASSERT((txnId == 0) || (txnId == msg->txnId), "Multiple transfers happening at once (%x != %x)", txnId,
|
||||||
|
msg->txnId);
|
||||||
|
|
||||||
|
switch (msg->type) {
|
||||||
|
case 0: // client_offer
|
||||||
|
{
|
||||||
|
PROTOCOL_ASSERT(state == STATE_CONNECTED, "Invalid state for client_offer: %d", state);
|
||||||
|
PROTOCOL_ASSERT(msg->body.clientOffer.version == 1, "Unsupported protocol version: %u",
|
||||||
|
msg->body.clientOffer.version);
|
||||||
|
|
||||||
|
totalBytes = msg->body.clientOffer.bodyLength;
|
||||||
|
|
||||||
|
size_t filenameLength = msg_len - 8 - sizeof(lfbt_msg_client_offer);
|
||||||
|
std::string originalFilename =
|
||||||
|
StringUtils::sanitizeFilename(std::string(msg->body.clientOffer.name, filenameLength), MAX_FILENAME_LENGTH);
|
||||||
|
|
||||||
|
PROTOCOL_ASSERT(SdMan.ensureDirectoryExists(OUTPUT_DIRECTORY), "Couldn't create output directory %s",
|
||||||
|
OUTPUT_DIRECTORY);
|
||||||
|
|
||||||
|
// generate unique filepath
|
||||||
|
auto splitName = StringUtils::splitFileName(originalFilename);
|
||||||
|
filename = originalFilename;
|
||||||
|
std::string filepath = OUTPUT_DIRECTORY "/" + filename;
|
||||||
|
uint32_t duplicateIndex = 0;
|
||||||
|
while (SdMan.exists(filepath.c_str())) {
|
||||||
|
duplicateIndex++;
|
||||||
|
if (splitName.second.empty()) {
|
||||||
|
// no extension
|
||||||
|
filename = splitName.first + "-" + std::to_string(duplicateIndex);
|
||||||
|
} else {
|
||||||
|
filename = splitName.first + "-" + std::to_string(duplicateIndex) + splitName.second;
|
||||||
|
}
|
||||||
|
filepath = OUTPUT_DIRECTORY "/" + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
PROTOCOL_ASSERT(SdMan.openFileForWrite("BT", filepath, file), "Couldn't open file %s for writing",
|
||||||
|
filepath.c_str());
|
||||||
|
// TODO: would be neat to check if we have enough space, but SDCardManager doesn't seem to expose that info
|
||||||
|
// currently
|
||||||
|
|
||||||
|
txnId = msg->txnId;
|
||||||
|
|
||||||
|
intoState(STATE_OFFERED);
|
||||||
|
|
||||||
|
lfbt_message response = {.type = 1, // server_response
|
||||||
|
.txnId = txnId,
|
||||||
|
.body = {.serverResponse = {.status = 0}}};
|
||||||
|
pResponseChar->setValue(reinterpret_cast<uint8_t*>(&response), 8 + sizeof(lfbt_msg_server_response));
|
||||||
|
pResponseChar->indicate();
|
||||||
|
|
||||||
|
updateRequired = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: // client_chunk
|
||||||
|
{
|
||||||
|
Serial.printf("[%lu] [BT] Received client_chunk, offset %u, length %zu\n", millis(), msg->body.clientChunk.offset,
|
||||||
|
msg_len - 8 - sizeof(lfbt_msg_client_chunk));
|
||||||
|
PROTOCOL_ASSERT(state == STATE_OFFERED || state == STATE_RECEIVING, "Invalid state for client_chunk: %d", state);
|
||||||
|
PROTOCOL_ASSERT(msg->body.clientChunk.offset == receivedBytes, "Expected chunk %zu, got %u", receivedBytes,
|
||||||
|
msg->body.clientChunk.offset);
|
||||||
|
|
||||||
|
size_t written = file.write(reinterpret_cast<const uint8_t*>(msg->body.clientChunk.body),
|
||||||
|
msg_len - 8 - sizeof(lfbt_msg_client_chunk));
|
||||||
|
PROTOCOL_ASSERT(written > 0, "Couldn't write to file");
|
||||||
|
receivedBytes += msg_len - 8 - sizeof(lfbt_msg_client_chunk);
|
||||||
|
if (receivedBytes >= totalBytes) {
|
||||||
|
PROTOCOL_ASSERT(receivedBytes == totalBytes, "Got more bytes than expected: %zu > %zu", receivedBytes,
|
||||||
|
totalBytes);
|
||||||
|
PROTOCOL_ASSERT(file.close(), "Couldn't finalize writing the file");
|
||||||
|
intoState(STATE_DONE);
|
||||||
|
} else {
|
||||||
|
intoState(STATE_RECEIVING);
|
||||||
|
}
|
||||||
|
updateRequired = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BluetoothActivity::RequestCallbacks::onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) {
|
||||||
|
const lfbt_message* msg = reinterpret_cast<const lfbt_message*>(pCharacteristic->getValue().data());
|
||||||
|
Serial.printf("[%lu] [BT] Received BLE message of type %u, txnId %x, length %d\n", millis(), msg->type, msg->txnId,
|
||||||
|
pCharacteristic->getValue().length());
|
||||||
|
activity->onRequest(msg, pCharacteristic->getValue().length());
|
||||||
|
}
|
||||||
126
src/activities/bluetooth/BluetoothActivity.h
Normal file
126
src/activities/bluetooth/BluetoothActivity.h
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
#include <NimBLEServer.h>
|
||||||
|
#include <NimBLEUtils.h>
|
||||||
|
#include <SDCardManager.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "../Activity.h"
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint32_t version;
|
||||||
|
uint32_t bodyLength;
|
||||||
|
uint32_t nameLength;
|
||||||
|
char name[];
|
||||||
|
} lfbt_msg_client_offer; // msg type 0
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint32_t status;
|
||||||
|
} lfbt_msg_server_response; // msg type 1
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint32_t offset;
|
||||||
|
char body[];
|
||||||
|
} lfbt_msg_client_chunk; // msg type 2
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
lfbt_msg_client_offer clientOffer;
|
||||||
|
lfbt_msg_server_response serverResponse;
|
||||||
|
lfbt_msg_client_chunk clientChunk;
|
||||||
|
} lfbt_message_body;
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint32_t type;
|
||||||
|
uint32_t txnId;
|
||||||
|
lfbt_message_body body;
|
||||||
|
} lfbt_message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BluetoothActivity receives files over a custom BLE protocol and stores them on the SD card.
|
||||||
|
*
|
||||||
|
* The onCancel callback is called if the user presses back.
|
||||||
|
* onFileReceived is called when a file is successfully received with the path to the file.
|
||||||
|
*/
|
||||||
|
class BluetoothActivity final : public Activity {
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
bool updateRequired = false;
|
||||||
|
const std::function<void()> onCancel;
|
||||||
|
const std::function<void(const std::string&)> onFileReceived;
|
||||||
|
|
||||||
|
static void displayTaskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void render() const;
|
||||||
|
|
||||||
|
static void reportTaskTrampoline(void* param);
|
||||||
|
void report();
|
||||||
|
|
||||||
|
void onConnected(bool isConnected);
|
||||||
|
void onRequest(const lfbt_message* msg, size_t msg_len);
|
||||||
|
|
||||||
|
class ServerCallbacks : public NimBLEServerCallbacks {
|
||||||
|
friend class BluetoothActivity;
|
||||||
|
BluetoothActivity* activity;
|
||||||
|
|
||||||
|
void onConnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo);
|
||||||
|
void onDisconnect(NimBLEServer* pServer, NimBLEConnInfo& connInfo, int reason);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
explicit ServerCallbacks(BluetoothActivity* activity) : activity(activity) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
ServerCallbacks serverCallbacks;
|
||||||
|
|
||||||
|
class RequestCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
|
friend class BluetoothActivity;
|
||||||
|
BluetoothActivity* activity;
|
||||||
|
|
||||||
|
void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
explicit RequestCallbacks(BluetoothActivity* activity) : activity(activity) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
RequestCallbacks requestCallbacks;
|
||||||
|
|
||||||
|
NimBLECharacteristic* pResponseChar = nullptr;
|
||||||
|
void startAdvertising();
|
||||||
|
void stopAdvertising();
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
STATE_INITIALIZING,
|
||||||
|
STATE_WAITING,
|
||||||
|
STATE_CONNECTED,
|
||||||
|
STATE_OFFERED,
|
||||||
|
STATE_RECEIVING,
|
||||||
|
STATE_DONE,
|
||||||
|
STATE_ERROR
|
||||||
|
} State;
|
||||||
|
|
||||||
|
State state = STATE_INITIALIZING;
|
||||||
|
std::string filename;
|
||||||
|
FsFile file;
|
||||||
|
size_t receivedBytes = 0;
|
||||||
|
size_t totalBytes = 0;
|
||||||
|
char errorMessage[256] = {};
|
||||||
|
uint32_t txnId = 0;
|
||||||
|
|
||||||
|
void intoState(State newState);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit BluetoothActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const std::function<void()>& onCancel,
|
||||||
|
const std::function<void(const std::string&)>& onFileReceived)
|
||||||
|
: Activity("Bluetooth", renderer, mappedInput),
|
||||||
|
onCancel(onCancel),
|
||||||
|
onFileReceived(onFileReceived),
|
||||||
|
serverCallbacks(this),
|
||||||
|
requestCallbacks(this) {}
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
};
|
||||||
@ -23,7 +23,7 @@ void HomeActivity::taskTrampoline(void* param) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int HomeActivity::getMenuItemCount() const {
|
int HomeActivity::getMenuItemCount() const {
|
||||||
int count = 3; // My Library, File transfer, Settings
|
int count = 4; // My Library, File transfer, Bluetooth, Settings
|
||||||
if (hasContinueReading) count++;
|
if (hasContinueReading) count++;
|
||||||
if (hasOpdsUrl) count++;
|
if (hasOpdsUrl) count++;
|
||||||
return count;
|
return count;
|
||||||
@ -175,6 +175,7 @@ void HomeActivity::loop() {
|
|||||||
const int myLibraryIdx = idx++;
|
const int myLibraryIdx = idx++;
|
||||||
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
|
const int opdsLibraryIdx = hasOpdsUrl ? idx++ : -1;
|
||||||
const int fileTransferIdx = idx++;
|
const int fileTransferIdx = idx++;
|
||||||
|
const int bluetoothIdx = idx++;
|
||||||
const int settingsIdx = idx;
|
const int settingsIdx = idx;
|
||||||
|
|
||||||
if (selectorIndex == continueIdx) {
|
if (selectorIndex == continueIdx) {
|
||||||
@ -185,6 +186,8 @@ void HomeActivity::loop() {
|
|||||||
onOpdsBrowserOpen();
|
onOpdsBrowserOpen();
|
||||||
} else if (selectorIndex == fileTransferIdx) {
|
} else if (selectorIndex == fileTransferIdx) {
|
||||||
onFileTransferOpen();
|
onFileTransferOpen();
|
||||||
|
} else if (selectorIndex == bluetoothIdx) {
|
||||||
|
onBluetoothOpen();
|
||||||
} else if (selectorIndex == settingsIdx) {
|
} else if (selectorIndex == settingsIdx) {
|
||||||
onSettingsOpen();
|
onSettingsOpen();
|
||||||
}
|
}
|
||||||
@ -503,7 +506,7 @@ void HomeActivity::render() {
|
|||||||
|
|
||||||
// --- Bottom menu tiles ---
|
// --- Bottom menu tiles ---
|
||||||
// Build menu items dynamically
|
// Build menu items dynamically
|
||||||
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"};
|
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Bluetooth", "Settings"};
|
||||||
if (hasOpdsUrl) {
|
if (hasOpdsUrl) {
|
||||||
// Insert OPDS Browser after My Library
|
// Insert OPDS Browser after My Library
|
||||||
menuItems.insert(menuItems.begin() + 1, "OPDS Browser");
|
menuItems.insert(menuItems.begin() + 1, "OPDS Browser");
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class HomeActivity final : public Activity {
|
|||||||
const std::function<void()> onMyLibraryOpen;
|
const std::function<void()> onMyLibraryOpen;
|
||||||
const std::function<void()> onSettingsOpen;
|
const std::function<void()> onSettingsOpen;
|
||||||
const std::function<void()> onFileTransferOpen;
|
const std::function<void()> onFileTransferOpen;
|
||||||
|
const std::function<void()> onBluetoothOpen;
|
||||||
const std::function<void()> onOpdsBrowserOpen;
|
const std::function<void()> onOpdsBrowserOpen;
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
@ -39,12 +40,13 @@ class HomeActivity final : public Activity {
|
|||||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::function<void()>& onContinueReading, const std::function<void()>& onMyLibraryOpen,
|
const std::function<void()>& onContinueReading, const std::function<void()>& onMyLibraryOpen,
|
||||||
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
||||||
const std::function<void()>& onOpdsBrowserOpen)
|
const std::function<void()>& onBluetoothOpen, const std::function<void()>& onOpdsBrowserOpen)
|
||||||
: Activity("Home", renderer, mappedInput),
|
: Activity("Home", renderer, mappedInput),
|
||||||
onContinueReading(onContinueReading),
|
onContinueReading(onContinueReading),
|
||||||
onMyLibraryOpen(onMyLibraryOpen),
|
onMyLibraryOpen(onMyLibraryOpen),
|
||||||
onSettingsOpen(onSettingsOpen),
|
onSettingsOpen(onSettingsOpen),
|
||||||
onFileTransferOpen(onFileTransferOpen),
|
onFileTransferOpen(onFileTransferOpen),
|
||||||
|
onBluetoothOpen(onBluetoothOpen),
|
||||||
onOpdsBrowserOpen(onOpdsBrowserOpen) {}
|
onOpdsBrowserOpen(onOpdsBrowserOpen) {}
|
||||||
void onEnter() override;
|
void onEnter() override;
|
||||||
void onExit() override;
|
void onExit() override;
|
||||||
|
|||||||
14
src/main.cpp
14
src/main.cpp
@ -15,6 +15,7 @@
|
|||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
|
#include "activities/bluetooth/BluetoothActivity.h"
|
||||||
#include "activities/boot_sleep/BootActivity.h"
|
#include "activities/boot_sleep/BootActivity.h"
|
||||||
#include "activities/boot_sleep/SleepActivity.h"
|
#include "activities/boot_sleep/SleepActivity.h"
|
||||||
#include "activities/browser/OpdsBookBrowserActivity.h"
|
#include "activities/browser/OpdsBookBrowserActivity.h"
|
||||||
@ -25,6 +26,7 @@
|
|||||||
#include "activities/settings/SettingsActivity.h"
|
#include "activities/settings/SettingsActivity.h"
|
||||||
#include "activities/util/FullScreenMessageActivity.h"
|
#include "activities/util/FullScreenMessageActivity.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
#include "util/StringUtils.h"
|
||||||
|
|
||||||
HalDisplay display;
|
HalDisplay display;
|
||||||
HalGPIO gpio;
|
HalGPIO gpio;
|
||||||
@ -216,6 +218,16 @@ void onGoToFileTransfer() {
|
|||||||
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onGoToBluetooth() {
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new BluetoothActivity(renderer, mappedInputManager, onGoHome, [](const std::string& filepath) {
|
||||||
|
Serial.printf("[%lu] [ ] File received over Bluetooth: %s\n", millis(), filepath.c_str());
|
||||||
|
if (StringUtils::readableFileExtension(filepath)) {
|
||||||
|
onGoToReader(filepath, MyLibraryActivity::Tab::Recent);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
void onGoToSettings() {
|
void onGoToSettings() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome));
|
||||||
@ -239,7 +251,7 @@ void onGoToBrowser() {
|
|||||||
void onGoHome() {
|
void onGoHome() {
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings,
|
enterNewActivity(new HomeActivity(renderer, mappedInputManager, onContinueReading, onGoToMyLibrary, onGoToSettings,
|
||||||
onGoToFileTransfer, onGoToBrowser));
|
onGoToFileTransfer, onGoToBluetooth, onGoToBrowser));
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupDisplayAndFonts() {
|
void setupDisplayAndFonts() {
|
||||||
|
|||||||
@ -61,6 +61,19 @@ bool checkFileExtension(const String& fileName, const char* extension) {
|
|||||||
return localFile.endsWith(localExtension);
|
return localFile.endsWith(localExtension);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool readableFileExtension(const std::string& fileName) {
|
||||||
|
return (StringUtils::checkFileExtension(fileName, ".epub") || StringUtils::checkFileExtension(fileName, ".xtch") ||
|
||||||
|
StringUtils::checkFileExtension(fileName, ".xtc") || StringUtils::checkFileExtension(fileName, ".txt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<std::string, std::string> splitFileName(const std::string& name) {
|
||||||
|
size_t lastDot = name.find_last_of('.');
|
||||||
|
if (lastDot == std::string::npos) {
|
||||||
|
return std::make_pair(name, "");
|
||||||
|
}
|
||||||
|
return std::make_pair(name.substr(0, lastDot), name.substr(lastDot));
|
||||||
|
}
|
||||||
|
|
||||||
size_t utf8RemoveLastChar(std::string& str) {
|
size_t utf8RemoveLastChar(std::string& str) {
|
||||||
if (str.empty()) return 0;
|
if (str.empty()) return 0;
|
||||||
size_t pos = str.size() - 1;
|
size_t pos = str.size() - 1;
|
||||||
|
|||||||
@ -19,6 +19,17 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100);
|
|||||||
bool checkFileExtension(const std::string& fileName, const char* extension);
|
bool checkFileExtension(const std::string& fileName, const char* extension);
|
||||||
bool checkFileExtension(const String& fileName, const char* extension);
|
bool checkFileExtension(const String& fileName, const char* extension);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given filename ends with an extension we can open.
|
||||||
|
*/
|
||||||
|
bool readableFileExtension(const std::string& fileName);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a filename into base name and extension.
|
||||||
|
* If there is no extension, the second element of the pair will be an empty string.
|
||||||
|
*/
|
||||||
|
std::pair<std::string, std::string> splitFileName(const std::string& name);
|
||||||
|
|
||||||
// UTF-8 safe string truncation - removes one character from the end
|
// UTF-8 safe string truncation - removes one character from the end
|
||||||
// Returns the new size after removing one UTF-8 character
|
// Returns the new size after removing one UTF-8 character
|
||||||
size_t utf8RemoveLastChar(std::string& str);
|
size_t utf8RemoveLastChar(std::string& str);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user