mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
343 lines
11 KiB
C++
343 lines
11 KiB
C++
#include "cardboy/backend/esp_backend.hpp"
|
|
|
|
#include "cardboy/backend/esp/bat_mon.hpp"
|
|
#include "cardboy/backend/esp/buttons.hpp"
|
|
#include "cardboy/backend/esp/buzzer.hpp"
|
|
#include "cardboy/backend/esp/config.hpp"
|
|
#include "cardboy/backend/esp/display.hpp"
|
|
#include "cardboy/backend/esp/fs_helper.hpp"
|
|
#include "cardboy/backend/esp/i2c_global.hpp"
|
|
#include "cardboy/backend/esp/shutdowner.hpp"
|
|
#include "cardboy/backend/esp/spi_global.hpp"
|
|
#include "cardboy/backend/esp/time_sync_service.hpp"
|
|
|
|
#include "cardboy/sdk/display_spec.hpp"
|
|
|
|
#include "driver/gpio.h"
|
|
#include "esp_err.h"
|
|
#include "esp_random.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/task.h"
|
|
#include "nvs.h"
|
|
#include "nvs_flash.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <ctime>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace cardboy::backend::esp {
|
|
|
|
namespace {
|
|
void ensureNvsInit() {
|
|
static bool nvsReady = false;
|
|
if (nvsReady)
|
|
return;
|
|
|
|
esp_err_t err = nvs_flash_init();
|
|
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
printf("Erasing flash!\n");
|
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
|
err = nvs_flash_init();
|
|
}
|
|
ESP_ERROR_CHECK(err);
|
|
nvsReady = true;
|
|
}
|
|
} // namespace
|
|
|
|
class EspRuntime::BuzzerService final : public cardboy::sdk::IBuzzer {
|
|
public:
|
|
void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) override {
|
|
Buzzer::get().tone(freq, duration_ms, gap_ms);
|
|
}
|
|
|
|
void beepRotate() override { Buzzer::get().beepRotate(); }
|
|
void beepMove() override { Buzzer::get().beepMove(); }
|
|
void beepLock() override { Buzzer::get().beepLock(); }
|
|
void beepLines(int lines) override { Buzzer::get().beepLines(lines); }
|
|
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
|
|
void beepGameOver() override { Buzzer::get().beepGameOver(); }
|
|
|
|
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
|
|
void toggleMuted() override { Buzzer::get().toggleMuted(); }
|
|
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
|
|
};
|
|
|
|
class EspRuntime::BatteryService final : public cardboy::sdk::IBatteryMonitor {
|
|
public:
|
|
[[nodiscard]] bool hasData() const override { return true; }
|
|
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
|
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
|
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
|
};
|
|
|
|
class EspRuntime::StorageService final : public cardboy::sdk::IStorage {
|
|
public:
|
|
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
|
ensureNvsInit();
|
|
nvs_handle_t handle;
|
|
std::string nsStr(ns);
|
|
std::string keyStr(key);
|
|
if (nvs_open(nsStr.c_str(), NVS_READONLY, &handle) != ESP_OK)
|
|
return false;
|
|
std::uint32_t value = 0;
|
|
esp_err_t err = nvs_get_u32(handle, keyStr.c_str(), &value);
|
|
nvs_close(handle);
|
|
if (err != ESP_OK)
|
|
return false;
|
|
out = value;
|
|
return true;
|
|
}
|
|
|
|
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
|
ensureNvsInit();
|
|
nvs_handle_t handle;
|
|
std::string nsStr(ns);
|
|
std::string keyStr(key);
|
|
if (nvs_open(nsStr.c_str(), NVS_READWRITE, &handle) != ESP_OK)
|
|
return;
|
|
nvs_set_u32(handle, keyStr.c_str(), value);
|
|
nvs_commit(handle);
|
|
nvs_close(handle);
|
|
}
|
|
};
|
|
|
|
class EspRuntime::RandomService final : public cardboy::sdk::IRandom {
|
|
public:
|
|
[[nodiscard]] std::uint32_t nextUint32() override { return esp_random(); }
|
|
};
|
|
|
|
class EspRuntime::HighResClockService final : public cardboy::sdk::IHighResClock {
|
|
public:
|
|
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
|
|
};
|
|
|
|
class EspRuntime::FilesystemService final : public cardboy::sdk::IFilesystem {
|
|
public:
|
|
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
|
|
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
|
|
[[nodiscard]] std::string basePath() const override {
|
|
const char* path = FsHelper::get().basePath();
|
|
return path ? std::string(path) : std::string{};
|
|
}
|
|
};
|
|
|
|
class EspRuntime::LoopHooksService final : public cardboy::sdk::ILoopHooks {
|
|
public:
|
|
void onLoopIteration() override { vTaskDelay(1); }
|
|
};
|
|
|
|
class EspRuntime::NotificationService final : public cardboy::sdk::INotificationCenter {
|
|
public:
|
|
void pushNotification(Notification notification) override {
|
|
if (notification.timestamp == 0) {
|
|
notification.timestamp = static_cast<std::uint64_t>(std::time(nullptr));
|
|
}
|
|
|
|
capLengths(notification);
|
|
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
if (notification.externalId != 0) {
|
|
for (auto it = entries.begin(); it != entries.end();) {
|
|
if (it->externalId == notification.externalId)
|
|
it = entries.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
notification.id = nextId++;
|
|
notification.unread = true;
|
|
|
|
entries.push_back(std::move(notification));
|
|
if (entries.size() > kMaxEntries)
|
|
entries.erase(entries.begin());
|
|
++revisionCounter;
|
|
}
|
|
|
|
[[nodiscard]] std::uint32_t revision() const override {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
return revisionCounter;
|
|
}
|
|
|
|
[[nodiscard]] std::vector<Notification> recent(std::size_t limit) const override {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
std::vector<Notification> out;
|
|
const std::size_t count = std::min<std::size_t>(limit, entries.size());
|
|
out.reserve(count);
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
out.push_back(entries[entries.size() - 1 - i]);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void markAllRead() override {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
bool changed = false;
|
|
for (auto& entry: entries) {
|
|
if (entry.unread) {
|
|
entry.unread = false;
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed)
|
|
++revisionCounter;
|
|
}
|
|
|
|
void clear() override {
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
if (entries.empty())
|
|
return;
|
|
entries.clear();
|
|
++revisionCounter;
|
|
}
|
|
|
|
void removeByExternalId(std::uint64_t externalId) override {
|
|
if (externalId == 0)
|
|
return;
|
|
std::lock_guard<std::mutex> lock(mutex);
|
|
bool removed = false;
|
|
for (auto it = entries.begin(); it != entries.end();) {
|
|
if (it->externalId == externalId) {
|
|
it = entries.erase(it);
|
|
removed = true;
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
if (removed)
|
|
++revisionCounter;
|
|
}
|
|
|
|
private:
|
|
static constexpr std::size_t kMaxEntries = 8;
|
|
static constexpr std::size_t kMaxTitleBytes = 96;
|
|
static constexpr std::size_t kMaxBodyBytes = 256;
|
|
|
|
static void capLengths(Notification& notification) {
|
|
if (notification.title.size() > kMaxTitleBytes)
|
|
notification.title.resize(kMaxTitleBytes);
|
|
if (notification.body.size() > kMaxBodyBytes)
|
|
notification.body.resize(kMaxBodyBytes);
|
|
}
|
|
|
|
mutable std::mutex mutex;
|
|
std::vector<Notification> entries;
|
|
std::uint64_t nextId = 1;
|
|
std::uint32_t revisionCounter = 0;
|
|
};
|
|
|
|
EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
|
|
initializeHardware();
|
|
|
|
buzzerService = std::make_unique<BuzzerService>();
|
|
batteryService = std::make_unique<BatteryService>();
|
|
storageService = std::make_unique<StorageService>();
|
|
randomService = std::make_unique<RandomService>();
|
|
highResClockService = std::make_unique<HighResClockService>();
|
|
filesystemService = std::make_unique<FilesystemService>();
|
|
eventBus = std::make_unique<EventBus>();
|
|
loopHooksService = std::make_unique<LoopHooksService>();
|
|
notificationService = std::make_unique<NotificationService>();
|
|
|
|
services.buzzer = buzzerService.get();
|
|
services.battery = batteryService.get();
|
|
services.storage = storageService.get();
|
|
services.random = randomService.get();
|
|
services.highResClock = highResClockService.get();
|
|
services.filesystem = filesystemService.get();
|
|
services.eventBus = eventBus.get();
|
|
services.loopHooks = loopHooksService.get();
|
|
services.notifications = notificationService.get();
|
|
|
|
Buttons::get().setEventBus(eventBus.get());
|
|
set_notification_center(notificationService.get());
|
|
}
|
|
|
|
EspRuntime::~EspRuntime() {
|
|
set_notification_center(nullptr);
|
|
shutdown_time_sync_service();
|
|
}
|
|
|
|
cardboy::sdk::Services& EspRuntime::serviceRegistry() { return services; }
|
|
|
|
void EspRuntime::initializeHardware() {
|
|
static bool initialized = false;
|
|
if (initialized)
|
|
return;
|
|
initialized = true;
|
|
|
|
ensureNvsInit();
|
|
|
|
Shutdowner::get();
|
|
Buttons::get();
|
|
|
|
esp_err_t isrErr = gpio_install_isr_service(0);
|
|
if (isrErr != ESP_OK && isrErr != ESP_ERR_INVALID_STATE) {
|
|
ESP_ERROR_CHECK(isrErr);
|
|
}
|
|
|
|
Shutdowner::get().install_isr();
|
|
Buttons::get().install_isr();
|
|
|
|
I2cGlobal::get();
|
|
BatMon::get();
|
|
SpiGlobal::get();
|
|
SMD::init();
|
|
Buzzer::get().init();
|
|
|
|
FsHelper::get().mount();
|
|
|
|
ensure_time_sync_service_started();
|
|
}
|
|
|
|
void EspFramebuffer::clear_impl(bool on) {
|
|
for (int y = 0; y < height_impl(); ++y)
|
|
for (int x = 0; x < width_impl(); ++x)
|
|
SMD::set_pixel(x, y, on);
|
|
}
|
|
|
|
void EspFramebuffer::frameReady_impl() { SMD::frame_ready(); }
|
|
|
|
void EspFramebuffer::sendFrame_impl(bool clearAfterSend) { SMD::send_frame(clearAfterSend); }
|
|
|
|
bool EspFramebuffer::frameInFlight_impl() const { return SMD::frame_transfer_in_flight(); }
|
|
|
|
cardboy::sdk::InputState EspInput::readState_impl() {
|
|
cardboy::sdk::InputState state{};
|
|
const uint8_t pressed = Buttons::get().get_pressed();
|
|
if (pressed & BTN_UP)
|
|
state.up = true;
|
|
if (pressed & BTN_LEFT)
|
|
state.left = true;
|
|
if (pressed & BTN_RIGHT)
|
|
state.right = true;
|
|
if (pressed & BTN_DOWN)
|
|
state.down = true;
|
|
if (pressed & BTN_A)
|
|
state.a = true;
|
|
if (pressed & BTN_B)
|
|
state.b = true;
|
|
if (pressed & BTN_SELECT)
|
|
state.select = true;
|
|
if (pressed & BTN_START)
|
|
state.start = true;
|
|
return state;
|
|
}
|
|
|
|
std::uint32_t EspClock::millis_impl() {
|
|
TickType_t ticks = xTaskGetTickCount();
|
|
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
|
}
|
|
|
|
void EspClock::sleep_ms_impl(std::uint32_t ms) {
|
|
if (ms == 0)
|
|
return;
|
|
vTaskDelay(pdMS_TO_TICKS(ms));
|
|
}
|
|
|
|
} // namespace cardboy::backend::esp
|