blob: 4449fe27084f29f5152f523ec22dcecc4bf12c1c [file] [log] [blame]
#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;
}
}
}