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:
Adolfo Reyna
2026-02-12 22:52:57 -05:00
parent b26f3bf775
commit 84b009c33e
14 changed files with 1222 additions and 34 deletions

View File

@@ -0,0 +1,373 @@
# SD Card Best Practices for RP2350 + FatFS
This document captures critical best practices for working with SD card operations in this project, based on lessons learned during development.
## 1. SPI Speed Management
### Critical Rule: Always Set SD Card Speed Before Operations
The display and SD card share the same SPI bus but operate at different speeds:
- **Display**: 32 MHz
- **SD Card**: 12.5 MHz
**ALWAYS wrap SD card operations with speed switching:**
```cpp
// Save current speed and switch to SD card speed
uint prev_speed = sd_card_set_spi_speed();
// ... SD card operations here ...
// f_open(), f_write(), f_read(), f_readdir(), etc.
// Restore previous speed for display
sd_card_restore_spi_speed(prev_speed);
```
### Why This Matters
Running SD card operations at the wrong SPI speed causes:
- Unreliable reads/writes
- Corrupted data
- `FR_DISK_ERR` errors from FatFS
- Hardware-level protocol failures (0xFF data responses)
### Where Speed Switching is Already Handled
These functions handle SPI speed internally (you don't need to wrap them):
- `LuaGameLoader::register_all_games()`
- All functions in `sd_card.c` (low-level operations)
### Where You MUST Handle Speed Switching
Any code that calls FatFS functions directly:
- `f_open()`, `f_close()`
- `f_read()`, `f_write()`
- `f_opendir()`, `f_readdir()`, `f_closedir()`
- `f_stat()`, `f_mkdir()`, `f_unlink()`
- `f_getfree()`, `f_sync()`
## 2. SD Card Write Protocol
### The Data Response Polling Issue
When writing to SD card with `CMD24` (write single block), the data response token may not arrive immediately. You must poll for it:
```cpp
// After sending data block and CRC:
uint8_t response = 0xFF;
for (int i = 0; i < 10; i++) {
response = sd_card_transfer(0xFF);
if (response != 0xFF) {
break; // Got the response
}
}
// Check if data was accepted
if ((response & 0x1F) != SD_DATA_ACCEPTED) {
// Write failed
}
```
**Why:** The SD card may need a few clock cycles before sending the data response token. Reading only once may return 0xFF (no response yet).
### Wait for Card Ready After CMD24
After sending the write command and before sending data:
```cpp
// After CMD24 command:
uint8_t ready_byte;
do {
ready_byte = sd_card_transfer(0xFF);
timeout_count++;
if (timeout_count > 1000) {
return false; // Timeout
}
} while (ready_byte != 0xFF);
```
This ensures the card is ready to receive the data block.
## 3. FatFS Best Practices
### Always Check Return Codes
```cpp
FRESULT fr = f_open(&fil, path, FA_CREATE_ALWAYS | FA_WRITE);
if (fr != FR_OK) {
printf("ERROR: f_open failed: %d\n", fr);
// Clean up and return
return false;
}
```
Common FatFS error codes:
- `FR_OK (0)`: Success
- `FR_DISK_ERR (1)`: Low-level disk error (often SPI speed issue)
- `FR_NOT_READY (3)`: Card not initialized
- `FR_NO_FILE (4)`: File not found
- `FR_NO_PATH (5)`: Path not found
- `FR_EXIST (8)`: File/directory already exists
### Use FA_CREATE_ALWAYS to Overwrite
For rapid iteration (like our serial uploader):
```cpp
f_open(&fil, path, FA_CREATE_ALWAYS | FA_WRITE);
```
This overwrites existing files, perfect for development.
### Write in Chunks for Large Files
For files larger than 512 bytes, write in chunks:
```cpp
const uint32_t CHUNK_SIZE = 512;
uint32_t total_written = 0;
while (total_written < total_size) {
uint32_t chunk_size = min(CHUNK_SIZE, total_size - total_written);
UINT bytes_written;
fr = f_write(&fil, buffer + total_written, chunk_size, &bytes_written);
if (fr != FR_OK || bytes_written != chunk_size) {
// Handle error
break;
}
total_written += bytes_written;
}
```
### Always Sync and Close
```cpp
f_sync(&fil); // Ensure data is written to card
f_close(&fil); // Close file and update directory
```
Skipping `f_sync()` can lead to data loss if power is lost.
## 4. Memory Management
### Clean Up After Re-scanning
When re-scanning games (like after upload), clean up old data:
```cpp
// Clear game launcher entries
game_launcher->clear_games();
// Clear Lua game factory data
LuaGameLoader::clear_factory_data();
// Re-scan
LuaGameLoader::register_all_games(game_launcher);
```
**Why:** Without cleanup, you get duplicate registrations and memory leaks.
### Delete Old Game Instances Before Creating New Ones
```cpp
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
// Now create new game
selected_game = factory(width, height, renderer, gui, input_manager);
```
**Critical for Lua games:** Each LuaGame has a Lua state. Not cleaning up the old one before creating a new one causes conflicts and freezes.
## 5. Debugging Tips
### Add Targeted Debug Output
When debugging SD operations, add prints at key points:
```cpp
printf("✓ Wrote %u bytes to %s\n", bytes_written, filepath);
```
But avoid spamming the console - it slows down operations significantly.
### Check Hardware Layer First
If FatFS returns `FR_DISK_ERR`, the issue is usually at the hardware level:
1. Check SPI speed (most common issue)
2. Check SD card write protection
3. Check physical SD card connection
4. Verify SD card is properly initialized
### Use Root Directory for Testing
When debugging writes, test with root directory first:
```cpp
FIL test_file;
if (f_open(&test_file, "/test.txt", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) {
printf("Root write OK\n");
f_close(&test_file);
f_unlink("/test.txt");
}
```
This isolates directory-related issues.
## 6. Common Pitfalls
### ❌ DON'T: Forget SPI Speed Management
```cpp
// BAD - Will fail or be unreliable
f_open(&fil, "/games/test.lua", FA_WRITE);
```
### ✅ DO: Always Switch Speeds
```cpp
// GOOD
uint prev_speed = sd_card_set_spi_speed();
f_open(&fil, "/games/test.lua", FA_WRITE);
// ... operations ...
sd_card_restore_spi_speed(prev_speed);
```
### ❌ DON'T: Assume Immediate Data Response
```cpp
// BAD - May get 0xFF (no response yet)
uint8_t response = sd_card_transfer(0xFF);
if (response != 0x05) {
// Might incorrectly fail
}
```
### ✅ DO: Poll for Data Response
```cpp
// GOOD - Poll until response arrives
uint8_t response = 0xFF;
for (int i = 0; i < 10; i++) {
response = sd_card_transfer(0xFF);
if (response != 0xFF) break;
}
```
### ❌ DON'T: Skip Error Checking
```cpp
// BAD
f_write(&fil, buffer, size, &bytes_written);
f_close(&fil);
```
### ✅ DO: Check Every Return Value
```cpp
// GOOD
if (f_write(&fil, buffer, size, &bytes_written) != FR_OK) {
printf("Write failed\n");
f_close(&fil);
return false;
}
```
### ❌ DON'T: Create New Games Without Cleanup
```cpp
// BAD - Memory leak and Lua state conflicts
selected_game = new LuaGame(...);
```
### ✅ DO: Clean Up First
```cpp
// GOOD
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
selected_game = new LuaGame(...);
```
## 7. Serial Upload Pattern
The serial uploader demonstrates the complete pattern:
```cpp
bool SerialUploader::write_file_to_sd() {
// 1. Validate input
if (!file_buffer || bytes_received == 0) return false;
// 2. Set SD card SPI speed
uint prev_speed = sd_card_set_spi_speed();
// 3. Ensure directory exists
f_mkdir("/games");
// 4. Open file (overwrite mode for iteration)
FIL fil;
FRESULT fr = f_open(&fil, filepath, FA_CREATE_ALWAYS | FA_WRITE);
if (fr != FR_OK) {
sd_card_restore_spi_speed(prev_speed);
return false;
}
// 5. Write in chunks
const uint32_t CHUNK_SIZE = 512;
uint32_t total_written = 0;
while (total_written < bytes_received) {
uint32_t chunk = min(CHUNK_SIZE, bytes_received - total_written);
UINT written;
fr = f_write(&fil, file_buffer + total_written, chunk, &written);
if (fr != FR_OK || written != chunk) {
f_close(&fil);
sd_card_restore_spi_speed(prev_speed);
return false;
}
total_written += written;
}
// 6. Sync and close
f_sync(&fil);
f_close(&fil);
// 7. Restore display SPI speed
sd_card_restore_spi_speed(prev_speed);
return true;
}
```
## 8. Testing Checklist
When implementing new SD card functionality:
- [ ] SPI speed switching is in place
- [ ] All FatFS return codes are checked
- [ ] Files are properly closed after operations
- [ ] Memory is cleaned up (no leaks)
- [ ] Error messages are informative
- [ ] Tested with both small and large files
- [ ] Tested overwriting existing files
- [ ] Tested with non-existent directories
- [ ] Verified data integrity (read back after write)
## Summary
The most important rules:
1. **Always manage SPI speed** around FatFS operations
2. **Poll for SD card responses** - don't assume immediate response
3. **Check error codes** on every operation
4. **Clean up memory** before creating new game instances
5. **Write in chunks** for large files
6. **Sync before closing** to ensure data is written
Following these practices will save hours of debugging SD card issues!

View File

@@ -48,6 +48,7 @@ add_executable(basic1
basic1.cpp basic1.cpp
lib/input_manager.cpp lib/input_manager.cpp
lib/game_launcher.cpp lib/game_launcher.cpp
lib/serial_uploader.cpp
games/tic_tac_toe.cpp games/tic_tac_toe.cpp
games/demo_game.cpp games/demo_game.cpp
games/monopoly/monopoly_game.cpp games/monopoly/monopoly_game.cpp

151
UPLOAD_TOOL.md Normal file
View File

@@ -0,0 +1,151 @@
# Lua Game Upload Tool
Rapidly upload and execute Lua games on your RP2350 via USB serial for quick iteration during development!
## Features
- Upload Lua game files directly to the SD card via USB serial
- Automatically launches the uploaded game immediately
- No need to manually swap SD cards or restart the device
- Perfect for rapid game development and testing
## Requirements
### Computer Side
- Python 3.x
- pyserial library: `pip install pyserial`
### RP2350 Side
- Firmware must be built with serial uploader support (already included)
- SD card inserted and formatted (FAT32)
- USB connection to computer
## Usage
### 1. Connect Your Device
Connect your RP2350 to your computer via USB. The device will appear as a serial port:
- **Linux/Mac**: Usually `/dev/ttyACM0` or `/dev/ttyUSB0`
- **Windows**: Usually `COM3`, `COM4`, etc.
### 2. Upload a Lua Game
Basic usage:
```bash
python upload_game.py my_game.lua
```
Specify a custom serial port:
```bash
python upload_game.py my_game.lua /dev/ttyACM0 # Linux/Mac
python upload_game.py my_game.lua COM3 # Windows
```
The script will:
1. Read your Lua file
2. Upload it to the RP2350 via serial
3. Save it to `/games/<filename>.lua` on the SD card
4. Automatically launch the game!
### 3. View Available Serial Ports
Run the script without arguments to see available ports:
```bash
python upload_game.py
```
## Example Workflow
```bash
# Edit your game
vim snake.lua
# Upload and test (the game starts immediately!)
python upload_game.py snake.lua
# Make changes
vim snake.lua
# Upload again (instantly replaces and restarts)
python upload_game.py snake.lua
```
## Protocol Details
The tool uses a simple text-based protocol over USB serial:
```
UPLOAD <filename> <size_in_bytes>
<base64_encoded_file_content>
END
```
**Response:**
```
OK <bytes_written>
LAUNCHED <game_name>
```
## Troubleshooting
### "No serial port found"
- Make sure your device is connected via USB
- Check if the device appears in your system (use `ls /dev/tty*` on Linux/Mac)
- On Linux, you may need to add your user to the `dialout` group: `sudo usermod -a -G dialout $USER`
### "Permission denied"
On Linux/Mac, you may need permissions to access the serial port:
```bash
sudo chmod 666 /dev/ttyACM0 # Quick fix
# OR
sudo usermod -a -G dialout $USER && newgrp dialout # Permanent fix
```
### "Upload failed" or "ERROR" messages
- Make sure the SD card is properly inserted and formatted (FAT32)
- Check that there's enough space on the SD card
- Verify the Lua file is valid (syntax errors will appear when game launches)
### Game doesn't appear or launch
- Check the serial output from the RP2350 for error messages
- Ensure the Lua file has proper metadata comments at the top:
```lua
-- NAME: My Game
-- DESCRIPTION: A fun game
```
## Tips for Rapid Development
1. **Use a serial terminal** alongside uploads to see debug output:
```bash
screen /dev/ttyACM0 115200
```
2. **Create a watch script** to auto-upload on file changes:
```bash
while inotifywait -e modify my_game.lua; do
python upload_game.py my_game.lua
done
```
3. **Use printf() in Lua** for debugging:
```lua
function update(event)
print("Event type: " .. event.type)
-- your code here
end
```
## File Size Limits
- Maximum file size: 64 KB
- This should be plenty for most Lua games
- If you need more, consider splitting assets or code
## Related Files
- `lib/serial_uploader.h` - C++ header for serial upload handler
- `lib/serial_uploader.cpp` - C++ implementation
- `upload_game.py` - Python upload script
Happy game development! 🎮

View File

@@ -58,6 +58,7 @@ extern "C" {
#include "demo_game.h" #include "demo_game.h"
#include "monopoly_game.h" #include "monopoly_game.h"
#include "lua_game_loader.h" #include "lua_game_loader.h"
#include "serial_uploader.h"
// Binary info for RP2350 - ensures proper boot image structure // Binary info for RP2350 - ensures proper boot image structure
@@ -437,6 +438,10 @@ int main()
// Create GameLauncher // Create GameLauncher
GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager); GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager);
// Create SerialUploader for rapid game iteration
SerialUploader serial_uploader(&launcher);
printf("Serial uploader initialized\n");
// Register available games // Register available games
launcher.register_game("Tic-Tac-Toe", "Classic 2-player game", launcher.register_game("Tic-Tac-Toe", "Classic 2-player game",
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* {
@@ -563,7 +568,18 @@ int main()
const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms
uint32_t last_frame_time = 0; uint32_t last_frame_time = 0;
bool needs_refresh = false; // Track if screen needs redraw
while (1) { while (1) {
// 0. Process serial uploads (for rapid game iteration)
bool game_launched_via_serial = serial_uploader.process();
if (game_launched_via_serial) {
// A new game was uploaded and launched - trigger redraw
needs_refresh = true;
current_game = launcher.get_selected_game();
// Note: game is already initialized by select_game_by_name()
}
// Determine if we should sleep or stay awake for updates // Determine if we should sleep or stay awake for updates
bool stay_awake = false; bool stay_awake = false;
if (pending_refresh) stay_awake = true; if (pending_refresh) stay_awake = true;
@@ -581,7 +597,6 @@ int main()
} }
InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false}; InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false};
bool needs_refresh = false;
// 1. Process button input first (higher priority) // 1. Process button input first (higher priority)
input = input_manager.process_button_input(); input = input_manager.process_button_input();

View File

@@ -6,6 +6,8 @@
#include "diskio.h" #include "diskio.h"
#include "sd_card.h" #include "sd_card.h"
#include <string.h> #include <string.h>
#include <stdio.h>
#include <stdbool.h>
/* Definitions of physical drive number for each drive */ /* Definitions of physical drive number for each drive */
#define DEV_SD 0 /* SD card */ #define DEV_SD 0 /* SD card */
@@ -21,7 +23,7 @@ DSTATUS disk_status (
if (pdrv != DEV_SD) return STA_NOINIT; if (pdrv != DEV_SD) return STA_NOINIT;
// Assume SD card is always initialized after sd_card_init() is called // Assume SD card is always initialized after sd_card_init() is called
return 0; // OK return 0; // OK - not write protected, not removed
} }
/*-----------------------------------------------------------------------*/ /*-----------------------------------------------------------------------*/
@@ -76,7 +78,10 @@ DRESULT disk_write (
if (pdrv != DEV_SD) return RES_PARERR; if (pdrv != DEV_SD) return RES_PARERR;
for (UINT i = 0; i < count; i++) { for (UINT i = 0; i < count; i++) {
if (!sd_card_write_block(sector + i, (uint8_t*)(buff + (i * 512)))) { bool result = sd_card_write_block(sector + i, (uint8_t*)(buff + (i * 512)));
if (!result) {
printf("ERROR disk_write failed at sector %lu\n", (unsigned long)(sector + i));
return RES_ERROR; return RES_ERROR;
} }
} }
@@ -131,6 +136,11 @@ DRESULT disk_ioctl (
res = RES_OK; res = RES_OK;
break; break;
case CTRL_TRIM:
// Inform device that data on the block of sectors is no longer used (optional)
res = RES_OK;
break;
default: default:
res = RES_PARERR; res = RES_PARERR;
} }

View File

@@ -124,6 +124,7 @@ function draw()
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width() / 2 - 15, game.height() / 2 - 30, "2048", true, 2) renderer.text_scaled(game.width() / 2 - 15, game.height() / 2 - 30, "2048", true, 2)
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 30, "Welcome Adolfo2!", true, 2)
elseif state == STATE_PLAYING or state == STATE_WIN or state == STATE_GAME_OVER then elseif state == STATE_PLAYING or state == STATE_WIN or state == STATE_GAME_OVER then
-- Draw grid -- Draw grid
@@ -138,14 +139,14 @@ function draw()
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false) renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false)
else else
-- Filled tile -- Filled tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, true) renderer.rect(tile_x+2, tile_y+2, tile_size-4, tile_size-4, true, true)
-- Draw value (simplified) -- Draw value (simplified)
local text = tostring(value) local text = tostring(value)
if string.len(text) <= 2 then if string.len(text) <= 2 then
renderer.text_scaled(tile_x + 2, tile_y + 2, text, false, 2) renderer.text_scaled(tile_x + tile_size / 2 - 4, tile_y + tile_size / 2, text, false, 2)
else else
renderer.text_scaled(tile_x + 1, tile_y + 2, text, false, 2) renderer.text_scaled(tile_x + tile_size / 2 - 8, tile_y + tile_size / 2, text, false, 2)
end end
end end
end end

