Compare commits

..

6 Commits

Author SHA1 Message Date
Adolfo Reyna e406a06f61 Fix race condition in display refresh locking 2026-02-13 13:40:45 -05:00
Adolfo Reyna 034867d2a7 fix sneak game 2026-02-13 13:36:35 -05:00
Adolfo Reyna 76e3d2435e Make game name search case-insensitive
Serial uploader searches for games by filename (e.g., "tetris") but
games are registered by their metadata NAME field (e.g., "Tetris").
This caused launch failures when case didn't match.

Changed select_game_by_name() to use case-insensitive matching:
- strcasecmp() for exact match
- tolower() both strings before strstr() for partial match

Now uploading "tetris.lua" will successfully match and launch "Tetris".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 23:27:48 -05:00
Adolfo Reyna 518bc054c4 Fix SPI bus contention crash on serial game upload
Serial uploader was crashing the Pico when launching games because
it accessed SD card (SPI) while Core 1 was refreshing display (also SPI).
Display and SD card share the same SPI bus and cannot be accessed
simultaneously.

Split game launch into prepare and execute phases:
- prepare: Re-scan games directory (safe, SD access done immediately)
- execute: Load Lua script from SD (deferred until display is idle)

Main loop now checks !is_refresh_in_progress() before completing
launch, preventing SPI conflicts.

