#include <Preferences.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "esp_spiffs.h"
#include "esp_task_wdt.h"
#include <stdio.h>
#include "machine.h"
#include "progs/embedded/blue_star.h"
#include "progs/embedded/omega_fury.h"
#include "progs/embedded/quikman.h"
#include "progs/embedded/splatform.h"
#include "progs/embedded/demo_birthday.h"
#include "progs/embedded/arukanoido.h"
#include "progs/embedded/demo_bah_bah_final.h"
#include "progs/embedded/demo_digit.h"
#include "progs/embedded/dragonwing.h"
#include "progs/embedded/kikstart.h"
#include "progs/embedded/kweepoutmc.h"
#include "progs/embedded/nibbler.h"
#include "progs/embedded/pooyan.h"
#include "progs/embedded/popeye.h"
#include "progs/embedded/pulse.h"
#include "progs/embedded/spikes.h"
#include "progs/embedded/tank_battalion.h"
#include "progs/embedded/tetris_plus.h"
#include "progs/embedded/astro-panic.h"
#include "progs/embedded/frogger07.h"
#include "progs/embedded/froggie.h"
#include "progs/embedded/tammerfors.h"
char const * LIST_URL = "http://cloud.cbm8bit.com/adamcost/vic20list.txt";
constexpr int MAXLISTSIZE = 16384;
char const * ROOTDIR = "/spiffs";
char const * EMBDIR = "Free";
#define DEBUG 0
struct EmbeddedProgDef {
char const * filename;
uint8_t const * data;
int size;
};
const EmbeddedProgDef embeddedProgs[] = {
{ arukanoido_filename, arukanoido_prg, sizeof(arukanoido_prg) },
{ dragonwing_filename, dragonwing_prg, sizeof(dragonwing_prg) },
{ kikstart_filename, kikstart_prg, sizeof(kikstart_prg) },
{ kweepoutmc_filename, kweepoutmc_prg, sizeof(kweepoutmc_prg) },
{ nibbler_filename, nibbler_prg, sizeof(nibbler_prg) },
{ pooyan_filename, pooyan_prg, sizeof(pooyan_prg) },
{ popeye_filename, popeye_prg, sizeof(popeye_prg) },
{ pulse_filename, pulse_prg, sizeof(pulse_prg) },
{ spikes_filename, spikes_prg, sizeof(spikes_prg) },
{ tank_battalion_filename, tank_battalion_prg, sizeof(tank_battalion_prg) },
{ tetris_plus_filename, tetris_plus_prg, sizeof(tetris_plus_prg) },
{ demo_birthday_filename, demo_birthday_prg, sizeof(demo_birthday_prg) },
{ omega_fury_filename, omega_fury_prg, sizeof(omega_fury_prg) },
{ splatform_filename, splatform_prg, sizeof(splatform_prg) },
{ quikman_filename, quikman_prg, sizeof(quikman_prg) },
{ astro_panic_filename, astro_panic_prg, sizeof(astro_panic_prg) },
{ frogger07_filename, frogger07_prg, sizeof(frogger07_prg) },
{ froggie_filename, froggie_prg, sizeof(froggie_prg) },
{ tammerfors_filename, tammerfors_prg, sizeof(tammerfors_prg) },
};
#if DEBUG
static volatile int scycles = 0;
static volatile bool restart = false;
void vTimerCallback1SecExpired(xTimerHandle pxTimer)
{
Serial.printf("%d - ", scycles / 5);
Serial.printf("Free memory (total, min, largest): %d, %d, %d\n", heap_caps_get_free_size(0), heap_caps_get_minimum_free_size(0), heap_caps_get_largest_free_block(0));
restart = true;
}
#endif
Preferences preferences;
Machine machine;
void initSPIFFS()
{
esp_vfs_spiffs_conf_t conf = {
.base_path = ROOTDIR,
.partition_label = NULL,
.max_files = 2,
.format_if_mount_failed = true
};
fabgl::suspendInterrupts();
esp_vfs_spiffs_register(&conf);
fabgl::resumeInterrupts();
}
void copyEmbeddedPrograms()
{
auto dir = FileBrowser();
dir.setDirectory(ROOTDIR);
if (!dir.exists(EMBDIR)) {
dir.makeDirectory(EMBDIR);
dir.changeDirectory(EMBDIR);
for (int i = 0; i < sizeof(embeddedProgs) / sizeof(EmbeddedProgDef); ++i) {
int fullpathlen = dir.getFullPath(embeddedProgs[i].filename);
char fullpath[fullpathlen];
dir.getFullPath(embeddedProgs[i].filename, fullpath, fullpathlen);
FILE * f = fopen(fullpath, "wb");
if (f) {
fwrite(embeddedProgs[i].data, 1, embeddedProgs[i].size, f);
fclose(f);
}
}
}
}
struct DownloadProgressFrame : public uiFrame {
uiLabel * label1;
uiLabel * label2;
uiButton * button;
DownloadProgressFrame(uiFrame * parent)
: uiFrame(parent, "Download", Point(50, 100), Size(150, 110), false) {
frameProps().resizeable = false;
frameProps().moveable = false;
frameProps().hasCloseButton = false;
frameProps().hasMaximizeButton = false;
frameProps().hasMinimizeButton = false;
label1 = new uiLabel(this, "", Point(10, 25));
label2 = new uiLabel(this, "", Point(10, 45));
button = new uiButton(this, "Abort", Point(50, 75), Size(50, 20));
button->onClick = [&]() { exitModal(1); };
}
};
struct HelpFame : public uiFrame {
HelpFame(uiFrame * parent)
: uiFrame(parent, "Help", Point(50, 95), Size(160, 210), false) {
auto button = new uiButton(this, "OK", Point(57, 180), Size(50, 20));
button->onClick = [&]() { exitModal(0); };
onPaint = [&]() {
int x = 10;
int y = 10;
Canvas.
drawText(x, y += 14,
"Keyboard Shortcuts:");
Canvas.
drawText(x, y += 14,
" F12: Switch Emulator and Menu");
Canvas.
drawText(x, y += 14,
" DEL: Delete File or Folder");
Canvas.
drawText(x, y += 14,
" ALT + A-S-W-Z: Move Screen");
Canvas.
drawText(x, y += 18,
"\"None\" Joystick Mode:");
Canvas.
drawText(x, y += 14,
" ALT + MENU: Joystick Fire");
Canvas.
drawText(x, y += 14,
" ALT + CURSOR: Joystick Move");
Canvas.
drawText(x, y += 18,
"\"Cursor Keys\" Joystick Mode:");
Canvas.
drawText(x, y += 14,
" MENU: Joystick Fire");
Canvas.
drawText(x, y += 14,
" CURSOR: Joystick Move");
};
}
};
class Menu : public uiApp {
uiFileBrowser * fileBrowser;
uiComboBox * RAMExpComboBox;
uiLabel * WiFiStatusLbl;
uiLabel * freeSpaceLbl;
void init() {
rootWindow()->frameStyle().backgroundColor = RGB(3, 3, 3);
rootWindow()->onPaint = [&]() {
Canvas.
drawText(155, 345,
"V I C 2 0 Emulator");
Canvas.
drawText(167, 357,
"www.fabgl.com");
Canvas.
drawText(141, 369,
"2019 by Fabrizio Di Vittorio");
};
fileBrowser = new uiFileBrowser(rootWindow(), Point(5, 10), Size(140, 290));
fileBrowser->listBoxStyle().backgroundColor = RGB(0, 3, 0);
fileBrowser->listBoxStyle().selectedBackgroundColor = RGB(3, 0, 0);
fileBrowser->listBoxStyle().selectedBackgroundColor = RGB(2, 0, 0);
fileBrowser->listBoxStyle().focusedSelectedBackgroundColor = RGB(3, 0, 0);
fileBrowser->windowStyle().focusedBorderColor = RGB(3, 0, 0);
fileBrowser->listBoxStyle().focusedBackgroundColor = RGB(0, 3, 0);
fileBrowser->setDirectory(ROOTDIR);
fileBrowser->onChange = [&]() {
setSelectedProgramConf();
};
fileBrowser->onKeyUp = [&](uiKeyEventInfo key) {
if (!fileBrowser->isDirectory() && loadSelectedProgram())
runVIC20();
if (messageBox("Delete Item", "Are you sure?", "Yes", "Cancel") == uiMessageBoxResult::Button1) {
fileBrowser->content().remove( fileBrowser->filename() );
fileBrowser->update();
updateFreeSpaceLabel();
}
}
};
fileBrowser->onDblClick = [&]() {
if (!fileBrowser->isDirectory() && loadSelectedProgram())
runVIC20();
};
int x = 168;
auto VIC20Button = new uiButton(rootWindow(), "Back to VIC", Point(x, 10), Size(65, 19));
VIC20Button->onClick = [&]() {
runVIC20();
};
auto loadButton = new uiButton(rootWindow(), "Load", Point(x, 35), Size(65, 19));
loadButton->onClick = [&]() {
if (loadSelectedProgram())
runVIC20();
};
auto resetButton = new uiButton(rootWindow(), "Soft Reset", Point(x, 60), Size(65, 19));
resetButton->onClick = [&]() {
machine.reset();
runVIC20();
};
auto hresetButton = new uiButton(rootWindow(), "Hard Reset", Point(x, 85), Size(65, 19));
hresetButton->onClick = [&]() {
machine.removeCRT();
machine.reset();
runVIC20();
};
auto helpButton = new uiButton(rootWindow(), "Help", Point(x, 282), Size(65, 19));
helpButton->onClick = [&]() {
auto hframe = new HelpFame(rootWindow());
showModalWindow(hframe);
destroyWindow(hframe);
};
int y = 120;
auto lbl = new uiLabel(rootWindow(), "RAM Expansion:", Point(150, y));
lbl->labelStyle().textColor = RGB(1, 1, 3);
RAMExpComboBox = new uiComboBox(rootWindow(), Point(158, y + 20), Size(85, 19), 130);
char const * RAMOPTS[] = { "Unexpanded", "3K", "8K", "16K", "24K", "27K (24K+3K)", "32K", "35K (32K+3K)" };
for (int i = 0; i < 8; ++i)
RAMExpComboBox->items().append(RAMOPTS[i]);
RAMExpComboBox->selectItem((int)machine.RAMExpansion());
RAMExpComboBox->onChange = [&]() {
machine.setRAMExpansion((RAMExpansionOption)(RAMExpComboBox->selectedItem()));
};
y += 50;
lbl = new uiLabel(rootWindow(), "Joystick:", Point(150, y));
lbl->labelStyle().textColor = RGB(1, 1, 3);
new uiLabel(rootWindow(), "None", Point(180, y + 20));
auto radioJNone = new uiCheckBox(rootWindow(), Point(160, y + 20), Size(16, 16), uiCheckBoxKind::RadioButton);
new uiLabel(rootWindow(), "Cursor Keys", Point(180, y + 40));
auto radioJCurs = new uiCheckBox(rootWindow(), Point(160, y + 40), Size(16, 16), uiCheckBoxKind::RadioButton);
new uiLabel(rootWindow(), "Mouse", Point(180, y + 60));
auto radioJMous = new uiCheckBox(rootWindow(), Point(160, y + 60), Size(16, 16), uiCheckBoxKind::RadioButton);
radioJNone->setGroupIndex(1);
radioJCurs->setGroupIndex(1);
radioJMous->setGroupIndex(1);
radioJNone->setChecked(machine.joyEmu() == JE_None);
radioJCurs->setChecked(machine.joyEmu() == JE_CursorKeys);
radioJMous->setChecked(machine.joyEmu() == JE_Mouse);
radioJNone->onChange = [&]() { machine.setJoyEmu(JE_None); };
radioJCurs->onChange = [&]() { machine.setJoyEmu(JE_CursorKeys); };
radioJMous->onChange = [&]() { machine.setJoyEmu(JE_Mouse); };
auto setupWifiBtn = new uiButton(rootWindow(), "Setup", Point(28, 330), Size(40, 19));
setupWifiBtn->onClick = [&]() {
char SSID[32] = "";
char psw[32] = "";
if (inputBox("WiFi Connect", "WiFi Name", SSID, sizeof(SSID), "OK", "Cancel") == uiMessageBoxResult::Button1 &&
inputBox("WiFi Connect", "Password", psw, sizeof(psw), "OK", "Cancel") == uiMessageBoxResult::Button1) {
fabgl::suspendInterrupts();
preferences.putString("SSID", SSID);
preferences.putString("WiFiPsw", psw);
fabgl::resumeInterrupts();
}
};
auto onWiFiBtn = new uiButton(rootWindow(), "On", Point(72, 330), Size(40, 19));
onWiFiBtn->onClick = [&]() {
connectWiFi();
};
freeSpaceLbl = new uiLabel(rootWindow(), "", Point(5, 305));
updateFreeSpaceLabel();
WiFiStatusLbl = new uiLabel(rootWindow(), "WiFi", Point(5, 332));
WiFiStatusLbl->labelStyle().textColor = RGB(2, 2, 2);
auto downloadFromLbl = new uiLabel(rootWindow(), "Download From:", Point(5, 354));
downloadFromLbl->labelStyle().textColor = RGB(1, 1, 3);
auto downloadProgsBtn = new uiButton(rootWindow(), "List", Point(75, 352), Size(27, 19));
downloadProgsBtn->onClick = [&]() {
if (!WiFiConnected()) {
messageBox("Network Error", "Please activate WiFi", "OK", nullptr, nullptr, uiMessageBoxIcon::Error);
} else if (messageBox("Download Programs listed in \"LIST_URL\"", "Check your local laws for restrictions", "OK", "Cancel", nullptr, uiMessageBoxIcon::Warning) == uiMessageBoxResult::Button1) {
auto pframe = new DownloadProgressFrame(rootWindow());
auto modalStatus = initModalWindow(pframe);
processModalWindowEvents(modalStatus, 100);
prepareForDownload();
char * list = downloadList();
char * plist = list;
int count = countListItems(list);
int dcount = 0;
for (int i = 0; i < count; ++i) {
char const * filename;
char const * URL;
plist = getNextListItem(plist, &filename, &URL);
pframe->label1->setText(filename);
if (!processModalWindowEvents(modalStatus, 100))
break;
if (downloadURL(URL, filename))
++dcount;
pframe->label2->setTextFmt("Downloaded %d/%d", dcount, count);
}
free(list);
pframe->button->setText("OK");
pframe->button->repaint();
processModalWindowEvents(modalStatus, -1);
endModalWindow(modalStatus);
destroyWindow(pframe);
fileBrowser->update();
updateFreeSpaceLabel();
}
};
auto downloadURLBtn = new uiButton(rootWindow(), "URL", Point(107, 352), Size(27, 19));
downloadURLBtn->onClick = [&]() {
char * URL = new char[128];
char * filename = new char[25];
strcpy(URL, "http://");
if (inputBox("Download From URL", "URL", URL, 127, "OK", "Cancel") == uiMessageBoxResult::Button1) {
char * lastslash = strrchr(URL, '/');
if (lastslash) {
strcpy(filename, lastslash + 1);
if (inputBox("Download From URL", "Filename", filename, 24, "OK", "Cancel") == uiMessageBoxResult::Button1) {
if (downloadURL(URL, filename)) {
messageBox("Success", "Download OK!", "OK", nullptr, nullptr);
fileBrowser->update();
updateFreeSpaceLabel();
} else
messageBox("Error", "Download Failed!", "OK", nullptr, nullptr, uiMessageBoxIcon::Error);
}
}
}
delete [] filename;
delete [] URL;
};
rootWindow()->onKeyUp = [&](uiKeyEventInfo key) {
runVIC20();
};
setFocusedWindow(fileBrowser);
}
void runVIC20()
{
enableKeyboardAndMouseEvents(false);
machine.VIC().enableAudio(true);
bool run = true;
while (run) {
#if DEBUG
int cycles = machine.run();
if (restart) {
scycles = cycles;
restart = false;
} else
scycles += cycles;
#else
machine.run();
#endif
bool keyDown;
switch (vk) {
if (!keyDown)
run = false;
break;
default:
machine.setKeyboard(vk, keyDown);
break;
}
}
}
machine.VIC().enableAudio(false);
enableKeyboardAndMouseEvents(true);
rootWindow()->repaint();
}
void setSelectedProgramConf()
{
char const * fname = fileBrowser->filename();
if (fname) {
static char const * EXP[8] = { "", " 3K", " 8K", " 16K", " 24K", " 27K", " 32K", " 35K" };
int r = 0;
for (int i = 1; i < 8; ++i)
if (strstr(fname, EXP[i])) {
r = i;
break;
}
RAMExpComboBox->selectItem(r);
}
}
int getAddress(char * filename, char * * addrPos = nullptr)
{
static char const * ASTR[] = { " 20", " 40", " 60", " a0", " A0" };
static const int ADDR[] = { 0x2000, 0x4000, 0x6000, 0xa000, 0xa000 };
for (int i = 0; i < sizeof(ADDR) / sizeof(int); ++i) {
auto p = strstr(filename, ASTR[i]);
if (p) {
if (addrPos)
*addrPos = p + 1;
return ADDR[i];
}
}
return -1;
}
bool loadSelectedProgram()
{
bool backToVIC = false;
char const * fname = fileBrowser->filename();
if (fname) {
FileBrowser & dir = fileBrowser->content();
bool isPRG = strstr(fname, ".PRG") || strstr(fname, ".prg");
bool isCRT = strstr(fname, ".CRT") || strstr(fname, ".crt");
if (isPRG || isCRT) {
machine.removeCRT();
int fullpathlen = dir.getFullPath(fname);
char fullpath[fullpathlen];
dir.getFullPath(fname, fullpath, fullpathlen);
if (isPRG) {
machine.loadPRG(fullpath, true, true);
} else if (isCRT) {
char * addrPos = nullptr;
int addr = getAddress(fullpath, &addrPos);
if (addrPos) {
static const char * POS = { "246Aa" };
for (int i = 0; i < sizeof(POS); ++i) {
*addrPos = POS[i];
machine.loadCRT(fullpath, true, getAddress(fullpath));
}
} else {
machine.loadCRT(fullpath, true, addr);
}
}
backToVIC = true;
}
}
return backToVIC;
}
bool WiFiConnected() {
fabgl::suspendInterrupts();
bool r = WiFi.status() == WL_CONNECTED;
fabgl::resumeInterrupts();
return r;
}
void connectWiFi()
{
fabgl::suspendInterrupts();
char SSID[32], psw[32];
if (preferences.getString("SSID", SSID, sizeof(SSID)) && preferences.getString("WiFiPsw", psw, sizeof(psw))) {
WiFi.begin(SSID, psw);
for (int i = 0; i < 6 && WiFi.status() != WL_CONNECTED; ++i) {
WiFi.reconnect();
delay(1000);
}
}
WiFiStatusLbl->labelStyle().textColor = (WiFi.status() == WL_CONNECTED ? RGB(0, 2, 0) : RGB(2, 2, 2));
WiFiStatusLbl->update();
fabgl::resumeInterrupts();
if (WiFi.status() != WL_CONNECTED)
messageBox("Network Error", "Failed to connect WiFi. Try again!", "OK", nullptr, nullptr, uiMessageBoxIcon::Error);
}
void prepareForDownload()
{
static char const * DOWNDIR = "List";
FileBrowser & dir = fileBrowser->content();
fabgl::suspendInterrupts();
dir.setDirectory(ROOTDIR);
dir.makeDirectory(DOWNDIR);
dir.changeDirectory(DOWNDIR);
fabgl::resumeInterrupts();
}
char * downloadList()
{
fabgl::suspendInterrupts();
auto list = (char*) malloc(MAXLISTSIZE);
auto dest = list;
HTTPClient http;
http.begin(LIST_URL);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
WiFiClient * stream = http.getStreamPtr();
int bufspace = MAXLISTSIZE;
while (http.connected() && bufspace > 1) {
auto size = stream->available();
if (size) {
int c = stream->readBytes(dest, fabgl::imin(bufspace, size));
dest += c;
bufspace -= c;
}
}
}
*dest = 0;
fabgl::resumeInterrupts();
return list;
}
int countListItems(char const * list)
{
int count = 0;
auto p = list;
while (*p++)
if (*p == 0x0a)
++count;
return (count + 1) / 3;
}
char * getNextListItem(char * list, char const * * filename, char const * * URL)
{
while (*list == 0x0a || *list == 0x0d || *list == 0x20)
++list;
*filename = list;
while (*list && *list != 0x0a && *list != 0x0d)
++list;
*list++ = 0;
while (*list && (*list == 0x0a || *list == 0x0d || *list == 0x20))
++list;
*URL = list;
while (*list && *list != 0x0a && *list != 0x0d)
++list;
*list++ = 0;
return list;
}
bool downloadURL(char const * URL, char const * filename)
{
fabgl::suspendInterrupts();
FileBrowser & dir = fileBrowser->content();
if (dir.exists(filename)) {
fabgl::resumeInterrupts();
return true;
}
bool success = false;
HTTPClient http;
http.begin(URL);
int httpCode = http.GET();
if (httpCode == HTTP_CODE_OK) {
int fullpathLen = dir.getFullPath(filename);
char fullpath[fullpathLen];
dir.getFullPath(filename, fullpath, fullpathLen);
FILE * f = fopen(fullpath, "wb");
if (f) {
int len = http.getSize();
uint8_t * buf = (uint8_t*) malloc(128);
WiFiClient * stream = http.getStreamPtr();
int dsize = 0;
while (http.connected() && (len > 0 || len == -1)) {
size_t size = stream->available();
if (size) {
int c = stream->readBytes(buf, fabgl::imin(sizeof(buf), size));
fwrite(buf, c, 1, f);
dsize += c;
if (len > 0)
len -= c;
}
}
free(buf);
fclose(f);
success = (len == 0 || (len == -1 && dsize > 0));
}
}
fabgl::resumeInterrupts();
return success;
}
void updateFreeSpaceLabel() {
size_t total = 0, used = 0;
esp_spiffs_info(NULL, &total, &used);
freeSpaceLbl->setTextFmt("%d KB Free", (total - used) / 1024);
freeSpaceLbl->update();
}
};
void setup()
{
Serial.begin(115200);
preferences.begin("VIC20", false);
PS2Controller.
begin(PS2Preset::KeyboardPort0_MousePort1, KbdMode::CreateVirtualKeysQueue);
Canvas.
drawText(25, 10,
"Initializing SPIFFS...");
initSPIFFS();
Canvas.
drawText(25, 30,
"Copying embedded programs...");
copyEmbeddedPrograms();
}
void loop()
{
#if DEBUG
TimerHandle_t xtimer = xTimerCreate("", pdMS_TO_TICKS(5000), pdTRUE, (void*)0, vTimerCallback1SecExpired);
xTimerStart(xtimer, 0);
#endif
auto menu = new Menu;
menu->run();
}