Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ set(GAME_COMPONENTS
set(
COMPONENTS
main esptool_py esp_lcd esp_psram task format display display_drivers monitor timer
${GAME_COMPONENTS} tab5-emu gui menu rom_info statistics
${GAME_COMPONENTS} tab5-emu gui menu rom_info statistics settings devlink
CACHE STRING
"List of components to include"
FORCE
Expand Down
10 changes: 10 additions & 0 deletions components/devlink/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src"
PRIV_REQUIRES esp_system
REQUIRES app_update coredump dispatcher monitor system tab5-emu)
# the two-slot OTA service: off on the Tab5, whose app does not fit twice (an
# update goes through the updater app in ota_1, see updater/)
target_compile_definitions(${COMPONENT_LIB} PUBLIC DEVLINK_HAS_OTA=0)
# keep the last panic's reason across the reset (DevLink::last_panic())
target_link_options(${COMPONENT_LIB} INTERFACE "-Wl,--wrap=esp_panic_handler" "-Wl,--wrap=panic_abort")
7 changes: 7 additions & 0 deletions components/devlink/idf_component.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## IDF Component Manager Manifest File
dependencies:
idf: '>=6.0'
espp/coredump: '>=1.4.2'
espp/dispatcher: '>=1.4.2'
espp/monitor: '>=1.4.2'
espp/system: '>=1.4.2'
112 changes: 112 additions & 0 deletions components/devlink/include/devlink.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma once

#include <memory>
#include <string>

#include "coredump.hpp"
#include "coredump_service.hpp"
#include "dispatcher_worker.hpp"
#include "monitor_service.hpp"
#include "system_service.hpp"

#include <esp_heap_caps.h>
#include <esp_system.h>

#include "box-emu.hpp"

/// Check the heap around every flash-touching request (debugging aid).
#ifndef DEVLINK_HEAP_CHECK
#define DEVLINK_HEAP_CHECK 0
#endif
#if DEVLINK_HAS_OTA
#include "ota.hpp"
#include "ota_service.hpp"
#endif

/// A module whose requests touch the flash (erase / write / read), run under
/// the launcher's LVGL lock. While the flash is busy the cache is off and only
/// interrupt handlers placed in internal RAM run; the display's SPI "transfer
/// done" interrupt is one and calls into LVGL (flash / PSRAM): a screen update
/// finishing during an OTA erase panics (esp-box-emu). LVGL waits for its
/// transfers before it lets go of the lock, so none is in flight meanwhile.
template <espp::DispatcherModuleConcept Module> class FlashGuarded {
public:
explicit FlashGuarded(Module &module) : module_(module) {}
uint8_t module_id() const { return module_.module_id(); }
espp::DispatcherModuleInfo module_info() const { return module_.module_info(); }
void handle(const espp::stream_frame::Frame &frame) {
std::lock_guard<std::recursive_mutex> lk(BoxEmu::lvgl_mutex());
#if DEVLINK_HEAP_CHECK
if (!heap_caps_check_integrity_all(true)) abort_corrupt("before", frame);
#endif
module_.handle(frame);
#if DEVLINK_HEAP_CHECK
if (!heap_caps_check_integrity_all(true)) abort_corrupt("after", frame);
#endif
}

protected:
#if DEVLINK_HEAP_CHECK
[[noreturn]] static void abort_corrupt(const char *when, const espp::stream_frame::Frame &frame) {
static char msg[80];
const bool internal_ok = heap_caps_check_integrity(MALLOC_CAP_INTERNAL, false);
const bool psram_ok = heap_caps_check_integrity(MALLOC_CAP_SPIRAM, false);
snprintf(msg, sizeof(msg), "heap corrupt %s request: internal %s, psram %s (frame @%p)", when,
internal_ok ? "ok" : "BAD", psram_ok ? "ok" : "BAD", (const void *)&frame);
esp_system_abort(msg);
}
#endif
Module &module_;
};

/// The framed services on the composite USB device's vendor interface, one
/// espp::DispatcherWorker routing them (shared by esp-box-emu and tab5-emu):
///
/// | module | service | host tool |
/// |--------|-------------------------------------------|--------------------------|
/// | 0 | OTA (DEVLINK_HAS_OTA: a two-slot layout) | espp_ota, emulink ota |
/// | 4 | core dump of the last crash | espp_coredump, emulink |
/// | 7 | system info, reboot, ROM download mode | emulink reboot |
/// | 8 | heap / task monitor | the espp Device Hub |
/// | 0x20 | test control (registered by the app) | emulink |
///
/// The device answers discovery, so the hosted espp web consoles find them.
class DevLink {
public:
struct Config {
std::string device_name; ///< advertised through discovery
std::string firmware; ///< firmware version, advertised through discovery
};

explicit DevLink(const Config &config);
~DevLink();

/// Register an application module (e.g. the test control service).
template <espp::DispatcherModuleConcept Module> void register_module(Module &module) {
worker_->register_module(module);
}

/// Send one frame to the host (serialized; all or nothing).
static void send(std::span<const uint8_t> frame);

/// The running image booted after an update and has not been confirmed
/// yet: confirm it (call once the app is up). No-op otherwise.
static void mark_app_valid();

/// After a panic reset: what it was ("reason (detail) at PC on core N"),
/// kept in RTC memory (no core dump needed); "" otherwise.
static std::string last_panic();

protected:
std::unique_ptr<espp::DispatcherWorker> worker_;
espp::CoreDump core_dump_;
std::unique_ptr<espp::CoreDumpService> core_dump_service_;
std::unique_ptr<FlashGuarded<espp::CoreDumpService>> core_dump_guarded_;
std::unique_ptr<espp::SystemService> system_service_;
std::unique_ptr<espp::MonitorService> monitor_service_;
#if DEVLINK_HAS_OTA
std::unique_ptr<espp::Ota> ota_;
std::unique_ptr<espp::OtaService> ota_service_;
std::unique_ptr<FlashGuarded<espp::OtaService>> ota_guarded_;
#endif
};
163 changes: 163 additions & 0 deletions components/devlink/src/devlink.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include "devlink.hpp"

#include <cstring>

#include <esp_attr.h>
#include <esp_ota_ops.h>
#include <sdkconfig.h>
#if CONFIG_IDF_TARGET_ARCH_XTENSA
#include <esp_cpu_utils.h>
#endif
#include <esp_debug_helpers.h>
#include <esp_private/panic_internal.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>

#include "box-emu.hpp"

void DevLink::send(std::span<const uint8_t> frame) { BoxEmu::get().vendor_write(frame); }

DevLink::DevLink(const Config &config) {
auto &emu = BoxEmu::get();
worker_ = std::make_unique<espp::DispatcherWorker>(espp::DispatcherWorker::Config{
.send = &DevLink::send,
.on_overflow =
[this]() {
#if DEVLINK_HAS_OTA
if (ota_service_) ota_service_->on_rx_overflow();
#endif
},
// ~3.5 KB used at the high-water mark (file transfers, core dump reads);
// internal RAM, since the core dump service reads the flash
.task_config = {.name = "devlink", .stack_size_bytes = 6144},
});
core_dump_service_ = std::make_unique<espp::CoreDumpService>(core_dump_, espp::CoreDumpService::Config{
.send = &DevLink::send,
});
system_service_ = std::make_unique<espp::SystemService>(espp::SystemService::Config{
.send = &DevLink::send,
.on_reboot_request =
[](espp::SystemService::RebootKind kind) {
if (kind == espp::SystemService::RebootKind::Bootloader) {
BoxEmu::get().usb_prepare_for_rom_download();
}
return true;
},
});
monitor_service_ = std::make_unique<espp::MonitorService>(espp::MonitorService::Config{
.send = &DevLink::send,
});
core_dump_guarded_ = std::make_unique<FlashGuarded<espp::CoreDumpService>>(*core_dump_service_);
worker_->register_module(*core_dump_guarded_);
worker_->register_module(*system_service_);
worker_->register_module(*monitor_service_);
#if DEVLINK_HAS_OTA
ota_ = std::make_unique<espp::Ota>(espp::Ota::Config{});
ota_service_ = std::make_unique<espp::OtaService>(*ota_, espp::OtaService::Config{
.send = &DevLink::send,
});
ota_guarded_ = std::make_unique<FlashGuarded<espp::OtaService>>(*ota_service_);
worker_->register_module(*ota_guarded_);
#endif
worker_->serve_discovery(config.device_name, config.firmware);
emu.set_vendor_rx([this](std::span<const uint8_t> data) { worker_->push(data); });
}

DevLink::~DevLink() { BoxEmu::get().set_vendor_rx(nullptr); }

void DevLink::mark_app_valid() {
esp_ota_img_states_t state;
const esp_partition_t *running = esp_ota_get_running_partition();
if (running && esp_ota_get_state_partition(running, &state) == ESP_OK && state == ESP_OTA_IMG_PENDING_VERIFY) {
esp_ota_mark_app_valid_cancel_rollback();
fmt::print("devlink: the updated firmware is up; marked valid\n");
}
}

// The last panic, kept across the reset in RTC memory: a crash in the middle of
// a flash operation (an OTA erase) cannot write its core dump, and the console
// is the USB CDC port, which dies with the chip. The wrappers run in the panic
// path (possibly with the cache off): IRAM, and only plain byte copies.
namespace {
struct LastPanic {
uint32_t magic;
uint32_t addr;
int32_t core;
int32_t exception;
char reason[64];
char detail[96];
char task[16];
uint32_t backtrace[20]; // Xtensa: the abort()'s caller chain
};
constexpr uint32_t kPanicMagic = 0x50414e43; // "PANC"
RTC_NOINIT_ATTR LastPanic s_last_panic;
DRAM_ATTR volatile bool s_aborting = false;
IRAM_ATTR void copy_str(char *dst, size_t n, const char *src) {
size_t i = 0;
if (src) {
for (; i + 1 < n && src[i]; ++i) dst[i] = src[i];
}
dst[i] = 0;
}
} // namespace

extern "C" void __real_esp_panic_handler(panic_info_t *info);
extern "C" IRAM_ATTR void __wrap_esp_panic_handler(panic_info_t *info) {
// an abort() gets here too (panic_abort() traps): keep its message
if (s_aborting) {
__real_esp_panic_handler(info);
return;
}
s_last_panic.magic = kPanicMagic;
s_last_panic.addr = (uint32_t)info->addr;
s_last_panic.core = info->core;
s_last_panic.exception = (int32_t)info->exception;
copy_str(s_last_panic.reason, sizeof(s_last_panic.reason), info->reason);
copy_str(s_last_panic.detail, sizeof(s_last_panic.detail), info->description);
TaskHandle_t t = xTaskGetCurrentTaskHandle();
copy_str(s_last_panic.task, sizeof(s_last_panic.task), t ? pcTaskGetName(t) : "?");
for (auto &pc : s_last_panic.backtrace) pc = 0;
__real_esp_panic_handler(info);
}

extern "C" void __real_panic_abort(const char *details);
IRAM_ATTR void note_task_and_backtrace() {
TaskHandle_t t = xTaskGetCurrentTaskHandle();
copy_str(s_last_panic.task, sizeof(s_last_panic.task), t ? pcTaskGetName(t) : "?");
for (auto &pc : s_last_panic.backtrace) pc = 0;
#if CONFIG_IDF_TARGET_ARCH_XTENSA
esp_backtrace_frame_t frame{};
esp_backtrace_get_start(&frame.pc, &frame.sp, &frame.next_pc);
for (int i = 0; i < 20; ++i) {
s_last_panic.backtrace[i] = esp_cpu_process_stack_pc(frame.pc);
if (!esp_backtrace_get_next_frame(&frame)) break;
}
#endif
}

extern "C" IRAM_ATTR void __wrap_panic_abort(const char *details) {
s_aborting = true;
note_task_and_backtrace();
s_last_panic.magic = kPanicMagic;
s_last_panic.addr = 0;
s_last_panic.core = -1;
s_last_panic.exception = -1;
copy_str(s_last_panic.reason, sizeof(s_last_panic.reason), "abort");
copy_str(s_last_panic.detail, sizeof(s_last_panic.detail), details);
__real_panic_abort(details);
}

std::string DevLink::last_panic() {
if (esp_reset_reason() != ESP_RST_PANIC || s_last_panic.magic != kPanicMagic) {
return "";
}
s_last_panic.reason[sizeof(s_last_panic.reason) - 1] = 0;
s_last_panic.detail[sizeof(s_last_panic.detail) - 1] = 0;
s_last_panic.task[sizeof(s_last_panic.task) - 1] = 0;
std::string bt;
for (auto pc : s_last_panic.backtrace) {
if (pc) bt += fmt::format(" {:#010x}", pc);
}
return fmt::format("{} ({}) at {:#010x} on core {} in task '{}'{}{}", s_last_panic.reason, s_last_panic.detail,
s_last_panic.addr, s_last_panic.core, s_last_panic.task, bt.empty() ? "" : "; backtrace", bt);
}
2 changes: 1 addition & 1 deletion components/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src"
REQUIRES lvgl timer logger rom_info tab5-emu)
REQUIRES lvgl timer logger rom_info settings tab5-emu)
19 changes: 19 additions & 0 deletions components/gui/include/console_art.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <string>

