dbf850c434
- M5 Unit Modules library + M5 Unit Test app - Minor fixes for TodoList, TwoEleven, Snake, Brainfuck and Breakout - Fixed SerialConsole to use the uart controller as it was broken in one of the many updates - Fixed keyboard input in TwoEleven, Breakout, Magic8Ball and Snake - Bluetooth Media Keys app, supports pressing physical keys to trigger the corresponding buttonmatrix button - Epub Reader app
465 lines
16 KiB
C++
465 lines
16 KiB
C++
#include "Brainfuck.h"
|
|
#include <tt_app.h>
|
|
#include <tt_lvgl_toolbar.h>
|
|
#include <dirent.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
/* ── Built-in examples ────────────────────────────────────────────── */
|
|
|
|
struct BfExample {
|
|
const char* name;
|
|
const char* code;
|
|
};
|
|
|
|
static const BfExample examples[] = {
|
|
{
|
|
"Hello World (built-in)",
|
|
"++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]"
|
|
">>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++."
|
|
},
|
|
{
|
|
"Fibonacci (built-in)",
|
|
">++++++++++>+>+[[+++++[>++++++++<-]>.<++++++[>--------<-]+<<<"
|
|
"]>.>>[[-]<[>+<-]>>[<<+>+>-]<[>+<-[>+<-[>+<-[>+<-[>+<-[>+<-"
|
|
"[>+<-[>+<-[>+<-[>[-]>+>+<<<-[>+<-]]]]]]]]]]]+>>>]<<<]"
|
|
},
|
|
{
|
|
"Alphabet (built-in)",
|
|
"+++++[>+++++++++++++<-]++++++++++++++++++++++++++[>.+<-]"
|
|
},
|
|
{
|
|
"Beer (built-in)",
|
|
">++++++++++[<++++++++++>-]<->>>>>+++[>+++>+++<<-]<<<<+<[>[>+"
|
|
">+<<-]>>[-<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]>>"
|
|
">>>>[[-]<++++++++++<->>]<-[>+>+<<-]>[<+>-]+>[[-]<->]<<<<<<<<"
|
|
"<->>]<[>+>+<<-]>>[-<<+>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<"
|
|
"<[>>+>+<<<-]>>>[-<<<+>>>]++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<"
|
|
"<[>+<[-]]<[>>+<<[-]]>>[<<+>>[-]]<<<[>>+>+<<<-]>>>[-<<<+>>>]+"
|
|
"+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<"
|
|
"<+>>[-]]<<[[-]>>>++++++++[>>++++++<<-]>[<++++++++[>++++++<-]"
|
|
">.<++++++++[>------<-]>[<<+>>-]]>.<<++++++++[>>------<<-]<[-"
|
|
">>+<<]<++++++++[<++++>-]<.>+++++++[>+++++++++<-]>+++.<+++++["
|
|
">+++++++++<-]>.+++++..--------.-------.++++++++++++++>>[>>>+"
|
|
">+<<<<-]>>>>[-<<<<+>>>>]>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<<"
|
|
"[>>>+>+<<<<-]>>>>[-<<<<+>>>>]+>+<[-<->]<[[-]>>-<<]>>[[-]<<+>"
|
|
">]<<<[>>+<<[-]]>[>+<[-]]++>>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<"
|
|
"+<[[-]>-<]>[<<<<<<<.>>>>>>>[-]]<<<<<<<<<.>>----.---------.<<"
|
|
".>>----.+++..+++++++++++++.[-]<<[-]]<[>+>+<<-]>>[-<<+>>]+>+<"
|
|
"[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<<[>>+>+<<<-]>>>[-<<<+>>>]++++"
|
|
">+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[>+<[-]]<[>>+<<[-]]>>[<<+>"
|
|
">[-]]<<[[-]>++++++++[<++++>-]<.>++++++++++[>+++++++++++<-]>+"
|
|
".-.<<.>>++++++.------------.---.<<.>++++++[>+++<-]>.<++++++["
|
|
">----<-]>++.+++++++++++..[-]<<[-]++++++++++.[-]]<[>+>+<<-]>>"
|
|
"[-<<+>>]+++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++."
|
|
">+++++++++[>+++++++++<-]>+++.+++++++++++++.++++++++++.------"
|
|
".<++++++++[>>++++<<-]>>.<++++++++++.-.---------.>.<-.+++++++"
|
|
"++++.++++++++.---------.>.<-------------.+++++++++++++.-----"
|
|
"-----.>.<++++++++++++.---------------.<+++[>++++++<-]>..>.<-"
|
|
"---------.+++++++++++.>.<<+++[>------<-]>-.+++++++++++++++++"
|
|
".---.++++++.-------.----------.[-]>[-]<<<.[-]]<[>+>+<<-]>>[-"
|
|
"<<+>>]++++>+<[-<->]<[[-]>>-<<]>>[[-]<<+>>]<<[[-]++++++++++.["
|
|
"-]<[-]>]<+<]"
|
|
},
|
|
};
|
|
|
|
static constexpr int NUM_EXAMPLES = sizeof(examples) / sizeof(examples[0]);
|
|
|
|
/* ── App handle for user data path ────────────────────────────────── */
|
|
|
|
static AppHandle s_appHandle = nullptr;
|
|
|
|
static bool getScriptDir(char* buf, size_t bufSize) {
|
|
if (!s_appHandle) return false;
|
|
size_t size = bufSize;
|
|
tt_app_get_user_data_path(s_appHandle, buf, &size);
|
|
if (size == 0) return false;
|
|
for (char* p = buf + 1; *p; ++p) {
|
|
if (*p == '/') { *p = '\0'; mkdir(buf, 0755); *p = '/'; }
|
|
}
|
|
mkdir(buf, 0755);
|
|
return true;
|
|
}
|
|
|
|
static char** scriptPaths = nullptr;
|
|
static int scriptCount = 0;
|
|
|
|
static int ciStrcmp(const char* a, const char* b) {
|
|
while (*a && *b) {
|
|
int ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a;
|
|
int cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b;
|
|
if (ca != cb) return ca - cb;
|
|
a++; b++;
|
|
}
|
|
return (unsigned char)*a - (unsigned char)*b;
|
|
}
|
|
|
|
static void freeScriptPaths() {
|
|
for (int i = 0; i < scriptCount; i++) {
|
|
free(scriptPaths[i]);
|
|
}
|
|
free(scriptPaths);
|
|
scriptPaths = nullptr;
|
|
scriptCount = 0;
|
|
}
|
|
|
|
/* File-scope instance pointer for index-based and path-based callbacks */
|
|
static Brainfuck* g_instance = nullptr;
|
|
|
|
/* ── VM Logic ─────────────────────────────────────────────────────── */
|
|
|
|
void Brainfuck::bfInit() {
|
|
memset(&vm, 0, sizeof(BfVM));
|
|
}
|
|
|
|
static int bfFindBracket(const char* code, int pos, int dir) {
|
|
int depth = 1;
|
|
while (depth > 0) {
|
|
pos += dir;
|
|
if (pos < 0 || code[pos] == '\0') return -1;
|
|
if (code[pos] == '[') depth += dir;
|
|
if (code[pos] == ']') depth -= dir;
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
void Brainfuck::bfRun(const char* code) {
|
|
int len = strlen(code);
|
|
|
|
while (vm.pc < len && !vm.error) {
|
|
vm.cycles++;
|
|
if (vm.cycles > MAX_CYCLES) {
|
|
vm.error = true;
|
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Halted after %d cycles (infinite loop?)", MAX_CYCLES);
|
|
return;
|
|
}
|
|
|
|
char c = code[vm.pc];
|
|
switch (c) {
|
|
case '>':
|
|
vm.ptr++;
|
|
if (vm.ptr >= TAPE_SIZE) {
|
|
vm.error = true;
|
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer overflow (>%d)", TAPE_SIZE);
|
|
return;
|
|
}
|
|
break;
|
|
case '<':
|
|
vm.ptr--;
|
|
if (vm.ptr < 0) {
|
|
vm.error = true;
|
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Pointer underflow (<0)");
|
|
return;
|
|
}
|
|
break;
|
|
case '+': vm.tape[vm.ptr]++; break;
|
|
case '-': vm.tape[vm.ptr]--; break;
|
|
case '.':
|
|
if (vm.outLen < MAX_OUTPUT - 1) {
|
|
vm.output[vm.outLen++] = (char)vm.tape[vm.ptr];
|
|
vm.output[vm.outLen] = '\0';
|
|
}
|
|
break;
|
|
case ',': vm.tape[vm.ptr] = 0; break;
|
|
case '[':
|
|
if (vm.tape[vm.ptr] == 0) {
|
|
int target = bfFindBracket(code, vm.pc, 1);
|
|
if (target < 0) {
|
|
vm.error = true;
|
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched '[' at pos %d", vm.pc);
|
|
return;
|
|
}
|
|
vm.pc = target;
|
|
}
|
|
break;
|
|
case ']':
|
|
if (vm.tape[vm.ptr] != 0) {
|
|
int target = bfFindBracket(code, vm.pc, -1);
|
|
if (target < 0) {
|
|
vm.error = true;
|
|
snprintf(vm.errorMsg, sizeof(vm.errorMsg), "Unmatched ']' at pos %d", vm.pc);
|
|
return;
|
|
}
|
|
vm.pc = target;
|
|
}
|
|
break;
|
|
}
|
|
vm.pc++;
|
|
}
|
|
}
|
|
|
|
void Brainfuck::runCode(const char* code) {
|
|
if (!outputTa) return;
|
|
bfInit();
|
|
bfRun(code);
|
|
|
|
constexpr int resultSize = MAX_OUTPUT + 128;
|
|
char* result = (char*)malloc(resultSize);
|
|
if (!result) {
|
|
lv_textarea_set_text(outputTa, "Out of memory");
|
|
return;
|
|
}
|
|
int pos = 0;
|
|
int remaining;
|
|
|
|
remaining = resultSize - pos;
|
|
if (remaining > 0) {
|
|
int n = snprintf(result + pos, remaining, "> RUN (%d chars)\n", (int)strlen(code));
|
|
pos += (n < remaining) ? n : (remaining - 1);
|
|
}
|
|
|
|
if (vm.outLen > 0) {
|
|
remaining = resultSize - pos;
|
|
if (remaining > 0) {
|
|
int n = snprintf(result + pos, remaining, "%s\n", vm.output);
|
|
pos += (n < remaining) ? n : (remaining - 1);
|
|
}
|
|
}
|
|
|
|
if (vm.error) {
|
|
remaining = resultSize - pos;
|
|
if (remaining > 0) {
|
|
int n = snprintf(result + pos, remaining, "ERROR: %s\n", vm.errorMsg);
|
|
pos += (n < remaining) ? n : (remaining - 1);
|
|
}
|
|
} else {
|
|
remaining = resultSize - pos;
|
|
if (remaining > 0) {
|
|
int n = snprintf(result + pos, remaining, "OK (%d cycles)\n", vm.cycles);
|
|
pos += (n < remaining) ? n : (remaining - 1);
|
|
}
|
|
}
|
|
|
|
lv_textarea_set_text(outputTa, result);
|
|
lv_obj_scroll_to_y(outputTa, LV_COORD_MAX, LV_ANIM_ON);
|
|
free(result);
|
|
}
|
|
|
|
/* ── View management ──────────────────────────────────────────────── */
|
|
|
|
void Brainfuck::showMainView() {
|
|
state = BfState::Main;
|
|
if (examplesList) lv_obj_add_flag(examplesList, LV_OBJ_FLAG_HIDDEN);
|
|
if (outputTa) lv_obj_remove_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
|
|
if (inputRow) lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
|
|
if (clrBtn) lv_obj_remove_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
void Brainfuck::showExamplesView() {
|
|
state = BfState::Examples;
|
|
if (outputTa) lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
|
|
if (inputRow) lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
|
|
if (examplesList) lv_obj_remove_flag(examplesList, LV_OBJ_FLAG_HIDDEN);
|
|
if (clrBtn) lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
/* ── Callbacks ────────────────────────────────────────────────────── */
|
|
|
|
void Brainfuck::onRunClicked(lv_event_t* e) {
|
|
if (!g_instance || !g_instance->inputTa) return;
|
|
const char* code = lv_textarea_get_text(g_instance->inputTa);
|
|
if (code && code[0]) {
|
|
g_instance->runCode(code);
|
|
}
|
|
}
|
|
|
|
void Brainfuck::onClearClicked(lv_event_t* e) {
|
|
if (!g_instance) return;
|
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "");
|
|
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, "");
|
|
}
|
|
|
|
void Brainfuck::onExamplesClicked(lv_event_t* e) {
|
|
if (!g_instance) return;
|
|
if (g_instance->state == BfState::Examples) {
|
|
g_instance->showMainView();
|
|
} else {
|
|
g_instance->showExamplesView();
|
|
}
|
|
}
|
|
|
|
void Brainfuck::onExampleSelected(lv_event_t* e) {
|
|
if (!g_instance) return;
|
|
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
|
if (idx >= 0 && idx < NUM_EXAMPLES) {
|
|
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, examples[idx].code);
|
|
g_instance->showMainView();
|
|
g_instance->runCode(examples[idx].code);
|
|
}
|
|
}
|
|
|
|
void Brainfuck::onFileSelected(lv_event_t* e) {
|
|
if (!g_instance) return;
|
|
const char* path = (const char*)lv_event_get_user_data(e);
|
|
FILE* f = fopen(path, "rb");
|
|
if (!f) {
|
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Cannot open file");
|
|
g_instance->showMainView();
|
|
return;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
long fsize = ftell(f);
|
|
if (fsize <= 0 || fsize > 32768) {
|
|
fclose(f);
|
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "File too large or empty");
|
|
g_instance->showMainView();
|
|
return;
|
|
}
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
char* buf = (char*)malloc(fsize + 1);
|
|
if (!buf) {
|
|
fclose(f);
|
|
if (g_instance->outputTa) lv_textarea_set_text(g_instance->outputTa, "Out of memory");
|
|
g_instance->showMainView();
|
|
return;
|
|
}
|
|
|
|
size_t bytesRead = fread(buf, 1, fsize, f);
|
|
fclose(f);
|
|
buf[bytesRead] = '\0';
|
|
|
|
if (g_instance->inputTa) lv_textarea_set_text(g_instance->inputTa, buf);
|
|
g_instance->showMainView();
|
|
g_instance->runCode(buf);
|
|
free(buf);
|
|
}
|
|
|
|
void Brainfuck::onInputReady(lv_event_t* e) {
|
|
onRunClicked(e);
|
|
}
|
|
|
|
/* ── Script list building ─────────────────────────────────────────── */
|
|
|
|
void Brainfuck::buildScriptList(lv_obj_t* list) {
|
|
freeScriptPaths();
|
|
|
|
for (int i = 0; i < NUM_EXAMPLES; i++) {
|
|
lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_PLAY, examples[i].name);
|
|
lv_obj_add_event_cb(btn, onExampleSelected, LV_EVENT_CLICKED, (void*)(intptr_t)i);
|
|
}
|
|
|
|
char scriptDir[256];
|
|
if (!getScriptDir(scriptDir, sizeof(scriptDir))) {
|
|
lv_list_add_text(list, "No storage available for .bf files");
|
|
return;
|
|
}
|
|
|
|
DIR* dir = opendir(scriptDir);
|
|
if (!dir) {
|
|
char hint[300];
|
|
snprintf(hint, sizeof(hint), "Put .bf files in %s", scriptDir);
|
|
lv_list_add_text(list, hint);
|
|
return;
|
|
}
|
|
|
|
struct dirent* entry;
|
|
while ((entry = readdir(dir)) != nullptr) {
|
|
const char* name = entry->d_name;
|
|
size_t len = strlen(name);
|
|
if (len < 3) continue;
|
|
|
|
const char* ext3 = &name[len - 3];
|
|
const char* ext2 = &name[len - 2];
|
|
if (ciStrcmp(ext3, ".bf") != 0 && ciStrcmp(ext2, ".b") != 0) {
|
|
continue;
|
|
}
|
|
|
|
size_t pathLen = strlen(scriptDir) + 1 + len + 1;
|
|
char* path = (char*)malloc(pathLen);
|
|
if (!path) break;
|
|
snprintf(path, pathLen, "%s/%s", scriptDir, name);
|
|
|
|
char** tmp = (char**)realloc(scriptPaths, sizeof(char*) * (scriptCount + 1));
|
|
if (!tmp) { free(path); break; }
|
|
scriptPaths = tmp;
|
|
scriptPaths[scriptCount] = path;
|
|
|
|
lv_obj_t* btn = lv_list_add_button(list, LV_SYMBOL_FILE, name);
|
|
lv_obj_add_event_cb(btn, onFileSelected, LV_EVENT_CLICKED, path);
|
|
scriptCount++;
|
|
}
|
|
closedir(dir);
|
|
|
|
if (scriptCount == 0) {
|
|
lv_list_add_text(list, "No custom scripts found on storage");
|
|
}
|
|
}
|
|
|
|
/* ── Lifecycle ────────────────────────────────────────────────────── */
|
|
|
|
void Brainfuck::onShow(AppHandle app, lv_obj_t* parent) {
|
|
g_instance = this;
|
|
s_appHandle = app;
|
|
state = BfState::Examples;
|
|
|
|
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
|
|
|
|
lv_obj_t* toolbar = tt_lvgl_toolbar_create_for_app(parent, app);
|
|
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
|
|
clrBtn = tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_TRASH, onClearClicked, nullptr);
|
|
lv_obj_add_flag(clrBtn, LV_OBJ_FLAG_HIDDEN);
|
|
tt_lvgl_toolbar_add_text_button_action(toolbar, LV_SYMBOL_LIST, onExamplesClicked, nullptr);
|
|
|
|
lv_obj_t* cont = lv_obj_create(parent);
|
|
lv_obj_set_width(cont, LV_PCT(100));
|
|
lv_obj_set_flex_grow(cont, 1);
|
|
lv_obj_set_flex_flow(cont, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_all(cont, 4, 0);
|
|
lv_obj_set_style_pad_gap(cont, 4, 0);
|
|
lv_obj_remove_flag(cont, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_border_width(cont, 0, 0);
|
|
|
|
outputTa = lv_textarea_create(cont);
|
|
lv_textarea_set_text(outputTa, "");
|
|
lv_textarea_set_cursor_click_pos(outputTa, false);
|
|
lv_obj_add_state(outputTa, LV_STATE_DISABLED);
|
|
lv_obj_set_width(outputTa, LV_PCT(100));
|
|
lv_obj_set_flex_grow(outputTa, 1);
|
|
lv_obj_set_style_text_font(outputTa, lv_font_get_default(), 0);
|
|
lv_obj_add_flag(outputTa, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
inputRow = lv_obj_create(cont);
|
|
lv_obj_set_size(inputRow, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(inputRow, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(inputRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_all(inputRow, 0, 0);
|
|
lv_obj_set_style_pad_gap(inputRow, 4, 0);
|
|
lv_obj_remove_flag(inputRow, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_style_border_width(inputRow, 0, 0);
|
|
lv_obj_add_flag(inputRow, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
inputTa = lv_textarea_create(inputRow);
|
|
lv_textarea_set_placeholder_text(inputTa, "++++++[>++++++++++<-]>+++++.");
|
|
lv_textarea_set_one_line(inputTa, false);
|
|
lv_obj_set_flex_grow(inputTa, 1);
|
|
lv_obj_set_height(inputTa, 50);
|
|
lv_obj_set_style_text_font(inputTa, lv_font_get_default(), 0);
|
|
lv_obj_add_event_cb(inputTa, onInputReady, LV_EVENT_READY, nullptr);
|
|
|
|
lv_obj_t* runBtn = lv_button_create(inputRow);
|
|
lv_obj_t* runLbl = lv_label_create(runBtn);
|
|
lv_label_set_text(runLbl, LV_SYMBOL_PLAY);
|
|
lv_obj_add_event_cb(runBtn, onRunClicked, LV_EVENT_CLICKED, nullptr);
|
|
|
|
examplesList = lv_list_create(cont);
|
|
lv_obj_set_width(examplesList, LV_PCT(100));
|
|
lv_obj_set_flex_grow(examplesList, 1);
|
|
buildScriptList(examplesList);
|
|
}
|
|
|
|
void Brainfuck::onHide(AppHandle app) {
|
|
freeScriptPaths();
|
|
outputTa = nullptr;
|
|
inputTa = nullptr;
|
|
inputRow = nullptr;
|
|
examplesList = nullptr;
|
|
clrBtn = nullptr;
|
|
g_instance = nullptr;
|
|
s_appHandle = nullptr;
|
|
}
|