Files
edgetx-gx12/radio/src/targets/simu/simulib.cpp
T
2026-05-08 12:40:56 +10:00

883 lines
18 KiB
C++

/*
* Copyright (C) EdgeTX
*
* Based on code named
* opentx - https://github.com/opentx/opentx
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "simulib.h"
#include "simulcd.h"
#include "hal/adc_driver.h"
#include "hal/rotary_encoder.h"
#include "hal/usb_driver.h"
#include "os/sleep.h"
#include "os/task.h"
#include "edgetx.h"
#include "debug.h"
#include "switches.h"
#include "input_mapping.h"
#include "gui/gui_common.h"
#include "mixes.h"
#if defined(GVARS)
#include "gvars.h"
#endif
#include "trainer.h"
#include "telemetry/frsky.h"
#include "telemetry/crossfire.h"
#if defined(LUA)
#include "lua/lua_api.h"
#endif
#include <assert.h>
#include <deque>
int g_snapshot_idx = 0;
extern uint8_t startOptions;
char * main_thread_error = nullptr;
bool simu_shutdown = false;
bool simu_running = false;
bool simuCreateDefaultSettings = false;
volatile rotenc_t rotencValue = 0;
volatile uint32_t rotencDt = 0;
rotenc_t rotaryEncoderGetValue()
{
return rotencValue / ROTARY_ENCODER_GRANULARITY;
}
extern const etx_hal_adc_driver_t simu_adc_driver;
void lcdCopy(void * dest, void * src);
void lcdFlushed();
#if defined(AUX_SERIAL) || defined(AUX2_SERIAL)
static void hostSerialInit();
#endif
void simuInit()
{
#if defined(ROTARY_ENCODER_NAVIGATION)
rotencValue = 0;
#endif
// Route firmware TRACE() output to host via WASM import
traceCallback = simuTrace;
// Init ADC driver callback
adcInit(&simu_adc_driver);
// Switches
switchInit();
#if defined(AUX_SERIAL) || defined(AUX2_SERIAL)
hostSerialInit();
#endif
}
bool keysStates[MAX_KEYS] = { false };
void simuSetKey(uint8_t key, bool state)
{
assert(key < DIM(keysStates));
keysStates[key] = state;
}
bool trimsStates[MAX_TRIMS * 2] = { false };
void simuSetTrim(uint8_t trim, bool state)
{
assert(trim < DIM(trimsStates));
trimsStates[trim] = state;
}
#if defined(SIMU_BOOTLOADER)
int bootloaderMain();
static void* bootloaderThread(void*)
{
bootloaderMain();
return nullptr;
}
#endif
void simuCreateDefaults()
{
simuCreateDefaultSettings = true;
}
void simuStart(bool tests)
{
if (simu_running)
return;
#if !defined(COLORLCD)
menuLevel = 0;
#endif
startOptions = (tests ? 0 : OPENTX_START_NO_SPLASH | OPENTX_START_NO_CALIBRATION | OPENTX_START_NO_CHECKS);
simu_shutdown = false;
/*
g_tmr10ms must be non-zero otherwise some SF functions (that use this timer as a marker when it was last executed)
will be executed twice on startup. Normal radio does not see this issue because g_tmr10ms is already a big number
before the first call to the Special Functions. Not so in the simulator.
There is another issue, some other function static variables depend on this value. If simulator is started
multiple times in one Companion session, they are set to their initial values only first time the simulator
is started. Therefore g_tmr10ms must also be set to non-zero value only the first time, then it must be left
alone to continue from the previous simulator session value. See the issue #2446
*/
if (g_tmr10ms == 0) {
g_tmr10ms = 1;
}
#if defined(RTCLOCK)
time_t rawtime;
struct tm * timeinfo;
time (&rawtime);
timeinfo = localtime (&rawtime);
if (timeinfo != nullptr) {
struct gtm gti;
gti.tm_sec = timeinfo->tm_sec;
gti.tm_min = timeinfo->tm_min;
gti.tm_hour = timeinfo->tm_hour;
gti.tm_mday = timeinfo->tm_mday;
gti.tm_mon = timeinfo->tm_mon;
gti.tm_year = timeinfo->tm_year;
gti.tm_wday = timeinfo->tm_wday;
gti.tm_yday = timeinfo->tm_yday;
g_rtcTime = gmktime(&gti);
} else {
g_rtcTime = rawtime;
}
#endif
// Init LCD callbacks
lcdInit();
#if !defined(SIMU_BOOTLOADER)
simuMain();
#else
pthread_attr_t attr;
pthread_attr_init(&attr);
struct sched_param sp;
sp.sched_priority = SCHED_RR;
pthread_attr_setschedparam(&attr, &sp);
pthread_t bl_pid;
pthread_create(&bl_pid, &attr, &bootloaderThread, nullptr);
#endif
simu_running = true;
}
extern task_handle_t mixerTaskId;
extern task_handle_t menusTaskId;
#if defined(AUDIO)
extern task_handle_t audioTaskId;
#endif
void simuStop()
{
if (!simu_running)
return;
simu_shutdown = true;
task_shutdown_all();
simu_running = false;
}
bool simuIsRunning()
{
return simu_running;
}
bool simuLcdChanged()
{
bool changed = simuLcdRefresh;
simuLcdRefresh = false;
return changed;
}
uint32_t simuLcdCopy(uint8_t* buf, uint32_t maxLen)
{
uint32_t size = DISPLAY_BUFFER_SIZE * sizeof(pixel_t);
if (size > maxLen) size = maxLen;
memcpy(buf, simuLcdBuf, size);
return size;
}
uint32_t simuLcdGetWidth()
{
return LCD_W;
}
uint32_t simuLcdGetHeight()
{
return LCD_H;
}
uint32_t simuLcdGetDepth()
{
#if defined(COLORLCD)
return 16;
#elif LCD_W == 212
return 4;
#else
return 1;
#endif
}
#if !defined(COLORLCD)
void lcdSetRefVolt(uint8_t val)
{
}
#endif
#if LCD_W == 128
void lcdSetInvert(bool invert)
{
}
#endif
void boardInit()
{
switchInit();
}
uint32_t pwrCheck() { return simu_shutdown ? e_power_off : e_power_on; }
bool pwrPressed() { return false; }
bool pwrOffPressed()
{
#if defined(PWR_BUTTON_PRESS)
return pwrPressed();
#else
return !pwrPressed();
#endif
}
void pwrInit() {}
void pwrOn() {}
void pwrOff() {}
bool UNEXPECTED_SHUTDOWN() { return false; }
void SET_POWER_REASON(uint32_t value) {}
#if defined(TRIMS_EMULATE_BUTTONS)
bool trimsAsButtons = false;
void setHatsAsKeys(bool val) { trimsAsButtons = val; }
bool getHatsAsKeys()
{
bool lua = false;
#if defined(LUA)
lua = isLuaStandaloneRunning();
#endif
return (trimsAsButtons || lua);
}
#endif
void pollKeys()
{
}
uint32_t readKeys()
{
uint32_t result = 0;
for (int i = 0; i < MAX_KEYS; i++) {
if (keysStates[i]) {
result |= 1 << i;
}
}
return result;
}
uint32_t readTrims()
{
uint32_t trims = 0;
for (int i = 0; i < keysGetMaxTrims() * 2; i++) {
if (trimsStates[i]) {
trims |= 1 << i;
}
}
return trims;
}
int usbPlugged() { return false; }
int getSelectedUsbMode() { return USB_JOYSTICK_MODE; }
void setSelectedUsbMode(int mode) {}
void delay_ms(uint32_t ms) { }
void delay_us(uint16_t us) { }
void flashWrite(uint32_t *address, const uint32_t *buffer)
{
sleep_ms(10);
}
uint32_t isBootloaderStart(const uint8_t * block)
{
return 1;
}
#if defined(PCBXLITES)
bool isJackPlugged()
{
return false;
}
#endif
void serialPrintf(const char * format, ...) { }
void serialCrlf() { }
void serialPutc(char c) { }
void boardOff()
{
}
void hapticOff() {}
#if defined(HAS_HARDWARE_OPTIONS)
HardwareOptions hardwareOptions;
#endif
uint32_t Master_frequency = 0;
uint32_t Current_used = 0;
uint16_t Current_max = 0;
void setSticksGain(uint8_t)
{
}
uint16_t getCurrent()
{
return 10;
}
void calcConsumption()
{
}
void handleJackConnection() {}
int trainerModuleSbusGetByte(unsigned char*) { return 0; }
void rtcInit()
{
}
void rtcGetTime(struct gtm * t)
{
}
void rtcSetTime(const struct gtm * t)
{
}
#if defined(PCBTARANIS)
void sdPoll10ms() {}
#endif
uint32_t SD_GetCardType() { return 0; }
#if defined(USB_SERIAL)
const etx_serial_port_t UsbSerialPort = { "USB-VCP", nullptr, nullptr };
#endif
#if defined(AUX_SERIAL) || defined(AUX2_SERIAL)
#if !defined(__wasm__)
// Native builds (unit-tests, SDL simulator) don't have a WASM host to
// forward aux-serial traffic to: WASM_IMPORT is a no-op macro off-target,
// so the declarations in simulib.h become plain externs with no
// definitions. Provide no-op stubs so host_drv_* can link — the host
// bridge is only meaningfully exercised in the WASM simulator, where
// these symbols are resolved as WAMR imports by Companion.
void simuAuxSerialStart(uint8_t, uint32_t, uint8_t) {}
void simuAuxSerialStop(uint8_t) {}
void simuAuxSerialSetBaudrate(uint8_t, uint32_t) {}
void simuAuxSerialSendBuffer(uint8_t, const uint8_t*, uint32_t) {}
#endif
// Per-port bridge state. TX is forwarded to the host via WASM imports;
// RX bytes pushed in via simuAuxSerialReceive() are buffered in rxQueue
// and consumed by the firmware through host_drv_get_byte().
struct host_serial_port_t {
uint8_t index;
mutex_handle_t rxMutex;
std::deque<uint8_t> rxQueue;
};
static host_serial_port_t hostSerialPorts[MAX_AUX_SERIAL] = {
{ SP_AUX1, {}, {} },
{ SP_AUX2, {}, {} },
};
static void hostSerialInit()
{
for (uint8_t i = 0; i < MAX_AUX_SERIAL; ++i)
mutex_create(&hostSerialPorts[i].rxMutex);
}
static void* host_drv_init(void* hw_def, const etx_serial_init* dev)
{
if (hw_def == nullptr || dev == nullptr) return nullptr;
auto* port = static_cast<host_serial_port_t*>(hw_def);
mutex_lock(&port->rxMutex);
port->rxQueue.clear();
mutex_unlock(&port->rxMutex);
simuAuxSerialStart(port->index, dev->baudrate, dev->encoding);
return port;
}
static void host_drv_deinit(void* ctx)
{
if (ctx == nullptr) return;
auto* port = static_cast<host_serial_port_t*>(ctx);
simuAuxSerialStop(port->index);
}
static void host_drv_send_byte(void* ctx, uint8_t b)
{
if (ctx == nullptr) return;
auto* port = static_cast<host_serial_port_t*>(ctx);
simuAuxSerialSendBuffer(port->index, &b, 1);
}
static void host_drv_send_buffer(void* ctx, const uint8_t* b, uint32_t l)
{
if (ctx == nullptr || b == nullptr || l == 0) return;
auto* port = static_cast<host_serial_port_t*>(ctx);
simuAuxSerialSendBuffer(port->index, b, l);
}
static bool host_drv_tx_completed(void*) { return true; }
static int host_drv_get_byte(void* ctx, uint8_t* b)
{
if (ctx == nullptr || b == nullptr) return 0;
auto* port = static_cast<host_serial_port_t*>(ctx);
mutex_lock(&port->rxMutex);
if (port->rxQueue.empty()) {
mutex_unlock(&port->rxMutex);
return 0;
}
*b = port->rxQueue.front();
port->rxQueue.pop_front();
mutex_unlock(&port->rxMutex);
return 1;
}
static void host_drv_set_baudrate(void* ctx, uint32_t baudrate)
{
if (ctx == nullptr) return;
auto* port = static_cast<host_serial_port_t*>(ctx);
simuAuxSerialSetBaudrate(port->index, baudrate);
}
const etx_serial_driver_t host_drv = {
.init = host_drv_init,
.deinit = host_drv_deinit,
.sendByte = host_drv_send_byte,
.sendBuffer = host_drv_send_buffer,
.txCompleted = host_drv_tx_completed,
.waitForTxCompleted = nullptr,
.enableRx = nullptr,
.getByte = host_drv_get_byte,
.getLastByte = nullptr,
.getBufferedBytes = nullptr,
.copyRxBuffer = nullptr,
.clearRxBuffer = nullptr,
.getBaudrate = nullptr,
.setBaudrate = host_drv_set_baudrate,
.setPolarity = nullptr,
.setHWOption = nullptr,
.setReceiveCb = nullptr,
.setIdleCb = nullptr,
.setBaudrateCb = nullptr,
};
void simuAuxSerialReceive(uint8_t port_nr, const uint8_t* data, uint32_t len)
{
if (port_nr >= MAX_AUX_SERIAL || data == nullptr || len == 0) return;
auto& port = hostSerialPorts[port_nr];
mutex_lock(&port.rxMutex);
for (uint32_t i = 0; i < len; ++i)
port.rxQueue.push_back(data[i]);
mutex_unlock(&port.rxMutex);
}
#if defined(AUX_SERIAL_PWR_GPIO)
static void null_pwr_aux(uint8_t) {}
#endif
#endif
#if defined(AUX_SERIAL)
#if defined(AUX_SERIAL_PWR_GPIO)
#define AUX_SERIAL_PWR null_pwr_aux
#else
#define AUX_SERIAL_PWR nullptr
#endif
static etx_serial_port_t auxSerialPort = {
"AUX1",
&host_drv,
&hostSerialPorts[SP_AUX1],
AUX_SERIAL_PWR
};
#define AUX_SERIAL_PORT &auxSerialPort
#else
#define AUX_SERIAL_PORT nullptr
#endif
#if defined(AUX2_SERIAL)
#if defined(AUX_SERIAL_PWR_GPIO)
#define AUX2_SERIAL_PWR null_pwr_aux
#else
#define AUX2_SERIAL_PWR nullptr
#endif
static etx_serial_port_t aux2SerialPort = {
"AUX2",
&host_drv,
&hostSerialPorts[SP_AUX2],
AUX2_SERIAL_PWR
};
#define AUX2_SERIAL_PORT &aux2SerialPort
#else
#define AUX2_SERIAL_PORT nullptr
#endif // AUX2_SERIAL
etx_serial_port_t* serialPorts[MAX_AUX_SERIAL] = {
AUX_SERIAL_PORT,
AUX2_SERIAL_PORT,
};
const etx_serial_port_t* auxSerialGetPort(int port_nr)
{
if (port_nr >= MAX_AUX_SERIAL) return nullptr;
return serialPorts[port_nr];
}
#if defined(HARDWARE_TOUCH)
struct TouchState simTouchState = {};
bool simTouchOccured = false;
bool touchPanelInit()
{
simTouchState.x = simTouchState.y = 0;
return true;
}
bool touchPanelEventOccured()
{
if(simTouchOccured)
{
simTouchOccured = false;
return true;
}
return false;
}
void touchPanelDown(short x, short y)
{
simTouchState.x = x;
simTouchState.y = y;
simTouchState.event = TE_DOWN;
simTouchOccured = true;
}
void touchPanelUp()
{
simTouchState.event = TE_UP;
simTouchOccured = true;
}
struct TouchState touchPanelRead()
{
struct TouchState st = simTouchState;
simTouchState.deltaX = 0;
simTouchState.deltaY = 0;
return st;
}
struct TouchState getInternalTouchState()
{
return simTouchState;
}
#endif
void simuTouchDown(int16_t x, int16_t y)
{
#if defined(HARDWARE_TOUCH)
touchPanelDown(x, y);
#endif
}
void simuTouchUp()
{
#if defined(HARDWARE_TOUCH)
touchPanelUp();
#endif
}
void simuRotaryEncoderEvent(int32_t steps)
{
#if defined(ROTARY_ENCODER_NAVIGATION)
rotencValue += steps * ROTARY_ENCODER_GRANULARITY;
#endif
}
int32_t simuGetCapability(uint8_t cap)
{
switch (cap) {
case 0: // CAP_LUA
#ifdef LUA
return 1;
#else
return 0;
#endif
case 1: // CAP_ROTARY_ENC
return 0;
case 2: // CAP_ROTARY_ENC_NAV
#ifdef ROTARY_ENCODER_NAVIGATION
return 1;
#else
return 0;
#endif
case 3: // CAP_TELEM_FRSKY_SPORT
return 1;
case 4: // CAP_SERIAL_AUX1
return (auxSerialGetPort(SP_AUX1) != nullptr) ? 1 : 0;
case 5: // CAP_SERIAL_AUX2
return (auxSerialGetPort(SP_AUX2) != nullptr) ? 1 : 0;
default:
return 0;
}
}
void simuSetTrimValue(uint8_t idx, int32_t value)
{
unsigned i = inputMappingConvertMode(idx);
uint8_t phase = getTrimFlightMode(getFlightMode(), i);
setTrimValue(phase, i, value);
}
void simuSendTelemetry(uint8_t module, uint8_t protocol,
const uint8_t* data, uint32_t len)
{
switch (protocol) {
case 0: // SIMU_TELEMETRY_PROTOCOL_FRSKY_SPORT
sportProcessTelemetryPacket(module, data, len);
break;
case 1: // SIMU_TELEMETRY_PROTOCOL_FRSKY_HUB
frskyDProcessPacket(module, data, len);
break;
case 2: // SIMU_TELEMETRY_PROTOCOL_CROSSFIRE
processCrossfireTelemetryFrame(module, (uint8_t*)data, len);
break;
case 3: // SIMU_TELEMETRY_PROTOCOL_FRSKY_HUB_OOB
if (len >= 3) {
uint8_t id = data[0];
int16_t value = ((uint8_t)(data[2]) << 8) + (uint8_t)(data[1]);
processHubPacket(id, value);
}
break;
default:
break;
}
}
void simuLuaReloadPermanentScripts()
{
#if defined(LUA)
luaState = INTERPRETER_RELOAD_PERMANENT_SCRIPTS;
#endif
}
void simuLcdFlushed()
{
::lcdFlushed();
}
uint8_t simuGetMaxTrainerChannels()
{
return MAX_TRAINER_CHANNELS;
}
void simuCopyTrainerInput(const int16_t* buf, uint8_t count)
{
if (count > MAX_TRAINER_CHANNELS)
count = MAX_TRAINER_CHANNELS;
for (uint8_t i = 0; i < count; i++) {
int16_t v = buf[i];
if (v < -512) v = -512;
if (v > 512) v = 512;
trainerInput[i] = v;
}
}
void simuSetTrainerTimeout(uint16_t ms)
{
trainerSetTimer(ms / 10);
}
// -- Output values --
uint8_t simuGetNumChannels()
{
return MAX_OUTPUT_CHANNELS;
}
uint8_t simuCopyChannelOutputs(int16_t* buf, uint8_t maxCount)
{
uint8_t n = MAX_OUTPUT_CHANNELS < maxCount ? MAX_OUTPUT_CHANNELS : maxCount;
memcpy(buf, channelOutputs, n * sizeof(int16_t));
return n;
}
uint8_t simuCopyMixOutputs(int16_t* buf, uint8_t maxCount)
{
uint8_t n = MAX_OUTPUT_CHANNELS < maxCount ? MAX_OUTPUT_CHANNELS : maxCount;
memcpy(buf, ex_chans, n * sizeof(int16_t));
return n;
}
bool simuIsChannelUsed(uint8_t channel)
{
return isChannelUsed(channel);
}
int simuGetChannelsUsed()
{
return getChannelsUsed();
}
uint8_t simuGetMixCount()
{
return getMixCount();
}
uint8_t simuGetNumLogicalSwitches()
{
return MAX_LOGICAL_SWITCHES;
}
uint8_t simuCopyLogicalSwitches(uint8_t* buf, uint8_t maxCount)
{
uint8_t n = MAX_LOGICAL_SWITCHES < maxCount ? MAX_LOGICAL_SWITCHES : maxCount;
for (uint8_t i = 0; i < n; i++)
buf[i] = getSwitch(SWSRC_FIRST_LOGICAL_SWITCH + i, 0) ? 1 : 0;
return n;
}
int32_t simuGetTrimValue(uint8_t idx)
{
uint8_t phase = getFlightMode();
uint8_t mapped = inputMappingConvertMode(idx);
return getTrimValue(getTrimFlightMode(phase, mapped), mapped);
}
int16_t simuGetTrimRange()
{
return g_model.extendedTrims ? TRIM_EXTENDED_MAX : TRIM_MAX;
}
int32_t simuGetFlightMode()
{
return getFlightMode();
}
uint8_t simuGetNumGVars()
{
#if defined(GVARS)
return MAX_GVARS;
#else
return 0;
#endif
}
uint8_t simuGetNumFlightModes()
{
return MAX_FLIGHT_MODES;
}
int32_t simuGetGVar(uint8_t gv, uint8_t fm)
{
#if defined(GVARS)
if (gv < MAX_GVARS && fm < MAX_FLIGHT_MODES) {
uint8_t prec = g_model.gvars[gv].prec;
uint8_t unit = g_model.gvars[gv].unit;
int16_t value = (int16_t)GVAR_VALUE(gv, getGVarFlightMode(fm, gv));
// Encode as gVarMode_t: value[15:0] | mode[23:16] | prec[25:24] | unit[27:26]
return (((unit & 0x3) << 26) | ((prec & 0x3) << 24) |
((fm & 0xFF) << 16) | (value & 0xFFFF));
}
#endif
return 0;
}
bool simuGetBacklightState()
{
return isBacklightEnabled();
}
// -- Custom (function) switches --
uint8_t simuGetNumCustomSwitches()
{
return NUM_FUNCTIONS_SWITCHES;
}
uint8_t simuGetCustomSwitchIndex(uint8_t cfsIdx)
{
#if defined(FUNCTION_SWITCHES)
return switchGetSwitchFromCustomIdx(cfsIdx);
#else
(void)cfsIdx;
return 0;
#endif
}
bool simuGetCustomSwitchState(uint8_t idx)
{
#if defined(FUNCTION_SWITCHES)
if (idx < NUM_FUNCTIONS_SWITCHES)
return fsLedState(idx);
#endif
return false;
}
uint32_t simuGetCustomSwitchColor(uint8_t idx)
{
#if defined(FUNCTION_SWITCHES)
if (idx < NUM_FUNCTIONS_SWITCHES)
return fsGetLedRGB(idx);
#endif
return 0;
}