View File

@@ -23,6 +23,15 @@ struct LuaGameFactoryData {
static std::vector<LuaGameFactoryData*> factory_data_list; static std::vector<LuaGameFactoryData*> factory_data_list;
void LuaGameLoader::clear_factory_data() {
// Delete all factory data and clear the vector
for (LuaGameFactoryData* data : factory_data_list) {
delete data;
}
factory_data_list.clear();
printf("LuaGameLoader: Cleared all factory data\n");
}
// Factory wrapper that captures script path // Factory wrapper that captures script path
static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height, static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui, LowLevelRenderer* renderer, LowLevelGUI* gui,

View File

@@ -20,6 +20,11 @@ public:
*/ */
static int register_all_games(GameLauncher* launcher); static int register_all_games(GameLauncher* launcher);
/**
* @brief Clear all factory data (useful before re-scanning)
*/
static void clear_factory_data();
private: private:
/** /**
* @brief Parse metadata from Lua script comments * @brief Parse metadata from Lua script comments

View File

@@ -8,6 +8,7 @@
#include "display/low_level_render.h" #include "display/low_level_render.h"
#include "display/low_level_gui.h" #include "display/low_level_gui.h"
#include <stdio.h> #include <stdio.h>
#include <cstring>
GameLauncher::GameLauncher(uint16_t width, uint16_t height, GameLauncher::GameLauncher(uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager) LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager)
@@ -212,6 +213,72 @@ void GameLauncher::reset() {
printf("Launcher reset - returning to menu\n"); 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 { int GameLauncher::get_total_pages() const {
if (games.empty()) return 1; if (games.empty()) return 1;
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE; return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;

View File

@@ -79,12 +79,24 @@ public:
*/ */
void reset(); void reset();
/**
* @brief Clear all registered games (useful before re-scanning)
*/
void clear_games();
/** /**
* @brief Check if a game is currently selected * @brief Check if a game is currently selected
* @return true if game selected, false if in menu * @return true if game selected, false if in menu
*/ */
bool is_game_selected() const { return selected_game != nullptr; } 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: private:
uint16_t width; uint16_t width;
uint16_t height; uint16_t height;

View File

@@ -323,7 +323,9 @@ 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) { 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 // For non-SDHC cards, convert block address to byte address
if (g_card_info.type != SD_CARD_TYPE_SDHC) { if (g_card_info.type != SD_CARD_TYPE_SDHC) {
@@ -334,11 +336,24 @@ bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) {
// Send write command // Send write command
uint8_t r1 = sd_card_send_command(SD_CMD24, block_addr); uint8_t r1 = sd_card_send_command(SD_CMD24, block_addr);
if (r1 != SD_R1_READY) { if (r1 != SD_R1_READY) {
sd_card_deselect(); sd_card_deselect();
return false; 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 // Send start token
sd_card_transfer(SD_START_TOKEN); sd_card_transfer(SD_START_TOKEN);
@@ -351,8 +366,15 @@ bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) {
sd_card_transfer(0xFF); sd_card_transfer(0xFF);
sd_card_transfer(0xFF); sd_card_transfer(0xFF);
// Check data response // Check data response - may need to read several bytes before getting response
uint8_t response = sd_card_transfer(0xFF); 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) { if ((response & 0x1F) != SD_DATA_ACCEPTED) {
sd_card_deselect(); sd_card_deselect();
return false; return false;

321
lib/serial_uploader.cpp Normal file
View 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
View 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

149
upload_game.py Executable file
View File

@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Lua Game Uploader for RP2350
Rapidly upload and execute Lua games via USB serial for quick iteration.
Usage:
python upload_game.py <lua_file> [serial_port]
Example:
python upload_game.py my_game.lua /dev/ttyACM0
python upload_game.py my_game.lua COM3
"""
import sys
import os
import base64
import serial
import time
from pathlib import Path
def find_serial_port():
"""Auto-detect the RP2350 serial port"""
import serial.tools.list_ports
# Look for Pico/RP2350 devices
ports = list(serial.tools.list_ports.comports())
for port in ports:
# Common VID/PID for Raspberry Pi Pico
if 'Pico' in port.description or \
'RP2350' in port.description or \
'RP2040' in port.description or \
(port.vid == 0x2E8A): # Raspberry Pi VID
return port.device
# Fallback: return first available port
if ports:
print(f"Warning: Could not find RP2350, using first available port: {ports[0].device}")
return ports[0].device
return None
def upload_game(lua_file, serial_port=None):
"""Upload a Lua game file to the RP2350 and execute it"""
# Check if file exists
if not os.path.exists(lua_file):
print(f"Error: File '{lua_file}' not found")
return False
# Read the file
with open(lua_file, 'rb') as f:
file_data = f.read()
file_size = len(file_data)
filename = os.path.basename(lua_file)
# Ensure filename has .lua extension
if not filename.endswith('.lua'):
filename += '.lua'
print(f"Uploading {filename} ({file_size} bytes)...")
# Auto-detect serial port if not provided
if serial_port is None:
serial_port = find_serial_port()
if serial_port is None:
print("Error: No serial port found. Please specify manually.")
return False
print(f"Using serial port: {serial_port}")
try:
# Open serial connection
ser = serial.Serial(serial_port, 115200, timeout=5)
time.sleep(0.1) # Wait for connection to stabilize
# Encode file data as base64
base64_data = base64.b64encode(file_data).decode('ascii')
# Send upload command
command = f"UPLOAD {filename} {file_size}\n"
print(f"Sending command: {command.strip()}")
ser.write(command.encode('ascii'))
# Send base64-encoded file data
print("Sending file data...")
ser.write(base64_data.encode('ascii'))
ser.write(b'\n')
# Send END marker
ser.write(b'END\n')
print("Upload complete, waiting for confirmation...")
# Read response from device
start_time = time.time()
response_lines = []
while time.time() - start_time < 10: # 10 second timeout
if ser.in_waiting > 0:
line = ser.readline().decode('utf-8', errors='ignore').strip()
if line:
print(f" << {line}")
response_lines.append(line)
# Check for success
if line.startswith('OK'):
print(f"✓ File written successfully!")
if line.startswith('LAUNCHED'):
game_name = line.split(' ', 1)[1] if ' ' in line else filename
print(f"✓ Game '{game_name}' launched!")
ser.close()
return True
# Check for error
if line.startswith('ERROR'):
print(f"✗ Upload failed: {line}")
ser.close()
return False
print("Warning: No response received from device")
ser.close()
return False
except serial.SerialException as e:
print(f"Serial error: {e}")
return False
except Exception as e:
print(f"Unexpected error: {e}")
return False
def main():
if len(sys.argv) < 2:
print(__doc__)
print("\nAvailable serial ports:")
try:
import serial.tools.list_ports
for port in serial.tools.list_ports.comports():
print(f" {port.device}: {port.description}")
except ImportError:
print(" (install pyserial to see available ports)")
sys.exit(1)
lua_file = sys.argv[1]
serial_port = sys.argv[2] if len(sys.argv) > 2 else None
success = upload_game(lua_file, serial_port)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()