334 lines
9.9 KiB
C++
334 lines
9.9 KiB
C++
#include "serial_uploader.h"
|
|
#include "game_launcher.h"
|
|
#include "lua_game_loader.h"
|
|
#include "sd_card.h"
|
|
#include "pico/stdlib.h"
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <cctype>
|
|
|
|
// Maximum file size: 64KB (should be plenty for Lua games)
|
|
#define MAX_FILE_SIZE (64 * 1024)
|
|
|
|
SerialUploader::SerialUploader(GameLauncher* launcher)
|
|
: state(IDLE)
|
|
, game_launcher(launcher)
|
|
, file_size(0)
|
|
, bytes_received(0)
|
|
, file_buffer(nullptr)
|
|
, base64_index(0)
|
|
{
|
|
filename[0] = '\0';
|
|
last_uploaded_name[0] = '\0';
|
|
}
|
|
|
|
void SerialUploader::reset() {
|
|
state = IDLE;
|
|
file_size = 0;
|
|
bytes_received = 0;
|
|
base64_index = 0;
|
|
filename[0] = '\0';
|
|
|
|
if (file_buffer) {
|
|
delete[] file_buffer;
|
|
file_buffer = nullptr;
|
|
}
|
|
}
|
|
|
|
uint8_t SerialUploader::decode_base64_char(char c) {
|
|
if (c >= 'A' && c <= 'Z') return c - 'A';
|
|
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
|
if (c >= '0' && c <= '9') return c - '0' + 52;
|
|
if (c == '+') return 62;
|
|
if (c == '/') return 63;
|
|
return 0;
|
|
}
|
|
|
|
void SerialUploader::decode_base64_block(const char* input, uint8_t* output) {
|
|
uint32_t combined = (decode_base64_char(input[0]) << 18) |
|
|
(decode_base64_char(input[1]) << 12) |
|
|
(decode_base64_char(input[2]) << 6) |
|
|
decode_base64_char(input[3]);
|
|
|
|
output[0] = (combined >> 16) & 0xFF;
|
|
output[1] = (combined >> 8) & 0xFF;
|
|
output[2] = combined & 0xFF;
|
|
}
|
|
|
|
bool SerialUploader::write_file_to_sd() {
|
|
if (bytes_received == 0) {
|
|
printf("ERROR No data received!\n");
|
|
return false;
|
|
}
|
|
|
|
if (!file_buffer) {
|
|
printf("ERROR File buffer is NULL!\n");
|
|
return false;
|
|
}
|
|
|
|
// Set SPI speed to SD card speed (12.5 MHz) and save previous speed
|
|
uint prev_speed = sd_card_set_spi_speed();
|
|
|
|
// Ensure /games directory exists
|
|
FRESULT fr = f_mkdir("/games");
|
|
if (fr != FR_OK && fr != FR_EXIST) {
|
|
printf("ERROR Failed to create /games directory: %d\n", fr);
|
|
// Try to continue anyway in case it already exists
|
|
}
|
|
|
|
// Build file path - overwrite if file exists
|
|
char filepath[128];
|
|
snprintf(filepath, sizeof(filepath), "/games/%s", filename);
|
|
|
|
FIL fil;
|
|
fr = f_open(&fil, filepath, FA_CREATE_ALWAYS | FA_WRITE);
|
|
|
|
if (fr != FR_OK) {
|
|
printf("ERROR Failed to open file for writing: %d\n", fr);
|
|
sd_card_restore_spi_speed(prev_speed);
|
|
return false;
|
|
}
|
|
|
|
// Write data in chunks (more reliable for large files)
|
|
const uint32_t CHUNK_SIZE = 512;
|
|
uint32_t total_written = 0;
|
|
|
|
while (total_written < bytes_received) {
|
|
uint32_t chunk = (bytes_received - total_written > CHUNK_SIZE) ? CHUNK_SIZE : (bytes_received - total_written);
|
|
|
|
UINT bytes_written = 0;
|
|
fr = f_write(&fil, file_buffer + total_written, chunk, &bytes_written);
|
|
|
|
if (fr != FR_OK) {
|
|
printf("ERROR f_write failed at byte %u: error %d\n", total_written, fr);
|
|
f_close(&fil);
|
|
sd_card_restore_spi_speed(prev_speed);
|
|
return false;
|
|
}
|
|
|
|
if (bytes_written != chunk) {
|
|
printf("ERROR Partial write: wrote %u/%u bytes at offset %u\n", bytes_written, chunk, total_written);
|
|
f_close(&fil);
|
|
sd_card_restore_spi_speed(prev_speed);
|
|
return false;
|
|
}
|
|
|
|
total_written += bytes_written;
|
|
}
|
|
|
|
// Sync to ensure data is written to SD card
|
|
fr = f_sync(&fil);
|
|
if (fr != FR_OK) {
|
|
printf("ERROR f_sync failed: %d\n", fr);
|
|
}
|
|
|
|
f_close(&fil);
|
|
|
|
printf("✓ Wrote %u bytes to %s\n", total_written, filepath);
|
|
|
|
// Restore display SPI speed
|
|
sd_card_restore_spi_speed(prev_speed);
|
|
|
|
return true;
|
|
}
|
|
|
|
void SerialUploader::launch_game() {
|
|
// Extract base game name (remove .lua extension)
|
|
strncpy(last_uploaded_name, filename, sizeof(last_uploaded_name) - 1);
|
|
last_uploaded_name[sizeof(last_uploaded_name) - 1] = '\0';
|
|
|
|
// Remove .lua extension
|
|
char* ext = strstr(last_uploaded_name, ".lua");
|
|
if (ext) {
|
|
*ext = '\0';
|
|
}
|
|
|
|
printf("Re-scanning Lua games to pick up new file...\n");
|
|
|
|
// Clear existing games before re-scanning (prevents duplicates and memory leaks)
|
|
game_launcher->clear_games();
|
|
LuaGameLoader::clear_factory_data();
|
|
|
|
// Re-scan Lua games to pick up the newly uploaded file
|
|
// Note: LuaGameLoader::register_all_games handles SPI speed internally
|
|
int new_games = LuaGameLoader::register_all_games(game_launcher);
|
|
printf("Found %d Lua games after re-scan\n", new_games);
|
|
}
|
|
|
|
bool SerialUploader::complete_launch() {
|
|
// This should only be called when it's safe (no display refresh in progress)
|
|
// Now try to launch the newly uploaded game by name
|
|
printf("Attempting to launch game: %s\n", last_uploaded_name);
|
|
bool launched = game_launcher->select_game_by_name(last_uploaded_name);
|
|
|
|
if (launched) {
|
|
printf("LAUNCHED %s\n", last_uploaded_name);
|
|
} else {
|
|
printf("ERROR Failed to launch game: %s\n", last_uploaded_name);
|
|
}
|
|
|
|
// Reset state back to IDLE
|
|
reset();
|
|
|
|
return launched;
|
|
}
|
|
|
|
bool SerialUploader::process(bool spi_busy) {
|
|
if (state == IDLE) {
|
|
// Check for "UPLOAD" command
|
|
int c = getchar_timeout_us(0);
|
|
if (c == PICO_ERROR_TIMEOUT) return false;
|
|
|
|
if (c == 'U') {
|
|
// Check for "UPLOAD "
|
|
static char cmd_buffer[8];
|
|
cmd_buffer[0] = c;
|
|
int idx = 1;
|
|
|
|
// Read "PLOAD "
|
|
for (int i = 0; i < 6; i++) {
|
|
c = getchar_timeout_us(100000); // 100ms timeout
|
|
if (c == PICO_ERROR_TIMEOUT) return false;
|
|
cmd_buffer[idx++] = c;
|
|
}
|
|
cmd_buffer[idx] = '\0';
|
|
|
|
if (strcmp(cmd_buffer, "UPLOAD ") == 0) {
|
|
state = RECEIVING_FILENAME;
|
|
filename[0] = '\0';
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state == RECEIVING_FILENAME) {
|
|
// Read filename until space
|
|
int idx = strlen(filename);
|
|
while (idx < sizeof(filename) - 1) {
|
|
int c = getchar_timeout_us(100000);
|
|
if (c == PICO_ERROR_TIMEOUT) break;
|
|
|
|
if (c == ' ') {
|
|
filename[idx] = '\0';
|
|
state = RECEIVING_SIZE;
|
|
file_size = 0;
|
|
return false;
|
|
}
|
|
|
|
filename[idx++] = (char)c;
|
|
}
|
|
}
|
|
|
|
if (state == RECEIVING_SIZE) {
|
|
// Read file size until newline
|
|
char size_buffer[16];
|
|
int idx = 0;
|
|
|
|
while (idx < sizeof(size_buffer) - 1) {
|
|
int c = getchar_timeout_us(100000);
|
|
if (c == PICO_ERROR_TIMEOUT) break;
|
|
|
|
if (c == '\n' || c == '\r') {
|
|
size_buffer[idx] = '\0';
|
|
file_size = atoi(size_buffer);
|
|
|
|
if (file_size == 0 || file_size > MAX_FILE_SIZE) {
|
|
printf("ERROR Invalid file size: %u\n", file_size);
|
|
reset();
|
|
return false;
|
|
}
|
|
|
|
// Allocate buffer for decoded data
|
|
// file_size is the ORIGINAL (decoded) size, so allocate exactly that
|
|
file_buffer = new uint8_t[file_size];
|
|
|
|
if (!file_buffer) {
|
|
printf("ERROR Failed to allocate %u bytes for file buffer\n", file_size);
|
|
reset();
|
|
return false;
|
|
}
|
|
|
|
bytes_received = 0;
|
|
base64_index = 0;
|
|
|
|
state = RECEIVING_DATA;
|
|
printf("Receiving %u bytes for %s...\n", file_size, filename);
|
|
return false;
|
|
}
|
|
|
|
size_buffer[idx++] = (char)c;
|
|
}
|
|
}
|
|
|
|
if (state == RECEIVING_DATA) {
|
|
// Read base64 data until "END"
|
|
while (true) {
|
|
int c = getchar_timeout_us(1000);
|
|
if (c == PICO_ERROR_TIMEOUT) break;
|
|
|
|
// Check for "END" marker
|
|
static char end_check[4] = {0, 0, 0, 0};
|
|
end_check[0] = end_check[1];
|
|
end_check[1] = end_check[2];
|
|
end_check[2] = end_check[3];
|
|
end_check[3] = (char)c;
|
|
|
|
if (end_check[0] == '\n' && end_check[1] == 'E' && end_check[2] == 'N' && end_check[3] == 'D') {
|
|
state = WRITING_FILE;
|
|
return false;
|
|
}
|
|
|
|
// Skip whitespace
|
|
if (isspace(c)) continue;
|
|
|
|
// Accumulate base64 characters
|
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '+' || c == '/' || c == '=') {
|
|
|
|
base64_buffer[base64_index++] = (char)c;
|
|
|
|
// Decode every 4 characters
|
|
if (base64_index == 4) {
|
|
uint8_t decoded[3];
|
|
decode_base64_block(base64_buffer, decoded);
|
|
|
|
// Handle padding
|
|
int decoded_count = 3;
|
|
if (base64_buffer[2] == '=') decoded_count = 1;
|
|
else if (base64_buffer[3] == '=') decoded_count = 2;
|
|
|
|
// Copy decoded bytes
|
|
for (int i = 0; i < decoded_count; i++) {
|
|
if (bytes_received < file_size) {
|
|
file_buffer[bytes_received++] = decoded[i];
|
|
}
|
|
}
|
|
|
|
base64_index = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (state == WRITING_FILE) {
|
|
// Wait if SPI bus is busy (e.g. display refresh in progress on other core)
|
|
if (spi_busy) return false;
|
|
|
|
if (write_file_to_sd()) {
|
|
state = LAUNCHING_GAME;
|
|
// Prepare for launch by scanning games, but don't actually launch yet
|
|
launch_game();
|
|
} else {
|
|
reset();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (state == LAUNCHING_GAME) {
|
|
// Stay in this state until main loop calls complete_launch() when safe
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|