Files
wadamesh/web/platform/host_main.cpp
T

263 lines
11 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// wadamesh-host: the real touch UI in an SDL2 window (phase 1 of WEB_BUILD.md).
//
// The native front end of the web board: board.cpp owns the board globals and the
// boot/loop order (shared with web_main.cpp, the Emscripten front end); this file is
// the SDL window, the event pump and the scripted tour.
//
// wadamesh-host [--headless] [--script FILE] [--shots DIR] [--scale N] [--size WxH]
// [--serial PATH | --tcp HOST:PORT] [--read-only]
// --headless no window (CI / release gate); the framebuffer is still rendered
// --script scripted input: wait/tap/drag/key/type/shot/quit, one per line
// --shots DIR where `shot` and F12 write PNGs (default: current directory)
// --scale N initial window scale (default 1; the window is resizable)
// --serial PATH drive a real MeshCore companion node over USB instead of the demo
// --tcp HOST[:PORT] the same over the node's TCP companion server (default port 5000)
// --read-only never change the node's settings, contacts, channels or clock
// $WADA_DATA_DIR the virtual flash (SPIFFS, LittleFS, NVS), default ./wadamesh-data
#include <Arduino.h>
#include "WebDisplay.h"
#include "WebInput.h"
#include "WebPlatform.h"
#include "BackendSelect.h"
#include "wada_host.h"
#include "miniz.h"
#include <SDL.h>
#include <string>
#include <vector>
#include <fstream>
#include <unistd.h>
#include <sstream>
extern WebDisplay display; // board.cpp
// ---- presentation ------------------------------------------------------------------
namespace {
SDL_Window* s_win = nullptr;
SDL_Renderer* s_ren = nullptr;
SDL_Texture* s_tex = nullptr;
bool s_quit = false;
std::string s_shot_dir = ".";
bool savePng(const std::string& path) {
const uint16_t* fb = display.framebuffer();
std::vector<uint8_t> rgb((size_t)WADA_WEB_W * WADA_WEB_H * 3);
for (size_t i = 0; i < (size_t)WADA_WEB_W * WADA_WEB_H; ++i) {
const uint16_t p = fb[i];
const uint8_t r = (p >> 11) & 0x1F, g = (p >> 5) & 0x3F, b = p & 0x1F;
rgb[i * 3 + 0] = (uint8_t)((r << 3) | (r >> 2));
rgb[i * 3 + 1] = (uint8_t)((g << 2) | (g >> 4));
rgb[i * 3 + 2] = (uint8_t)((b << 3) | (b >> 2));
}
size_t len = 0;
void* png = tdefl_write_image_to_png_file_in_memory(rgb.data(), WADA_WEB_W, WADA_WEB_H, 3, &len);
if (!png) return false;
FILE* f = fopen(path.c_str(), "wb");
bool ok = f && fwrite(png, 1, len, f) == len;
if (f) fclose(f);
mz_free(png);
fprintf(stderr, "[web] screenshot %s %s\n", path.c_str(), ok ? "written" : "FAILED");
return ok;
}
void present() {
if (!s_ren || !display.takeDirty()) return;
SDL_UpdateTexture(s_tex, nullptr, display.framebuffer(), WADA_WEB_W * 2);
SDL_RenderClear(s_ren);
SDL_RenderCopy(s_ren, s_tex, nullptr, nullptr);
SDL_RenderPresent(s_ren);
}
// SDL key -> CardKB code (the external-keyboard path the UI already has).
void keyDown(const SDL_Keysym& k) {
switch (k.sym) {
case SDLK_RETURN: case SDLK_KP_ENTER: webKeyPush(0x0D); break;
case SDLK_BACKSPACE: webKeyPush(0x08); break;
case SDLK_ESCAPE: webKeyPush(0x1B); break;
case SDLK_TAB: webKeyPush(0x09); break;
case SDLK_LEFT: webKeyPush(0xB4); break;
case SDLK_UP: webKeyPush(0xB5); break;
case SDLK_DOWN: webKeyPush(0xB6); break;
case SDLK_RIGHT: webKeyPush(0xB7); break;
case SDLK_F12: {
char name[64]; snprintf(name, sizeof name, "/wadamesh-%lu.png", (unsigned long)millis());
savePng(s_shot_dir + name);
break;
}
default: break;
}
}
int s_mouse_x = 0, s_mouse_y = 0;
void pumpEvents() {
if (!s_win) return;
SDL_Event e;
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_QUIT: s_quit = true; break;
case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP:
if (e.button.button == SDL_BUTTON_LEFT) webPointer(e.type == SDL_MOUSEBUTTONDOWN, e.button.x, e.button.y);
break;
case SDL_MOUSEMOTION:
s_mouse_x = e.motion.x; s_mouse_y = e.motion.y; // where a wheel turn applies
if (e.motion.state & SDL_BUTTON_LMASK) webPointer(true, e.motion.x, e.motion.y);
break;
case SDL_MOUSEWHEEL: {
// precise (trackpad) deltas when SDL has them; a wheel notch = 3 lines of 16 px
const float dy = e.wheel.preciseY != 0 ? e.wheel.preciseY : (float)e.wheel.y;
const int sign = e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1;
webWheel(s_mouse_x, s_mouse_y, (int)(-dy * sign * 48.0f));
break;
}
case SDL_FINGERDOWN: case SDL_FINGERMOTION: case SDL_FINGERUP:
webPointer(e.type != SDL_FINGERUP, (int)(e.tfinger.x * WADA_WEB_W), (int)(e.tfinger.y * WADA_WEB_H));
break;
case SDL_KEYDOWN: keyDown(e.key.keysym); break;
case SDL_TEXTINPUT:
webTextPush(e.text.text); // any script: ASCII as CardKB codes, the rest as text
break;
case SDL_WINDOWEVENT:
if (e.window.event == SDL_WINDOWEVENT_EXPOSED || e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
SDL_UpdateTexture(s_tex, nullptr, display.framebuffer(), WADA_WEB_W * 2);
SDL_RenderClear(s_ren); SDL_RenderCopy(s_ren, s_tex, nullptr, nullptr); SDL_RenderPresent(s_ren);
}
break;
default: break;
}
}
}
// ---- scripted input (screenshots, smoke tests, the phase-4 release gate) ------------
struct Script {
std::vector<std::string> lines;
size_t pc = 0;
uint32_t wait_until = 0;
// an in-flight press: release at release_at
bool pressing = false; uint32_t release_at = 0;
int drag_x0 = 0, drag_y0 = 0, drag_x1 = 0, drag_y1 = 0; uint32_t drag_t0 = 0, drag_ms = 0; bool dragging = false;
} s_script;
void scriptStep() {
const uint32_t now = millis();
if (s_script.dragging) {
const uint32_t t = now - s_script.drag_t0;
if (t >= s_script.drag_ms) { webPointer(true, s_script.drag_x1, s_script.drag_y1); s_script.dragging = false; s_script.pressing = true; s_script.release_at = now + 40; }
else {
const float f = (float)t / (float)s_script.drag_ms;
webPointer(true, s_script.drag_x0 + (int)((s_script.drag_x1 - s_script.drag_x0) * f),
s_script.drag_y0 + (int)((s_script.drag_y1 - s_script.drag_y0) * f));
}
return;
}
if (s_script.pressing) {
if ((int32_t)(now - s_script.release_at) < 0) return;
webPointer(false, 0, 0); // (coordinates of a release are not used)
s_script.pressing = false;
s_script.wait_until = now + 120; // let the UI see the release before the next step
return;
}
if ((int32_t)(now - s_script.wait_until) < 0) return;
while (s_script.pc < s_script.lines.size()) {
std::istringstream in(s_script.lines[s_script.pc++]);
std::string cmd; in >> cmd;
if (cmd.empty() || cmd[0] == '#') continue;
fprintf(stderr, "[script] %s\n", s_script.lines[s_script.pc - 1].c_str());
if (cmd == "wait") { uint32_t ms = 0; in >> ms; s_script.wait_until = now + ms; return; }
if (cmd == "tap" || cmd == "hold") {
int x = 0, y = 0; uint32_t ms = (cmd == "tap") ? 60 : 1300; in >> x >> y; if (cmd == "hold") in >> ms;
webPointer(true, x, y); s_script.pressing = true; s_script.release_at = now + ms; return;
}
if (cmd == "drag") {
in >> s_script.drag_x0 >> s_script.drag_y0 >> s_script.drag_x1 >> s_script.drag_y1 >> s_script.drag_ms;
if (!s_script.drag_ms) s_script.drag_ms = 300;
webPointer(true, s_script.drag_x0, s_script.drag_y0);
s_script.drag_t0 = now; s_script.dragging = true; return;
}
if (cmd == "type") {
std::string rest; std::getline(in, rest);
if (!rest.empty() && rest[0] == ' ') rest.erase(0, 1);
for (char c : rest) webKeyPush((uint8_t)c);
s_script.wait_until = now + 200; return;
}
if (cmd == "key") {
std::string k; in >> k;
const uint8_t code = k == "enter" ? 0x0D : k == "esc" ? 0x1B : k == "bs" ? 0x08 : k == "tab" ? 0x09
: k == "left" ? 0xB4 : k == "up" ? 0xB5 : k == "down" ? 0xB6 : k == "right" ? 0xB7
: (uint8_t)(k.empty() ? 0 : k[0]);
if (code) webKeyPush(code);
s_script.wait_until = now + 150; return;
}
if (cmd == "wheel") { // wheel X Y DY: a wheel turn over (X, Y), DY canvas pixels (> 0 = down)
int x = 0, y = 0, dy = 0; in >> x >> y >> dy;
webWheel(x, y, dy);
s_script.wait_until = now + 50; return;
}
if (cmd == "shot") { std::string n; in >> n; savePng(s_shot_dir + "/" + n + ".png"); continue; }
if (cmd == "quit") { s_quit = true; return; }
fprintf(stderr, "[script] unknown command '%s'\n", cmd.c_str());
}
}
} // namespace
bool wadaHostStep() {
pumpEvents();
if (!s_script.lines.empty()) scriptStep();
wadaBoardStep();
present();
return !s_quit;
}
int main(int argc, char** argv) {
bool headless = false;
int scale = 1;
std::string script_path;
WadaBackendChoice choice; // the demo unless a node is named
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--headless") headless = true;
else if (a == "--script" && i + 1 < argc) script_path = argv[++i];
else if (a == "--shots" && i + 1 < argc) s_shot_dir = argv[++i];
else if (a == "--scale" && i + 1 < argc) scale = atoi(argv[++i]);
else if (a == "--size" && i + 1 < argc) { // e.g. 480x960: a phone's portrait canvas
int w = 0, h = 0;
if (sscanf(argv[++i], "%dx%d", &w, &h) == 2) wadaWebSetSize(w, h);
}
else if (a == "--serial" && i + 1 < argc) { choice.kind = "serial"; choice.arg = argv[++i]; }
else if (a == "--tcp" && i + 1 < argc) { choice.kind = "tcp"; choice.arg = argv[++i]; }
else if (a == "--read-only") choice.read_only = true;
}
// A headless run (the smoke tour, CI) stays offline unless asked: no map tiles from
// the network in its screenshots (compat/wada_http.h).
if (headless) setenv("WADA_NET", "0", 0);
if (!script_path.empty()) {
std::ifstream f(script_path);
for (std::string l; std::getline(f, l);) s_script.lines.push_back(l);
if (s_script.lines.empty()) fprintf(stderr, "[web] script %s is empty or missing\n", script_path.c_str());
}
wadaHostMkdirs(s_shot_dir);
setvbuf(stdout, nullptr, _IOLBF, 0);
if (!headless) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) { fprintf(stderr, "SDL_Init: %s\n", SDL_GetError()); return 1; }
if (scale < 1) scale = 1;
s_win = SDL_CreateWindow("wadamesh", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
WADA_WEB_W * scale, WADA_WEB_H * scale, SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
s_ren = SDL_CreateRenderer(s_win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!s_ren) s_ren = SDL_CreateRenderer(s_win, -1, 0);
SDL_RenderSetLogicalSize(s_ren, WADA_WEB_W, WADA_WEB_H); // mouse coordinates arrive in canvas pixels
s_tex = SDL_CreateTexture(s_ren, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STREAMING, WADA_WEB_W, WADA_WEB_H);
SDL_StartTextInput();
}
wadaBoardBegin(choice); // demo, or the companion node --serial / --tcp named
while (wadaHostStep()) SDL_Delay(4);
// Closing the window is the web board's power-off: flush what the device flushes
// on its deliberate power paths (chat history is written lazily).
wadaBoardFlush();
if (s_win) { SDL_DestroyTexture(s_tex); SDL_DestroyRenderer(s_ren); SDL_DestroyWindow(s_win); SDL_Quit(); }
fflush(stdout);
_exit(0); // detached host tasks may still be sleeping; do not unwind them
}