Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/building/compilation-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ These options are for development and debugging. All default to `OFF`.
| `SIMU_DISKIO` | OFF | Enable disk I/O simulation using a `sdcard.image` file |
| `SIMU_LUA_COMPILER` | ON | Pre-compile and cache Lua scripts in the simulator |
| `SIMU_TARGET` | ON | Build the simulator target |
| `WIDGET_STUDIO` | OFF | Enable native-simulator automation hooks, including command-file steering, deterministic PNG capture and the simulator-only `simu` Lua module |
| `DISABLE_COMPANION` | OFF | Skip building Companion and simulators |

---
Expand Down
11 changes: 11 additions & 0 deletions radio/src/gui/colorlcd/LvglWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
#include "os/time.h"
#include "view_main.h"

#if defined(SIMU) && defined(WIDGET_STUDIO)
#include "targets/simu/simulib.h"
#endif

LvglWrapper* LvglWrapper::_instance = nullptr;

static lv_indev_drv_t touchDriver;
Expand Down Expand Up @@ -367,6 +371,13 @@ void LvglWrapper::run()
if (!updating) {
// Normal UI loop - call lgvl timer handler
updating = true;
#if defined(SIMU) && defined(WIDGET_STUDIO)
// A capture armed against an already-static screen would otherwise wait
// forever for a flush that never comes (see simuCaptureArm).
if (simuConsumeCaptureRedraw()) {
lv_obj_invalidate(lv_scr_act());
}
#endif
lv_timer_handler();
updating = false;
} else {
Expand Down
114 changes: 114 additions & 0 deletions radio/src/lua/api_simu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright (C) EdgeTX
*
* Widget Studio sim-only Lua module (simu target + WIDGET_STUDIO only).
* Provides the low-level simulator controls the harness cannot reach through
* the public Lua API:
* simu.setSwitch(name, state) - physical switch -1/0/1
* simu.setAnalog(name, value) - ADC input in 0..4096
* simu.armCapture(path) - deterministic one-shot PNG of the next frame
* simu.reset() - full simulator reset (hot reload)
*
* Telemetry injection is intentionally NOT here: the public global
* setTelemetryValue(id, subId, instance, value, unit, prec, name) already
* registers real sensors through the real sensor registry, which is what the
* harness needs for truthful getValue()/getFieldInfo()/CELLS behaviour.
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*/

#include <string.h>
#include <string>

#define LUA_LIB
#include "edgetx.h"
#include "lua_api.h"
#include "lua_states.h"
#include "switches.h"
#include "strhelpers.h"
#include "hal/adc_driver.h"

#if defined(COLORLCD) && defined(SIMU) && defined(WIDGET_STUDIO)

#include "targets/simu/simulib.h"

static int luaSimuSetSwitch(lua_State* L)
{
const char* name = luaL_checkstring(L, 1);
int state = luaL_checkinteger(L, 2);
if (state < -1) state = -1;
if (state > 1) state = 1;

uint8_t count = getSwitchCount();
for (uint8_t i = 0; i < count; i++) {
char buf[16];
const char* sname = getSwitchName(buf, i);
if (sname && !strcmp(sname, name)) {
simuSetSwitch(i, (int8_t)state);
lua_pushboolean(L, true);
return 1;
}
}

lua_pushboolean(L, false);
return 1;
}

static int luaSimuSetAnalog(lua_State* L)
{
const char* name = luaL_checkstring(L, 1);
int value = luaL_checkinteger(L, 2);
if (value < 0) value = 0;
if (value > 4096) value = 4096;

int idx = adcGetInputIdx(name, strlen(name));
if (idx < 0) {
lua_pushboolean(L, false);
return 1;
}

simuSetAnalogValue((uint8_t)idx, (uint16_t)value);
lua_pushboolean(L, true);
return 1;
}

static int luaSimuArmCapture(lua_State* L)
{
const char* path = luaL_checkstring(L, 1);
if (path[0] == '\0') {
lua_pushboolean(L, false);
return 1;
}

// Resolve a radio-style path (for example /SCREENSHOTS/x.png) through the
// simulator's configured SD-card mapping.
std::string real = simuFatfsGetRealPath(path);
if (real.empty()) {
lua_pushboolean(L, false);
return 1;
}
simuCaptureArm(real.c_str());

lua_pushboolean(L, true);
return 1;
}

static int luaSimuReset(lua_State* L)
{
// Deliberately async: the native simu main loop consumes the request between
// frames, so this is safe to call from inside Lua.
simuRequestReset();
lua_pushboolean(L, true);
return 1;
}

extern "C" {
LROT_BEGIN(simulib, NULL, 0)
LROT_FUNCENTRY( setSwitch, luaSimuSetSwitch )
LROT_FUNCENTRY( setAnalog, luaSimuSetAnalog )
LROT_FUNCENTRY( armCapture, luaSimuArmCapture )
LROT_FUNCENTRY( reset, luaSimuReset )
LROT_END(simulib, NULL, 0)
}

#endif // COLORLCD && SIMU && WIDGET_STUDIO
16 changes: 16 additions & 0 deletions radio/src/lua/api_simu.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (C) EdgeTX
*
* Widget Studio sim-only Lua module. Compiled for the simu target only and
* gated by WIDGET_STUDIO (see radio/src/targets/simu/CMakeLists.txt). Keep
* this module limited to development automation; it must not grow into
* a firmware-facing API.
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*/

#pragma once

#if defined(COLORLCD) && defined(SIMU) && defined(WIDGET_STUDIO)
extern LROT_TABLE(simulib);
#endif
45 changes: 45 additions & 0 deletions radio/src/targets/simu/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ if(NOT SIMU_TARGET)
return()
endif()

# Widget Studio dev hooks (simu target only, never release firmware):
# - simu Lua module (api_simu.cpp), gated by WIDGET_STUDIO
# - deterministic PNG LCD capture (simuCaptureArm / simuLcdNotify)
# - analog override + --pipe steering channel (sdl_simu.cpp)
option(WIDGET_STUDIO "Widget Studio dev hooks" OFF)
if(WIDGET_STUDIO)
message(STATUS "Widget Studio dev hooks: ON (simu target only)")
endif()

if(MINGW)
# struct packing breaks on MinGW w/out -mno-ms-bitfields:
# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52991
Expand Down Expand Up @@ -57,6 +66,28 @@ get_property(SIMU_SRC_OPTIONS
target_compile_options(simu_drivers PRIVATE ${SIMU_SRC_OPTIONS})
set_property(TARGET simu_drivers PROPERTY POSITION_INDEPENDENT_CODE ON)

if(WIDGET_STUDIO AND NOT WASI)
# Compile the Lua module and simulator state only when explicitly enabled.
# The PNG backend remains in the interactive simu target below so
# gtests-radio and wasi-module never pull in another stb implementation.
target_sources(radiolib_native PRIVATE
${RADIO_SRC_DIR}/lua/api_simu.cpp
)

# Limit WIDGET_STUDIO to the sources that implement the hooks. This keeps
# the default simulator's preprocessor surface and rebuild footprint intact.
set_property(SOURCE
${RADIO_SRC_DIR}/lua/api_simu.cpp
${RADIO_SRC_DIR}/gui/colorlcd/LvglWrapper.cpp
${RADIO_SRC_DIR}/thirdparty/Lua/src/linit.c
TARGET_DIRECTORY radiolib_native
APPEND PROPERTY COMPILE_DEFINITIONS WIDGET_STUDIO
)
set_property(SOURCE simulib.cpp
APPEND PROPERTY COMPILE_DEFINITIONS WIDGET_STUDIO
)
endif()

if(SIMU_AUX)
target_compile_definitions(simu_drivers PRIVATE -DSIMU_COM_PORT=${SIMU_COM_PORT})
endif()
Expand Down Expand Up @@ -136,6 +167,20 @@ else()

target_compile_options(simu PUBLIC -DSIMU)

if(WIDGET_STUDIO)
# The SDL main loop (sdl_simu.cpp) owns the --pipe steering channel and
# the reset-request consumption; it lives in the simu executable target.
# simu_capture.cpp (the stb_image_write PNG encoder) is here rather than
# in simu_drivers for the same reason - see the comment above.
target_sources(simu PRIVATE simu_capture.cpp)
set_property(SOURCE
arg_parser.cpp
sdl_simu.cpp
simu_capture.cpp
APPEND PROPERTY COMPILE_DEFINITIONS WIDGET_STUDIO
)
endif()

add_png_target(image_assets "assets/images/*.png")
add_dependencies(simu image_assets)

Expand Down
23 changes: 22 additions & 1 deletion radio/src/targets/simu/arg_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ bool ArgumentParser::parse(int argc, char *argv[]) {
} else if (arg == "--settings") {
if (!getNextArg(argc, argv, i, settings_path, "settings"))
return false;
#if defined(WIDGET_STUDIO)
} else if (arg == "--pipe") {
if (!getNextArg(argc, argv, i, pipe_path, "pipe"))
return false;
#endif
} else if (arg == "-h" || arg == "--help") {
help_requested = true;
return true;
Expand All @@ -38,7 +43,11 @@ bool ArgumentParser::parse(int argc, char *argv[]) {

void ArgumentParser::printUsage() const {
printf("usage: %s [--width width] [--height height] [--storage path] "
"[--settings path] [-h | --help]\n",
"[--settings path] "
#if defined(WIDGET_STUDIO)
"[--pipe path] "
#endif
"[-h | --help]\n",
program_name.c_str());
}

Expand All @@ -49,6 +58,10 @@ void ArgumentParser::printHelp() const {
printf(" --height height Set the height (integer)\n");
printf(" --storage path Set the storage path\n");
printf(" --settings path Set the settings path\n");
#if defined(WIDGET_STUDIO)
printf(" --pipe path Read Widget Studio commands from an\n");
printf(" append-only file (one command per line)\n");
#endif
printf(" -h, --help Show this help message\n");
}

Expand All @@ -66,6 +79,10 @@ const std::string &ArgumentParser::getSettingsPath() const {
return settings_path;
}

#if defined(WIDGET_STUDIO)
const std::string &ArgumentParser::getPipePath() const { return pipe_path; }
#endif

bool ArgumentParser::hasWidth() const { return width != -1; }

bool ArgumentParser::hasHeight() const { return height != -1; }
Expand All @@ -74,6 +91,10 @@ bool ArgumentParser::hasStoragePath() const { return !storage_path.empty(); }

bool ArgumentParser::hasSettingsPath() const { return !settings_path.empty(); }

#if defined(WIDGET_STUDIO)
bool ArgumentParser::hasPipePath() const { return !pipe_path.empty(); }
#endif

bool ArgumentParser::getNextArg(int argc, char *argv[], int &i,
std::string &value,
const std::string &option_name) {
Expand Down
9 changes: 9 additions & 0 deletions radio/src/targets/simu/arg_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ class ArgumentParser {
int height = -1;
std::string storage_path;
std::string settings_path;
#if defined(WIDGET_STUDIO)
std::string pipe_path;
#endif
bool help_requested = false;
std::string program_name;

Expand All @@ -24,12 +27,18 @@ class ArgumentParser {
int getHeight() const;
const std::string &getStoragePath() const;
const std::string &getSettingsPath() const;
#if defined(WIDGET_STUDIO)
const std::string &getPipePath() const;
#endif

// Check if option was provided
bool hasWidth() const;
bool hasHeight() const;
bool hasStoragePath() const;
bool hasSettingsPath() const;
#if defined(WIDGET_STUDIO)
bool hasPipePath() const;
#endif

private:
bool getNextArg(int argc, char *argv[], int &i, std::string &value,
Expand Down
Loading
Loading