Also updated SD card best practices skill to document SPI bus
contention as the #1 most critical issue to avoid.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 23:25:31 -05:00
Adolfo Reyna f8fb04db1b improve game 2026-02-12 23:09:13 -05:00
Adolfo Reyna 84b009c33e 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>
2026-02-12 22:52:57 -05:00
16 changed files with 1934 additions and 45 deletions
+484
View File
@@ -0,0 +1,484 @@
# 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 Bus Contention (CRITICAL)
### ⚠️ CRITICAL: Display and SD Card Share the Same SPI Bus
**The display and SD card use the same SPI bus and CANNOT be accessed simultaneously.** Attempting to do so will cause the Pico to crash or behave unpredictably.
### Real-World Example: Game Launch Crash
The serial uploader originally crashed when launching games because:
1. `SerialUploader::launch_game()` writes file to SD (SPI)
2. Immediately calls `select_game_by_name()`
3. `LuaGame::load_script()` reads from SD (SPI)
4. **Meanwhile**, Core 1 is refreshing the display (also SPI)
5. **CRASH** due to simultaneous SPI access
### Solution: Wait for Display to Be Idle
Before any SD card operation that isn't already protected, ensure no display refresh is in progress:
```cpp
// In main loop:
if (serial_uploader.wants_to_launch_game() && !is_refresh_in_progress()) {
// Safe to launch now - no SPI conflict with display
bool game_launched = serial_uploader.complete_launch();
// ...
}
```
### Key Patterns for Avoiding Contention
**Pattern 1: Check `is_refresh_in_progress()` before SD operations**
```cpp
if (!is_refresh_in_progress()) {
uint prev_speed = sd_card_set_spi_speed();
// SD card operations
sd_card_restore_spi_speed(prev_speed);
}
```
**Pattern 2: Split operations into "prepare" and "execute" phases**
```cpp
// Phase 1: Prepare (safe, no SD access)
void prepare_operation() {
state = READY_TO_EXECUTE;
}
// Phase 2: Execute (only when !is_refresh_in_progress())
bool execute_operation() {
uint prev_speed = sd_card_set_spi_speed();
// SD card operations here
sd_card_restore_spi_speed(prev_speed);
}
```
**Pattern 3: Keep main loop responsive**
```cpp
while (1) {
// Check if we need to do SD operation
if (needs_sd_operation && !is_refresh_in_progress()) {
perform_sd_operation();
}
// Don't sleep if waiting for SD operation window
bool stay_awake = pending_refresh || needs_sd_operation;
if (!stay_awake) {
__wfi(); // Sleep until interrupt
}
}
```
### When This Matters Most
- **Game loading**: Reading Lua scripts from SD during game launch
- **Serial uploads**: Writing files and immediately loading them
- **Save/load operations**: Writing game state to SD
- **Directory scanning**: Re-scanning games while display is active
### The Core Architecture
This project uses **dual-core display refresh**:
- **Core 0**: Main logic, input processing, game updates, SD card operations
- **Core 1**: Display refresh (writes framebuffer to display via SPI)
Core 1 runs asynchronously, so you must explicitly check `is_refresh_in_progress()` before SD operations.
## 2. 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 (fast)
- **SD Card**: 12.5 MHz (slower, more reliable)
**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: Access SD Card During Display Refresh (MOST CRITICAL)
```cpp
// BAD - Will crash the Pico!
void launch_game() {
scan_games(); // Reads SD card
selected_game = create_game(); // Reads Lua script from SD
}
// Called directly without checking if display is refreshing
```
### ✅ DO: Wait for Display to Be Idle
```cpp
// GOOD - Wait for safe window
if (!is_refresh_in_progress()) {
uint prev_speed = sd_card_set_spi_speed();
scan_games();
selected_game = create_game();
sd_card_restore_spi_speed(prev_speed);
}
```
### ❌ 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;
}
```
## 9. Testing Checklist
When implementing new SD card functionality:
- [ ] **SPI bus contention checked** - verify `!is_refresh_in_progress()` before SD operations
- [ ] 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. **⚠️ CRITICAL: Avoid SPI bus contention** - Check `!is_refresh_in_progress()` before SD operations (display and SD share SPI bus)
2. **Always manage SPI speed** around FatFS operations
3. **Poll for SD card responses** - don't assume immediate response
4. **Check error codes** on every operation
5. **Clean up memory** before creating new game instances
6. **Write in chunks** for large files
7. **Sync before closing** to ensure data is written
Following these practices will save hours of debugging SD card issues!
+1
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
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! 🎮
+29 -3
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
@@ -87,7 +88,7 @@ void core1_entry() {
while (1) { while (1) {
// Wait for refresh request // Wait for refresh request
if (refresh_requested && refresh_buffer && refresh_display) { if (refresh_requested && refresh_buffer && refresh_display) {
refresh_in_progress = true; // refresh_in_progress is already set by Core 0 to lock the buffer
// Get local copies for safe access // Get local copies for safe access
LowLevelDisplay* display = refresh_display; LowLevelDisplay* display = refresh_display;
@@ -99,7 +100,7 @@ void core1_entry() {
// Clear flags // Clear flags
refresh_requested = false; refresh_requested = false;
refresh_in_progress = false; refresh_in_progress = false; // Unlock buffer for Core 0
} }
// Small delay to avoid busy-waiting // Small delay to avoid busy-waiting
@@ -123,6 +124,10 @@ bool refresh_screen_async(const uint8_t *buffer, LowLevelDisplay* display) {
return false; return false;
} }
// Lock the buffer immediately on Core 0 to prevent race condition
// Core 1 will unlock it (set to false) when done
refresh_in_progress = true;
// Queue refresh on Core 1 // Queue refresh on Core 1
refresh_buffer = buffer; refresh_buffer = buffer;
refresh_display = display; refresh_display = display;
@@ -437,6 +442,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,10 +572,28 @@ 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)
serial_uploader.process();
// If serial uploader wants to launch a game, wait until it's safe (no display refresh)
if (serial_uploader.wants_to_launch_game() && !is_refresh_in_progress()) {
// Safe to launch now - no SPI conflict with display
bool game_launched = serial_uploader.complete_launch();
if (game_launched) {
// 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;
if (serial_uploader.wants_to_launch_game()) stay_awake = true; // Don't sleep while waiting to launch
if (launcher.is_game_selected()) { if (launcher.is_game_selected()) {
Game* g = launcher.get_selected_game(); Game* g = launcher.get_selected_game();
@@ -581,7 +608,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();
+12 -2
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;
} }
+22 -5
View File
@@ -49,7 +49,7 @@ function init()
game.vars.won = false game.vars.won = false
-- Enable continuous updates -- Enable continuous updates
game.set_frame_updates(true) -- game.set_frame_updates(true)
print("2048 initialized") print("2048 initialized")
end end
@@ -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 Adolfo", 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
@@ -137,15 +138,31 @@ function draw()
-- Empty tile -- Empty tile
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
local reduce_size
if value == 2 then
reduce_size = 10
elseif value == 4 then
reduce_size = 8
elseif value == 8 then
reduce_size = 6
elseif value == 16 then
reduce_size = 4
elseif value == 32 then
reduce_size = 2
else
reduce_size = 0
end
-- Empty tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false)
-- Filled tile -- Filled tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, true) renderer.rect(tile_x+reduce_size, tile_y+reduce_size, tile_size-reduce_size*2, tile_size-reduce_size*2, 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 - 8, 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 - 12, tile_y + tile_size / 2 - 8, text, false, 2)
end end
end end
end end
+7 -7
View File
@@ -110,12 +110,12 @@ function draw()
-- Draw: MENU -- Draw: MENU
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2 - 20, "SNAKE", true) renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "SNAKE", true)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2, "Tap to Start", true) renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
if game.vars.high_score > 0 then if game.vars.high_score > 0 then
local hs_text = "High: " .. tostring(game.vars.high_score) local hs_text = "High: " .. tostring(game.vars.high_score)
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2 + 20, hs_text, true) renderer.text(game.width() / 2 - 30, game.height() / 2 + 20, hs_text, true)
end end
-- Draw: PLAYING -- Draw: PLAYING
@@ -134,16 +134,16 @@ function draw()
-- Draw score -- Draw score
local score_text = "Score: " .. tostring(game.vars.score) local score_text = "Score: " .. tostring(game.vars.score)
renderer.text_scaled(5, 5, score_text, true, 2) renderer.text(5, 5, score_text, true)
-- Draw: GAME_OVER -- Draw: GAME_OVER
elseif state == STATE_GAME_OVER then elseif state == STATE_GAME_OVER then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 20, "GAME OVER", true) renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
local score_text = "Score: " .. tostring(game.vars.score) local score_text = "Score: " .. tostring(game.vars.score)
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2, score_text, true) renderer.text(game.width() / 2 - 40, game.height() / 2, score_text, true)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true) renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true)
end end
end end
+9
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,
+5
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
+86
View File
@@ -8,6 +8,8 @@
#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>
#include <cctype>
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 +214,90 @@ 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 (case-insensitive)
for (size_t i = 0; i < games.size(); i++) {
if (strcasecmp(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 (case-insensitive, newest files first)
for (int i = games.size() - 1; i >= 0; i--) {
// Convert both strings to lowercase for comparison
char game_name_lower[256];
char search_name_lower[256];
strncpy(game_name_lower, games[i].name, sizeof(game_name_lower) - 1);
game_name_lower[sizeof(game_name_lower) - 1] = '\0';
strncpy(search_name_lower, name, sizeof(search_name_lower) - 1);
search_name_lower[sizeof(search_name_lower) - 1] = '\0';
// Convert to lowercase
for (size_t j = 0; game_name_lower[j]; j++) {
game_name_lower[j] = tolower(game_name_lower[j]);
}
for (size_t j = 0; search_name_lower[j]; j++) {
search_name_lower[j] = tolower(search_name_lower[j]);
}
if (strstr(game_name_lower, search_name_lower) != 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;
+12
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;
+25 -3
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;
+330
View File
@@ -0,0 +1,330 @@
#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() {
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;
// 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;
}
+59
View File
@@ -0,0 +1,59 @@
#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();
// Check if uploader wants to launch a game (after upload complete)
bool wants_to_launch_game() const { return state == LAUNCHING_GAME; }
// Complete the game launch (call only when safe - no display refresh in progress)
// Returns true if launch succeeded
bool complete_launch();
// 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
+528
View File
@@ -0,0 +1,528 @@
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (215,122)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (203,137)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (312,131)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (355,140)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (376,273)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (357,277)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (323,258)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (321,261)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (89,291)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (327,264)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (315,250)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (262,151)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (319,150)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
Long press detected - returning to launcher
Launcher reset - returning to menu
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (358,157)
Touch at (358,157) in launcher
Selected game: Snake Game
Lua bindings registered
LuaGame Error [load script]: [string "/games/SNAKE.LUA"]:113: unexpected symbol near ','
LuaGame: Failed to load /games/SNAKE.LUA: load script: [string "/games/SNAKE.LUA"]:113: unexpected symbol near ','
Game launched successfully
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (164,97)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (194,215)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (288,289)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (285,169)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (255,117)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (168,100)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
Long press detected - returning to launcher
Launcher reset - returning to menu
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (424,264)
Touch at (424,264) in launcher
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (389,237)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (392,247)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (399,249)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (138,49)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (125,111)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (80,146)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (125,37)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (100,73)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (82,154)
INT: RISE
INT: FALL
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (120,255)
Touch at (120,255) in launcher
INT: RISE
INT: FALL
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (191,125)
Touch at (191,125) in launcher
Selected game: Touch Counter
Lua bindings registered
LuaGame Error [load script]: [string "/games/COUNTER.LUA"]:36: unexpected symbol near ','
LuaGame: Failed to load /games/COUNTER.LUA: load script: [string "/games/COUNTER.LUA"]:36: unexpected symbol near ','
Game launched successfully
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (165,106)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (121,224)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (253,160)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (258,158)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (256,156)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (255,154)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (295,161)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (296,157)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
INT: RISE
Touch DOWN at (477,202)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (236,221)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (338,182)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
TFT: Dimmed to 5%
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (306,162)
TFT: Restored brightness
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (338,116)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (208,258)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (232,55)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (37,160)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (224,208)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (53,150)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (225,239)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (247,231)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (152,187)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (426,157)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (198,272)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (384,191)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (44,195)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (283,266)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (50,215)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (287,265)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (189,275)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (421,142)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (43,192)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (390,155)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (48,204)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (258,58)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (275,284)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (23,202)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (173,173)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (121,151)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (71,119)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (220,109)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (313,124)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (235,140)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (131,185)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (269,54)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (227,165)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
Executable
+149
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()