/* * Copyright 2023 jacqueline * * SPDX-License-Identifier: GPL-3.0-only */ #include "app_console/app_console.hpp" #include "audio/audio_events.hpp" #include "database/database.hpp" #include "database/db_events.hpp" #include "drivers/gpios.hpp" #include "drivers/spi.hpp" #include "ff.h" #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "result.hpp" #include "audio/audio_fsm.hpp" #include "drivers/storage.hpp" #include "events/event_queue.hpp" #include "system_fsm/system_events.hpp" #include "system_fsm/system_fsm.hpp" #include "ui/ui_fsm.hpp" namespace system_fsm { namespace states { [[maybe_unused]] static const char kTag[] = "RUN"; static const TickType_t kTicksBeforeUnmount = pdMS_TO_TICKS(120000); static TimerHandle_t sUnmountTimer = nullptr; static void timer_callback(TimerHandle_t timer) { events::System().Dispatch(internal::UnmountTimeout{}); } void Running::entry() { if (!sUnmountTimer) { sUnmountTimer = xTimerCreate("unmount_timeout", kTicksBeforeUnmount, false, NULL, timer_callback); } events::System().Dispatch(internal::Mount{.attempt = 1}); } void Running::exit() { unmountStorage(); } void Running::react(const KeyLockChanged& ev) { checkIdle(); } void Running::react(const audio::PlaybackUpdate& ev) { checkIdle(); } void Running::react(const database::event::UpdateFinished&) { checkIdle(); } void Running::react(const ui::UnmountRequest& ev) { events::Audio().Dispatch(audio::TogglePlayPause{.set_to = false}); events::Audio().Dispatch(UnmountRequest{.idle = false}); } void Running::react(const audio::UnmountReady& ev) { if (ev.idle) { transit(); } else { unmountStorage(); } } void Running::react(const internal::UnmountTimeout&) { if (IdleCondition()) { events::Audio().Dispatch(UnmountRequest{.idle = true}); } } void Running::react(const SdDetectChanged& ev) { if (sServices->samd().UsbMassStorage()) { // We don't currently control the sd card, so don't mess with it. return; } if (ev.has_sd_card && !sStorage) { events::System().Dispatch(internal::Mount{.attempt = 1}); return; } // Don't automatically unmount, since this event seems to occasionally happen // supriously. FIXME: Why? // Instead, check whether or not the card has actually gone away. if (sStorage) { FF_DIR dir; FRESULT res = f_opendir(&dir, "/"); if (res != FR_OK) { ESP_LOGW(kTag, "sd card ejected unsafely!"); unmountStorage(); } f_closedir(&dir); } } void Running::react(const SamdUsbMscChanged& ev) { if (ev.en) { // Stop using the sd card, and power it off. unmountStorage(); // Set up the SD card for usage by the samd21. auto& gpios = sServices->gpios(); gpios.WriteSync(drivers::IGpios::Pin::kSdPowerEnable, 1); gpios.SdMuxTarget(drivers::IGpios::SD_MUX_SAMD); // Off you go! sServices->samd().UsbMassStorage(true); } else { // Make sure the samd knows that its access is going away, and give it time // to finish up any remaining work. sServices->samd().UsbMassStorage(false); vTaskDelay(pdMS_TO_TICKS(250)); auto& gpios = sServices->gpios(); // No more writing, please! gpios.SdMuxTarget(drivers::IGpios::SD_MUX_ESP); vTaskDelay(pdMS_TO_TICKS(100)); // Reboot the SD card so that it comes up in a consistent state. // TODO: can we avoid doing this? gpios.WriteSync(drivers::IGpios::Pin::kSdPowerEnable, 0); // Now it's ready for us. events::System().Dispatch(internal::Mount{.attempt = 1}); } } void Running::react(const StorageError& ev) { ESP_LOGE(kTag, "storage error %u", ev.error); if (ev.error == FR_DISK_ERR || ev.error == FR_INVALID_OBJECT) { unmountStorage(); } } auto Running::checkIdle() -> void { xTimerStop(sUnmountTimer, portMAX_DELAY); if (IdleCondition()) { xTimerStart(sUnmountTimer, portMAX_DELAY); } } auto Running::updateSdState(drivers::SdState state) -> void { sServices->sd(state); events::Ui().Dispatch(SdStateChanged{}); events::Audio().Dispatch(SdStateChanged{}); events::System().Dispatch(SdStateChanged{}); } void Running::react(const internal::Mount& ev) { // Only mount our storage if we know it's not currently in use by the SAMD. if (sServices->samd().UsbMassStorage()) { updateSdState(drivers::SdState::kNotMounted); return; } if (ev.attempt > 3) { updateSdState(drivers::SdState::kNotPresent); return; } ESP_LOGI(kTag, "mounting sd card"); auto storage_res = drivers::SdStorage::Create(sServices->gpios()); if (storage_res.has_error()) { ESP_LOGW(kTag, "failed to mount!"); switch (storage_res.error()) { case drivers::SdStorage::FAILED_TO_MOUNT: updateSdState(drivers::SdState::kNotFormatted); break; case drivers::SdStorage::FAILED_TO_READ: default: if (!sServices->gpios().Get(drivers::Gpios::Pin::kSdCardDetect)) { vTaskDelay(pdMS_TO_TICKS(100)); events::System().Dispatch(internal::Mount{.attempt = ev.attempt + 1}); } else { updateSdState(drivers::SdState::kNotPresent); } break; } return; } sStorage.reset(storage_res.value()); ESP_LOGI(kTag, "opening database"); auto database_res = database::Database::Open( sServices->tag_parser(), sServices->collator(), sServices->bg_worker()); if (database_res.has_error()) { unmountStorage(); return; } sServices->database( std::unique_ptr{database_res.value()}); ESP_LOGI(kTag, "storage loaded okay"); updateSdState(drivers::SdState::kMounted); // Tell the database to refresh so that we pick up any changes from the newly // mounted card. if (sServices->nvs().DbAutoIndex()) { sServices->bg_worker().Dispatch([&]() { // Delay the index update for a bit, since we don't want to cause a lot // of disk contention immediately after mounting (especially when we've // just booted), or else we risk slowing down stuff like UI loading. vTaskDelay(pdMS_TO_TICKS(6000)); auto db = sServices->database().lock(); if (!db) { return; } db->updateIndexes(); }); } } auto Running::unmountStorage() -> void { ESP_LOGW(kTag, "unmounting storage"); sServices->track_queue().close(); sServices->database({}); sStorage.reset(); updateSdState(drivers::SdState::kNotMounted); } } // namespace states } // namespace system_fsm