Compare commits
4 Commits
b26f3bf775
...
76e3d2435e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76e3d2435e | ||
|
|
518bc054c4 | ||
|
|
f8fb04db1b | ||
|
|
84b009c33e |
484
.claude/skills/sd_card_best_practices.md
Normal file
484
.claude/skills/sd_card_best_practices.md
Normal 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!
|
||||
@@ -48,6 +48,7 @@ add_executable(basic1
|
||||
basic1.cpp
|
||||
lib/input_manager.cpp
|
||||
lib/game_launcher.cpp
|
||||
lib/serial_uploader.cpp
|
||||
games/tic_tac_toe.cpp
|
||||
games/demo_game.cpp
|
||||
games/monopoly/monopoly_game.cpp
|
||||
|
||||
151
UPLOAD_TOOL.md
Normal file
151
UPLOAD_TOOL.md
Normal 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! 🎮
|
||||
24
basic1.cpp
24
basic1.cpp
@@ -58,6 +58,7 @@ extern "C" {
|
||||
#include "demo_game.h"
|
||||
#include "monopoly_game.h"
|
||||
#include "lua_game_loader.h"
|
||||
#include "serial_uploader.h"
|
||||
|
||||
|
||||
// Binary info for RP2350 - ensures proper boot image structure
|
||||
@@ -437,6 +438,10 @@ int main()
|
||||
// Create GameLauncher
|
||||
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
|
||||
launcher.register_game("Tic-Tac-Toe", "Classic 2-player game",
|
||||
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* {
|
||||
@@ -563,10 +568,28 @@ int main()
|
||||
const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms
|
||||
uint32_t last_frame_time = 0;
|
||||
|
||||
bool needs_refresh = false; // Track if screen needs redraw
|
||||
|
||||
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
|
||||
bool stay_awake = false;
|
||||
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()) {
|
||||
Game* g = launcher.get_selected_game();
|
||||
@@ -581,7 +604,6 @@ int main()
|
||||
}
|
||||
|
||||
InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false};
|
||||
bool needs_refresh = false;
|
||||
|
||||
// 1. Process button input first (higher priority)
|
||||
input = input_manager.process_button_input();
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
#include "diskio.h"
|
||||
#include "sd_card.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Definitions of physical drive number for each drive */
|
||||
#define DEV_SD 0 /* SD card */
|
||||
@@ -21,7 +23,7 @@ DSTATUS disk_status (
|
||||
if (pdrv != DEV_SD) return STA_NOINIT;
|
||||
|
||||
// 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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -131,6 +136,11 @@ DRESULT disk_ioctl (
|
||||
res = RES_OK;
|
||||
break;
|
||||
|
||||
case CTRL_TRIM:
|
||||
// Inform device that data on the block of sectors is no longer used (optional)
|
||||
res = RES_OK;
|
||||
break;
|
||||
|
||||
default:
|
||||
res = RES_PARERR;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ function init()
|
||||
game.vars.won = false
|
||||
|
||||
-- Enable continuous updates
|
||||
game.set_frame_updates(true)
|
||||
-- game.set_frame_updates(true)
|
||||
|
||||
print("2048 initialized")
|
||||
end
|
||||
@@ -124,6 +124,7 @@ function draw()
|
||||
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 - 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
|
||||
-- Draw grid
|
||||
@@ -137,15 +138,31 @@ function draw()
|
||||
-- Empty tile
|
||||
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false)
|
||||
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
|
||||
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)
|
||||
local text = tostring(value)
|
||||
if string.len(text) <= 2 then
|
||||
renderer.text_scaled(tile_x + 2, tile_y + 2, text, false, 2)
|
||||
if string.len(text) < 2 then
|
||||
renderer.text_scaled(tile_x + tile_size / 2 - 4, tile_y + tile_size / 2 - 8, text, false, 2)
|
||||
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
|
||||
|
||||
@@ -23,6 +23,15 @@ struct LuaGameFactoryData {
|
||||
|
||||
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
|
||||
static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height,
|
||||
LowLevelRenderer* renderer, LowLevelGUI* gui,
|
||||
|
||||
@@ -20,6 +20,11 @@ public:
|
||||
*/
|
||||
static int register_all_games(GameLauncher* launcher);
|
||||
|
||||
/**
|
||||
* @brief Clear all factory data (useful before re-scanning)
|
||||
*/
|
||||
static void clear_factory_data();
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Parse metadata from Lua script comments
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "display/low_level_render.h"
|
||||
#include "display/low_level_gui.h"
|
||||
#include <stdio.h>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
|
||||
GameLauncher::GameLauncher(uint16_t width, uint16_t height,
|
||||
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager)
|
||||
@@ -212,6 +214,90 @@ void GameLauncher::reset() {
|
||||
printf("Launcher reset - returning to menu\n");
|
||||
}
|
||||
|
||||
void GameLauncher::clear_games() {
|
||||
// Clean up currently selected game first
|
||||
if (selected_game) {
|
||||
delete selected_game;
|
||||
selected_game = nullptr;
|
||||
}
|
||||
|
||||
// Clear the games vector
|
||||
games.clear();
|
||||
selected_index = 0;
|
||||
current_page = 0;
|
||||
printf("Launcher: Cleared all registered games\n");
|
||||
}
|
||||
|
||||
bool GameLauncher::select_game_by_name(const char* name) {
|
||||
// Clean up old game first if one exists
|
||||
if (selected_game) {
|
||||
printf("Cleaning up previous game...\n");
|
||||
delete selected_game;
|
||||
selected_game = nullptr;
|
||||
}
|
||||
|
||||
// First pass: search for exact match (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 {
|
||||
if (games.empty()) return 1;
|
||||
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;
|
||||
|
||||
@@ -79,12 +79,24 @@ public:
|
||||
*/
|
||||
void reset();
|
||||
|
||||
/**
|
||||
* @brief Clear all registered games (useful before re-scanning)
|
||||
*/
|
||||
void clear_games();
|
||||
|
||||
/**
|
||||
* @brief Check if a game is currently selected
|
||||
* @return true if game selected, false if in menu
|
||||
*/
|
||||
bool is_game_selected() const { return selected_game != nullptr; }
|
||||
|
||||
/**
|
||||
* @brief Select a game by name (for programmatic launching)
|
||||
* @param name Game name to select (partial match supported)
|
||||
* @return true if game was found and launched, false otherwise
|
||||
*/
|
||||
bool select_game_by_name(const char* name);
|
||||
|
||||
private:
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
|
||||
@@ -323,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) {
|
||||
if (!g_card_info.initialized || buffer == NULL) return false;
|
||||
if (!g_card_info.initialized || buffer == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For non-SDHC cards, convert block address to byte address
|
||||
if (g_card_info.type != SD_CARD_TYPE_SDHC) {
|
||||
@@ -334,11 +336,24 @@ bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) {
|
||||
|
||||
// Send write command
|
||||
uint8_t r1 = sd_card_send_command(SD_CMD24, block_addr);
|
||||
|
||||
if (r1 != SD_R1_READY) {
|
||||
sd_card_deselect();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wait for card to be ready to receive data
|
||||
uint32_t timeout_count = 0;
|
||||
uint8_t ready_byte;
|
||||
do {
|
||||
ready_byte = sd_card_transfer(0xFF);
|
||||
timeout_count++;
|
||||
if (timeout_count > 1000) {
|
||||
sd_card_deselect();
|
||||
return false;
|
||||
}
|
||||
} while (ready_byte != 0xFF);
|
||||
|
||||
// Send start token
|
||||
sd_card_transfer(SD_START_TOKEN);
|
||||
|
||||
@@ -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);
|
||||
|
||||
// Check data response
|
||||
uint8_t response = sd_card_transfer(0xFF);
|
||||
// Check data response - may need to read several bytes before getting response
|
||||
uint8_t response = 0xFF;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
response = sd_card_transfer(0xFF);
|
||||
if (response != 0xFF) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((response & 0x1F) != SD_DATA_ACCEPTED) {
|
||||
sd_card_deselect();
|
||||
return false;
|
||||
|
||||
330
lib/serial_uploader.cpp
Normal file
330
lib/serial_uploader.cpp
Normal 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
lib/serial_uploader.h
Normal file
59
lib/serial_uploader.h
Normal 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
screenlog.0
Normal file
528
screenlog.0
Normal 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
|
||||
149
upload_game.py
Executable file
149
upload_game.py
Executable 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()
|
||||
Reference in New Issue
Block a user