Add serial upload tool for rapid Lua game iteration
Implements a complete serial upload workflow that allows uploading and immediately testing Lua games via USB serial connection. New Components: - SerialUploader: Receives files via serial, writes to SD card - upload_game.py: Python tool for sending files from host computer - Protocol: Text-based with base64 encoding for reliability Key Features: - Uploads file to /games folder on SD card - Overwrites existing files (FA_CREATE_ALWAYS) - Auto-launches uploaded game immediately - Proper memory cleanup (prevents Lua state conflicts) SD Card Fixes: - Fixed SPI speed management (12.5MHz for SD, 32MHz for display) - Fixed SD write protocol (poll for data response token) - Added speed switching wrappers around all FatFS operations - Cleaned up excessive debug output Game Launcher Improvements: - Added clear_games() to prevent duplicate registrations - Added cleanup in select_game_by_name() to delete old instances - Added exact match priority in game selection - LuaGameLoader now has clear_factory_data() for memory cleanup Integration: - Added serial_uploader to CMakeLists.txt - Integrated into main loop in basic1.cpp - Re-scans games after upload to pick up new files Documentation: - UPLOAD_TOOL.md: Usage instructions - sd_card_best_practices.md: Critical lessons learned Known Issues: - Game launch after upload occasionally causes freeze (needs investigation) - Display may not refresh properly after upload Usage: python upload_game.py games/lua_examples/2048.lua /dev/tty.usbmodem101 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
#include "display/low_level_render.h"
|
||||
#include "display/low_level_gui.h"
|
||||
#include <stdio.h>
|
||||
#include <cstring>
|
||||
|
||||
GameLauncher::GameLauncher(uint16_t width, uint16_t height,
|
||||
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager)
|
||||
@@ -212,6 +213,72 @@ void GameLauncher::reset() {
|
||||
printf("Launcher reset - returning to menu\n");
|
||||
}
|
||||
|
||||
void GameLauncher::clear_games() {
|
||||
// Clean up currently selected game first
|
||||
if (selected_game) {
|
||||
delete selected_game;
|
||||
selected_game = nullptr;
|
||||
}
|
||||
|
||||
// Clear the games vector
|
||||
games.clear();
|
||||
selected_index = 0;
|
||||
current_page = 0;
|
||||
printf("Launcher: Cleared all registered games\n");
|
||||
}
|
||||
|
||||
bool GameLauncher::select_game_by_name(const char* name) {
|
||||
// Clean up old game first if one exists
|
||||
if (selected_game) {
|
||||
printf("Cleaning up previous game...\n");
|
||||
delete selected_game;
|
||||
selected_game = nullptr;
|
||||
}
|
||||
|
||||
// First pass: search for exact match (prefer exact matches)
|
||||
for (size_t i = 0; i < games.size(); i++) {
|
||||
if (strcmp(games[i].name, name) == 0) {
|
||||
printf("Found exact match: %s\n", games[i].name);
|
||||
|
||||
// Create and initialize the game
|
||||
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
|
||||
if (selected_game) {
|
||||
printf("Game instance created, initializing...\n");
|
||||
selected_game->init();
|
||||
selected_index = i;
|
||||
current_page = i / GAMES_PER_PAGE;
|
||||
return true;
|
||||
} else {
|
||||
printf("Failed to create game instance\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: search in reverse order for partial match (newest files first)
|
||||
for (int i = games.size() - 1; i >= 0; i--) {
|
||||
if (strstr(games[i].name, name) != nullptr) {
|
||||
printf("Found partial match: %s (searching for: %s)\n", games[i].name, name);
|
||||
|
||||
// Create and initialize the game
|
||||
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
|
||||
if (selected_game) {
|
||||
printf("Game instance created, initializing...\n");
|
||||
selected_game->init();
|
||||
selected_index = i;
|
||||
current_page = i / GAMES_PER_PAGE;
|
||||
return true;
|
||||
} else {
|
||||
printf("Failed to create game instance\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("Game not found: %s\n", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
int GameLauncher::get_total_pages() const {
|
||||
if (games.empty()) return 1;
|
||||
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;
|
||||
|
||||
@@ -78,13 +78,25 @@ public:
|
||||
* @brief Reset launcher to show menu again
|
||||
*/
|
||||
void reset();
|
||||
|
||||
|
||||
/**
|
||||
* @brief Clear all registered games (useful before re-scanning)
|
||||
*/
|
||||
void clear_games();
|
||||
|
||||
/**
|
||||
* @brief Check if a game is currently selected
|
||||
* @return true if game selected, false if in menu
|
||||
*/
|
||||
bool is_game_selected() const { return selected_game != nullptr; }
|
||||
|
||||
/**
|
||||
* @brief Select a game by name (for programmatic launching)
|
||||
* @param name Game name to select (partial match supported)
|
||||
* @return true if game was found and launched, false otherwise
|
||||
*/
|
||||
bool select_game_by_name(const char* name);
|
||||
|
||||
private:
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
|
||||
@@ -323,47 +323,69 @@ bool sd_card_read_blocks(uint32_t block_addr, uint32_t num_blocks, uint8_t *buff
|
||||
}
|
||||
|
||||
bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) {
|
||||
if (!g_card_info.initialized || buffer == NULL) return false;
|
||||
|
||||
if (!g_card_info.initialized || buffer == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For non-SDHC cards, convert block address to byte address
|
||||
if (g_card_info.type != SD_CARD_TYPE_SDHC) {
|
||||
block_addr *= SD_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
|
||||
sd_card_select();
|
||||
|
||||
|
||||
// Send write command
|
||||
uint8_t r1 = sd_card_send_command(SD_CMD24, block_addr);
|
||||
|
||||
if (r1 != SD_R1_READY) {
|
||||
sd_card_deselect();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Wait for card to be ready to receive data
|
||||
uint32_t timeout_count = 0;
|
||||
uint8_t ready_byte;
|
||||
do {
|
||||
ready_byte = sd_card_transfer(0xFF);
|
||||
timeout_count++;
|
||||
if (timeout_count > 1000) {
|
||||
sd_card_deselect();
|
||||
return false;
|
||||
}
|
||||
} while (ready_byte != 0xFF);
|
||||
|
||||
// Send start token
|
||||
sd_card_transfer(SD_START_TOKEN);
|
||||
|
||||
|
||||
// Write data block
|
||||
for (int i = 0; i < SD_BLOCK_SIZE; i++) {
|
||||
sd_card_transfer(buffer[i]);
|
||||
}
|
||||
|
||||
|
||||
// Send dummy CRC (2 bytes)
|
||||
sd_card_transfer(0xFF);
|
||||
sd_card_transfer(0xFF);
|
||||
|
||||
// Check data response
|
||||
uint8_t response = sd_card_transfer(0xFF);
|
||||
|
||||
// Check data response - may need to read several bytes before getting response
|
||||
uint8_t response = 0xFF;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
response = sd_card_transfer(0xFF);
|
||||
if (response != 0xFF) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((response & 0x1F) != SD_DATA_ACCEPTED) {
|
||||
sd_card_deselect();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Wait for card to finish writing
|
||||
if (!sd_card_wait_ready(500)) {
|
||||
sd_card_deselect();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
sd_card_deselect();
|
||||
return true;
|
||||
}
|
||||
|
||||
321
lib/serial_uploader.cpp
Normal file
321
lib/serial_uploader.cpp
Normal file
@@ -0,0 +1,321 @@
|
||||
#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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
bool SerialUploader::process() {
|
||||
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) {
|
||||
if (write_file_to_sd()) {
|
||||
state = LAUNCHING_GAME;
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state == LAUNCHING_GAME) {
|
||||
launch_game();
|
||||
reset();
|
||||
return true; // Signal that game was launched
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
52
lib/serial_uploader.h
Normal file
52
lib/serial_uploader.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#ifndef SERIAL_UPLOADER_H
|
||||
#define SERIAL_UPLOADER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include "ff.h"
|
||||
|
||||
class GameLauncher;
|
||||
|
||||
class SerialUploader {
|
||||
public:
|
||||
SerialUploader(GameLauncher* launcher);
|
||||
|
||||
// Process incoming serial data (call this frequently in main loop)
|
||||
// Returns true if a game was launched
|
||||
bool process();
|
||||
|
||||
// Get the filename of the last uploaded game (without .lua extension)
|
||||
const char* get_last_uploaded_filename() const { return last_uploaded_name; }
|
||||
|
||||
private:
|
||||
enum State {
|
||||
IDLE,
|
||||
RECEIVING_FILENAME,
|
||||
RECEIVING_SIZE,
|
||||
RECEIVING_DATA,
|
||||
WRITING_FILE,
|
||||
LAUNCHING_GAME
|
||||
};
|
||||
|
||||
State state;
|
||||
GameLauncher* game_launcher;
|
||||
|
||||
// Upload state
|
||||
char filename[64];
|
||||
char last_uploaded_name[64]; // Game name without .lua extension
|
||||
uint32_t file_size;
|
||||
uint32_t bytes_received;
|
||||
uint8_t* file_buffer;
|
||||
|
||||
// Base64 decoding buffer
|
||||
char base64_buffer[4];
|
||||
int base64_index;
|
||||
|
||||
// Helper methods
|
||||
void reset();
|
||||
bool write_file_to_sd();
|
||||
void launch_game();
|
||||
uint8_t decode_base64_char(char c);
|
||||
void decode_base64_block(const char* input, uint8_t* output);
|
||||
};
|
||||
|
||||
#endif // SERIAL_UPLOADER_H
|
||||
Reference in New Issue
Block a user