#include "lvgl.h"

#include "rom_info.hpp"

/// Draw a small icon of `info`'s console (a gamepad, a handheld, a disc...)
/// out of plain LVGL objects into a new `h * 8 / 5` x `h` container in
/// `parent`. Generic shapes in the console's accent color; no logos.
lv_obj_t *console_icon_create(lv_obj_t *parent, const ConsoleInfo &info, int h);

/// A rounded badge in the console's accent color: the icon and the console's
/// name, `h` pixels tall, sized to its content.
lv_obj_t *console_badge_create(lv_obj_t *parent, const ConsoleInfo &info, int h, const lv_font_t *font);

/// The SD card art for `info`, if there is any: `<root>/boxart/consoles/<key>.jpg`.
std::string console_art_path(const std::string &root, const ConsoleInfo &info);
32 changes: 29 additions & 3 deletions components/gui/include/gui.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@
#include "high_resolution_timer.hpp"
#include "logger.hpp"

#include "console_art.hpp"
#include "rom_info.hpp"
#include "settings.hpp"
#include "tab5-emu.hpp"

/// The launcher GUI: a list of games (from metadata.csv on the SD card) with
/// boxart, plus settings (volume, brightness, video scaling). Hand-written
/// LVGL, laid out for the Tab5's 1280x720 landscape screen.
/// The launcher GUI: a list of games (from metadata.csv on the SD card),
/// grouped under a header per console, with the selected game's boxart and
/// console, plus settings (volume, brightness, video scaling, resume), which
/// are kept in AppSettings. Hand-written LVGL, laid out for the Tab5's
/// 1280x720 landscape screen.
class Gui {
public:
struct Config {
Expand Down Expand Up @@ -47,6 +51,18 @@ class Gui {
void resume();

void update_rom_list();
/// Re-read metadata.csv even if its modification time did not change (FAT
/// times are coarse, and without a set clock they may not move at all).
void reload_rom_list() {
std::lock_guard<std::recursive_mutex> lk(mutex_);
metadata_last_modified_ = std::filesystem::file_time_type::min();
update_rom_list();
}
/// The games in list order (grouped by console).
std::vector<RomInfo> rom_infos() const {
std::lock_guard<std::recursive_mutex> lk(mutex_);
return rom_infos_;
}

/// Select the first game whose name or rom path contains `text`
bool select_rom_matching(std::string_view text);
Expand All @@ -72,6 +88,15 @@ class Gui {
lv_obj_t *boxart_{nullptr};
lv_draw_buf_t *boxart_buf_{nullptr}; ///< decoded boxart the image widget shows
void load_boxart(const std::string &path);
/// Decode a JPEG on the SD card into a new RGB565 draw buffer (nullptr if it cannot)
lv_draw_buf_t *decode_image(const std::string &path);

// the console the selected game runs on: its SD card art, or a badge
lv_obj_t *console_area_{nullptr};
lv_draw_buf_t *console_art_buf_{nullptr};
int shown_console_{-1};
void show_console(Console console);
void add_console_header(Console console);
lv_obj_t *title_label_{nullptr};
lv_obj_t *play_button_{nullptr};
lv_obj_t *volume_slider_{nullptr};
Expand All @@ -81,6 +106,7 @@ class Gui {
lv_obj_t *usb_label_{nullptr};
void set_usb_drive(bool on);
lv_obj_t *video_dropdown_{nullptr};
lv_obj_t *resume_switch_{nullptr};
lv_obj_t *battery_label_{nullptr};
std::vector<lv_obj_t *> rom_buttons_;

Expand Down
Loading
Loading