| #include <cstdio> |
| #include <cstdlib> |
| #include <cstring> |
| #include <tuple> |
| |
| #include "hardware/flash.h" |
| |
| #include "usb.hpp" |
| #include "blit_launch.hpp" |
| #include "multiplayer.hpp" |
| #include "overlay.hpp" |
| |
| #include "engine/engine.hpp" |
| #include "executable.hpp" |
| |
| #define MAX_FILENAME 256+1 |
| #define MAX_FILELEN 16+1 |
| |
| void init(); |
| |
| static uint8_t cur_header[8]; |
| static int header_pos = 0; |
| static CDCCommand *cur_command = nullptr; |
| |
| static constexpr uint32_t to_cmd_id(const char str[4]) { |
| return str[0] | str[1] << 8 | str[2] << 16 | str[3] << 24; |
| } |
| |
| class CDCParseBuffer final { |
| public: |
| void reset() { |
| offset = 0; |
| } |
| |
| uint8_t *get_data() { |
| return buffer; |
| } |
| |
| uint8_t *get_current_ptr() { |
| return buffer + offset; |
| } |
| |
| void add_read(uint32_t count) { |
| offset += count; |
| assert(offset <= buffer_size); |
| } |
| |
| uint32_t get_offset() const { |
| return offset; |
| } |
| |
| private: |
| static const size_t buffer_size = std::max((unsigned)std::max(MAX_FILELEN, MAX_FILENAME), FLASH_PAGE_SIZE); |
| uint8_t buffer[buffer_size]; |
| |
| uint32_t offset = 0; |
| }; |
| |
| /* |
| Helper to read a string |
| returns Done when the string is ready (in buf.get_ptr()) |
| returns Error when we've reached the max length without a terminator |
| returns Continue when there is currently no more data to read |
| */ |
| static CDCCommand::Status cdc_read_string(CDCParseBuffer &buf, uint32_t max_len) { |
| while(true) { |
| auto buf_ptr = buf.get_current_ptr(); |
| |
| if(!usb_cdc_read(buf_ptr, 1)) |
| return CDCCommand::Status::Continue; |
| |
| // end of string |
| if(*buf_ptr == 0) |
| return CDCCommand::Status::Done; |
| |
| buf.add_read(1); |
| |
| // too long |
| if(buf.get_offset() == max_len) |
| return CDCCommand::Status::Error; |
| } |
| } |
| |
| // helper in an attempt to keep commands more generic... |
| static void cdc_command_progress(const char *message, uint32_t progress, uint32_t total) { |
| set_render_overlay_enabled(message != nullptr || total); |
| |
| // null message preserves the old one |
| if(message) |
| set_overlay_message(message); |
| |
| set_overlay_progress(progress, total); |
| } |
| |
| class CDCProgCommand final : public CDCCommand { |
| public: |
| CDCProgCommand(CDCParseBuffer &buf) : buf(buf) {} |
| |
| void init() override { |
| parse_state = ParseState::Filename; |
| buf.reset(); |
| } |
| |
| Status update() override { |
| |
| while(true) { |
| switch(parse_state) { |
| case ParseState::Filename: { |
| auto status = cdc_read_string(buf, MAX_FILENAME); |
| if(status == Status::Done) { |
| // setup progress message |
| char message_buf[300]; |
| snprintf(message_buf, sizeof(message_buf), "Saving %s... to flash", buf.get_data()); |
| cdc_command_progress(message_buf, 0, 0); |
| |
| parse_state = ParseState::Length; |
| buf.reset(); |
| continue; |
| } |
| |
| return status; |
| } |
| |
| case ParseState::Length: { |
| auto status = cdc_read_string(buf, MAX_FILELEN); |
| if(status == Status::Done) { |
| auto file_len = strtoul((const char *)buf.get_data(), nullptr, 10); |
| parse_state = ParseState::Data; |
| buf.reset(); |
| |
| cdc_command_progress(nullptr, 0, file_len); |
| |
| writer.init(file_len); |
| continue; |
| } |
| |
| return status; |
| } |
| |
| case ParseState::Data: { |
| // read data |
| auto max = std::min(uint32_t(FLASH_PAGE_SIZE), writer.get_remaining()) - buf.get_offset(); |
| auto read = usb_cdc_read(buf.get_current_ptr(), max); |
| |
| if(!read) |
| return Status::Continue; |
| |
| buf.add_read(read); |
| |
| // got full page or final part of file |
| auto buf_off = buf.get_offset(); |
| if(buf_off == FLASH_PAGE_SIZE || buf_off == writer.get_remaining()) { |
| if(!writer.write(buf.get_data(), buf_off)) |
| return Status::Error; |
| |
| cdc_command_progress(nullptr, writer.get_offset(), writer.get_length()); |
| |
| buf.reset(); |
| } |
| |
| // end of file |
| if(writer.get_remaining() == 0) { |
| cdc_command_progress(nullptr, 0, 0); // clear progress |
| |
| // send response |
| auto block = writer.get_flash_offset() >> 16; |
| uint8_t res_data[]{'3', '2', 'B', 'L', '_', '_', 'O', 'K', uint8_t(block), uint8_t(block >> 8)}; |
| usb_cdc_write(res_data, sizeof(res_data)); |
| usb_cdc_flush_write(); |
| |
| // reinit loader |
| ::init(); |
| |
| return Status::Done; |
| } |
| |
| break; |
| } |
| } |
| } |
| |
| return Status::Continue; |
| } |
| |
| enum class ParseState { |
| Filename, |
| Length, |
| Data |
| } parse_state = ParseState::Filename; |
| |
| CDCParseBuffer &buf; |
| |
| BlitWriter writer; |
| }; |
| |
| class CDCSaveCommand final : public CDCCommand { |
| public: |
| CDCSaveCommand(CDCParseBuffer &buf) : buf(buf) {} |
| |
| void init() override { |
| parse_state = ParseState::Filename; |
| file_offset = 0; |
| buf.reset(); |
| } |
| |
| Status update() override { |
| while(true) { |
| switch(parse_state) { |
| case ParseState::Filename: { |
| auto status = cdc_read_string(buf, MAX_FILENAME); |
| if(status == Status::Done) { |
| auto filename = (const char *)buf.get_data(); |
| |
| file = blit::api.open_file(filename, blit::OpenMode::write); |
| if(!file) { |
| blit::debugf("Failed to open %s", filename); |
| return Status::Error; |
| } |
| |
| // setup progress message |
| char message_buf[300]; |
| snprintf(message_buf, sizeof(message_buf), "Saving %s...", filename); |
| cdc_command_progress(message_buf, 0, 0); |
| |
| parse_state = ParseState::Length; |
| buf.reset(); |
| continue; |
| } |
| |
| return status; |
| } |
| |
| case ParseState::Length: { |
| auto status = cdc_read_string(buf, MAX_FILELEN); |
| if(status == Status::Done) { |
| file_length = strtoul((const char *)buf.get_data(), nullptr, 10); |
| |
| cdc_command_progress(nullptr, 0, file_length); |
| |
| parse_state = ParseState::Data; |
| buf.reset(); |
| continue; |
| } |
| |
| return status; |
| } |
| |
| case ParseState::Data: { |
| // read data |
| auto max = std::min(uint32_t(FLASH_PAGE_SIZE), file_length - file_offset); |
| auto read = usb_cdc_read(buf.get_current_ptr(), max); |
| |
| if(!read) |
| return Status::Continue; |
| |
| // write whatever we got, the fs is going to buffer anyway |
| auto written = blit::api.write_file(file, file_offset, read, (const char *)buf.get_data()); |
| if(written != read) { |
| blit::api.close_file(file); |
| return Status::Error; |
| } |
| |
| file_offset += read; |
| |
| cdc_command_progress(nullptr, file_offset, file_length); |
| |
| // end of file |
| if(file_offset == file_length) { |
| blit::api.close_file(file); |
| cdc_command_progress(nullptr, 0, 0); // clear progress |
| |
| // send response |
| uint8_t res_data[]{'3', '2', 'B', 'L', '_', '_', 'O', 'K'}; |
| usb_cdc_write(res_data, sizeof(res_data)); |
| usb_cdc_flush_write(); |
| |
| return Status::Done; |
| } |
| |
| break; |
| } |
| } |
| } |
| |
| return Status::Continue; |
| } |
| |
| enum class ParseState { |
| Filename, |
| Length, |
| Data |
| } parse_state = ParseState::Filename; |
| |
| CDCParseBuffer &buf; |
| |
| void *file = nullptr; |
| uint32_t file_length = 0, file_offset = 0; |
| }; |
| |
| class CDCListCommand final : public CDCCommand { |
| void init() override { |
| } |
| |
| Status update() override { |
| blit::api.list_installed_games([](const uint8_t *ptr, uint32_t block, uint32_t size) { |
| // bit of a mismatch between the API and the CDC API |
| // this wants offset in bytes and the size WITHOUT metadata |
| uint32_t offset_bytes = block * game_block_size; |
| uint32_t header_size = ((BlitGameHeader *)ptr)->end & 0x1FFFFFF; |
| |
| usb_cdc_write((uint8_t *)&offset_bytes, sizeof(uint32_t)); |
| usb_cdc_write((uint8_t *)&header_size, sizeof(uint32_t)); |
| usb_cdc_flush_write(); |
| |
| // write metadata if found |
| auto meta = ptr + header_size; |
| |
| if(memcmp(meta, "BLITMETA", 8) == 0) { |
| auto meta_size = *(uint16_t *)(meta + 8); |
| usb_cdc_write(meta, meta_size + 10); |
| } else { |
| // no meta, write header + 0 len |
| usb_cdc_write((const uint8_t *)"BLITMETA\0", 10); |
| } |
| usb_cdc_flush_write(); |
| }); |
| |
| // end marker |
| uint32_t end = 0xFFFFFFFF; |
| usb_cdc_write((uint8_t *)&end, sizeof(uint32_t)); |
| usb_cdc_flush_write(); |
| |
| return Status::Done; |
| } |
| }; |
| |
| class CDCLaunchCommand final : public CDCCommand { |
| public: |
| CDCLaunchCommand(CDCParseBuffer &buf) : buf(buf) {} |
| |
| void init() override { |
| buf.reset(); |
| } |
| |
| Status update() override { |
| auto status = cdc_read_string(buf, MAX_FILENAME); |
| |
| if(status == Status::Done) |
| blit::api.launch((const char *)buf.get_data()); |
| |
| return status; |
| } |
| |
| CDCParseBuffer &buf; |
| }; |
| |
| class CDCEraseCommand final : public CDCCommand { |
| public: |
| CDCEraseCommand(CDCParseBuffer &buf) : buf(buf) {} |
| |
| void init() override { |
| buf.reset(); |
| } |
| |
| Status update() override { |
| while(true) { |
| auto buf_ptr = buf.get_current_ptr(); |
| if(!usb_cdc_read(buf_ptr, 1)) |
| return Status::Continue; |
| |
| buf.add_read(1); |
| |
| // end of word |
| if(buf.get_offset() == 4) { |
| blit::api.erase_game(*(uint32_t *)buf.get_data()); |
| return Status::Done; |
| } |
| } |
| |
| return Status::Continue; |
| } |
| |
| CDCParseBuffer &buf; |
| }; |
| |
| |
| static CDCHandshakeCommand handshake_command; |
| static CDCUserCommand user_command; |
| |
| #if defined(BUILD_LOADER) && !defined(USB_HOST) |
| #define FLASH_COMMANDS |
| static CDCParseBuffer parse_buffer; |
| static CDCProgCommand prog_command(parse_buffer); |
| static CDCSaveCommand save_command(parse_buffer); |
| static CDCListCommand list_command; |
| static CDCLaunchCommand launch_command(parse_buffer); |
| static CDCEraseCommand erase_command(parse_buffer); |
| #endif |
| |
| const std::tuple<uint32_t, CDCCommand *> cdc_commands[]{ |
| {to_cmd_id("MLTI"), &handshake_command}, |
| {to_cmd_id("USER"), &user_command}, |
| |
| #ifdef FLASH_COMMANDS |
| {to_cmd_id("PROG"), &prog_command}, |
| {to_cmd_id("SAVE"), &save_command}, |
| {to_cmd_id("__LS"), &list_command}, |
| {to_cmd_id("LNCH"), &launch_command}, |
| {to_cmd_id("ERSE"), &erase_command}, |
| #endif |
| }; |
| |
| void usb_cdc_update() { |
| while(usb_cdc_read_available()) { |
| // command in progress |
| if(cur_command) { |
| auto res = cur_command->update(); |
| |
| if(res == CDCCommand::Status::Continue) { |
| break; |
| } else { |
| // done/error |
| cur_command = nullptr; |
| } |
| |
| continue; |
| } |
| |
| // match header |
| if(header_pos < 8) { |
| usb_cdc_read(cur_header + header_pos, 1); |
| |
| const char *expected = "32BL"; |
| if(header_pos >= 4 || cur_header[header_pos] == expected[header_pos]) |
| header_pos++; |
| else |
| header_pos = 0; |
| } else { |
| |
| // got header |
| auto command_id = to_cmd_id((char *)cur_header + 4); |
| |
| // find command |
| for(auto &cmd : cdc_commands) { |
| if(std::get<0>(cmd) == command_id) { |
| cur_command = std::get<1>(cmd); |
| break; |
| } |
| } |
| |
| if(!cur_command) |
| printf("got: %c%c%c%c%c%c%c%c\n", cur_header[0], cur_header[1], cur_header[2], cur_header[3], cur_header[4], cur_header[5], cur_header[6], cur_header[7]); |
| else |
| cur_command->init(); |
| |
| header_pos = 0; |
| } |
| } |
| } |