From 14e6d6ce0139531fcd109af8535b31fac98699ac Mon Sep 17 00:00:00 2001 From: Kudzz <75142721+Kudzzo@users.noreply.github.com> Date: Mon, 20 Jul 2026 05:09:43 -0400 Subject: [PATCH] feat(cpn): visual and audible feedback for haptic in simulator (#7322) --- companion/src/simulation/simulatorinterface.h | 1 + .../src/simulation/simulatormainwindow.cpp | 34 +++++++++++++++++++ .../src/simulation/simulatormainwindow.h | 1 + .../src/simulation/wasmsimulatorinterface.cpp | 25 +++++++++++++- .../src/simulation/wasmsimulatorinterface.h | 3 ++ radio/src/haptic.cpp | 9 +++++ radio/src/targets/simu/simulib.cpp | 16 +++++++-- radio/src/targets/simu/simulib.h | 3 +- radio/src/targets/taranis/CMakeLists.txt | 1 + 9 files changed, 89 insertions(+), 4 deletions(-) diff --git a/companion/src/simulation/simulatorinterface.h b/companion/src/simulation/simulatorinterface.h index c24dd8e97..b7e4313e2 100644 --- a/companion/src/simulation/simulatorinterface.h +++ b/companion/src/simulation/simulatorinterface.h @@ -191,6 +191,7 @@ class SimulatorInterface : public QObject void auxSerialStart(const quint8 port_num); void auxSerialStop(const quint8 port_num); void txBatteryVoltageChanged(const int voltage); + void hapticChanged(int intensity); void fsColorChange(quint8 index, qint32 color); }; diff --git a/companion/src/simulation/simulatormainwindow.cpp b/companion/src/simulation/simulatormainwindow.cpp index 6a2e62668..dd7c93efc 100644 --- a/companion/src/simulation/simulatormainwindow.cpp +++ b/companion/src/simulation/simulatormainwindow.cpp @@ -38,6 +38,9 @@ #include #include #include +#include +#include +#include extern AppData g; // ensure what "g" means @@ -173,6 +176,7 @@ SimulatorMainWindow::SimulatorMainWindow(QWidget *parent, const QString & simula connect(m_simulator, &SimulatorInterface::auxSerialStart, hostSerialConnector, &HostSerialConnector::serialStart); connect(m_simulator, &SimulatorInterface::auxSerialStop, hostSerialConnector, &HostSerialConnector::serialStop); connect(m_simulator, &SimulatorInterface::txBatteryVoltageChanged, this, &SimulatorMainWindow::onTxBatteryVoltageChanged); + connect(m_simulator, &SimulatorInterface::hapticChanged, this, &SimulatorMainWindow::onHapticChanged); } SimulatorMainWindow::~SimulatorMainWindow() @@ -613,3 +617,33 @@ void SimulatorMainWindow::onTxBatteryVoltageChanged(const unsigned int voltage) { m_batVoltage = voltage; } + +// Haptic feedback handler: provides visual and audible substitutes +// for physical vibration when running in the simulator. +void SimulatorMainWindow::onHapticChanged(int intensity) { + Q_UNUSED(intensity); + + // AUDIBLE: system beep as substitute for haptic motor sound + QApplication::beep(); + + // VISUAL: animate opacity to simulate vibration + // Uses QGraphicsEffect which works on Wayland without moving the window + if (m_simulatorWidget) { + QGraphicsOpacityEffect* effect = new QGraphicsOpacityEffect(m_simulatorWidget); + m_simulatorWidget->setGraphicsEffect(effect); + + QPropertyAnimation* anim = new QPropertyAnimation(effect, "opacity", effect); + anim->setDuration(200); + anim->setKeyValueAt(0.0, 1.0); + anim->setKeyValueAt(0.25, 0.5); + anim->setKeyValueAt(0.5, 1.0); + anim->setKeyValueAt(0.75, 0.5); + anim->setKeyValueAt(1.0, 1.0); + anim->start(QAbstractAnimation::DeleteWhenStopped); + + connect(anim, &QPropertyAnimation::finished, this, [this, effect]() { + if (m_simulatorWidget && m_simulatorWidget->graphicsEffect() == effect) + m_simulatorWidget->setGraphicsEffect(nullptr); + }); + } +} diff --git a/companion/src/simulation/simulatormainwindow.h b/companion/src/simulation/simulatormainwindow.h index 56771ac89..528a4dc59 100644 --- a/companion/src/simulation/simulatormainwindow.h +++ b/companion/src/simulation/simulatormainwindow.h @@ -89,6 +89,7 @@ class SimulatorMainWindow : public QMainWindow // TODO: detect if changed via ui as currently event only on GeneralSettings (re)load void onSettingsBatteryChanged(const int batMin, const int batMax, const unsigned int batWarn); void onTxBatteryVoltageChanged(const unsigned int voltage); // something else changed voltage + void onHapticChanged(int intensity); protected: void createDockWidgets(); diff --git a/companion/src/simulation/wasmsimulatorinterface.cpp b/companion/src/simulation/wasmsimulatorinterface.cpp index 35d5dbe0c..42b7bd478 100644 --- a/companion/src/simulation/wasmsimulatorinterface.cpp +++ b/companion/src/simulation/wasmsimulatorinterface.cpp @@ -18,6 +18,7 @@ #include #include #include +#include // WAMR native callback: called by WASM module to get analog values in // ADC range (0..4096, center=2048). The WASM ADC driver does a direct @@ -131,7 +132,8 @@ WasmSimulatorInterface::WasmSimulatorInterface(const QString & wasmPath, : SimulatorInterface(), m_wasmPath(wasmPath), m_boardName(boardName), - m_boardType(boardType) + m_boardType(boardType), + m_lastHaptic(0) { } @@ -392,6 +394,9 @@ bool WasmSimulatorInterface::resolveExports() m_fnAuxSerialReceive = wasm_runtime_lookup_function(m_moduleInst, "simuAuxSerialReceive"); + // Haptic feedback polling + m_fnGetHaptic = wasm_runtime_lookup_function(m_moduleInst, "simuGetHaptic"); + m_fnMalloc = wasm_runtime_lookup_function(m_moduleInst, "malloc"); m_fnFree = wasm_runtime_lookup_function(m_moduleInst, "free"); @@ -481,6 +486,7 @@ void WasmSimulatorInterface::init() m_lastOutputs.clear(); m_resetOutputsData = true; + m_lastHaptic = 0; qDebug() << "WASM simulator" << m_boardName << "initialized: LCD" << m_lcdWidth << "x" << m_lcdHeight << "depth" << m_lcdDepth; @@ -1010,6 +1016,23 @@ void WasmSimulatorInterface::run() if (!(loops % 5)) { QMutexLocker lckr(&m_mutex); checkOutputsChanged(); + + // 1. Read the global variable from the WASM module + // "simuHapticValue" must match exactly what you wrote in simulib.cpp + // Poll haptic state from WASM every 50ms. + // When haptic fires, emit hapticChanged() to trigger + // visual (window jitter) and audible (beep) feedback in the simulatormainwindow.cpp + // as a substitute for physical vibration hardware. + if (m_fnGetHaptic) { + uint32_t argv[1] = {0}; + if (wasm_runtime_call_wasm(m_execEnv, m_fnGetHaptic, 0, argv)) { + uint32_t currentHapticValue = argv[0]; + if (currentHapticValue != m_lastHaptic) { + m_lastHaptic = currentHapticValue; + emit hapticChanged((int)currentHapticValue); + } + } + } } } diff --git a/companion/src/simulation/wasmsimulatorinterface.h b/companion/src/simulation/wasmsimulatorinterface.h index 651bab8a3..84deaffb3 100644 --- a/companion/src/simulation/wasmsimulatorinterface.h +++ b/companion/src/simulation/wasmsimulatorinterface.h @@ -189,6 +189,7 @@ class WasmSimulatorInterface : public SimulatorInterface wasm_function_inst_t m_fnGetMaxTrainerChannels = nullptr; wasm_function_inst_t m_fnCopyTrainerInput = nullptr; wasm_function_inst_t m_fnSetTrainerTimeout = nullptr; + wasm_function_inst_t m_fnGetHaptic = nullptr; // Backlight and function switch LEDs wasm_function_inst_t m_fnGetBacklightState = nullptr; @@ -208,6 +209,8 @@ class WasmSimulatorInterface : public SimulatorInterface // Aux serial: host -> firmware data injection wasm_function_inst_t m_fnAuxSerialReceive = nullptr; + uint32_t m_lastHaptic = 0; + wasm_function_inst_t m_fnMalloc = nullptr; wasm_function_inst_t m_fnFree = nullptr; }; diff --git a/radio/src/haptic.cpp b/radio/src/haptic.cpp index 09a334ecb..ed1d7cbdf 100644 --- a/radio/src/haptic.cpp +++ b/radio/src/haptic.cpp @@ -22,6 +22,11 @@ #include "edgetx.h" #include "haptic.h" +#if defined(SIMU) +#include +extern uint32_t simuHapticValue; +#endif + hapticQueue::hapticQueue() { buzzTimeLeft = 0; @@ -96,6 +101,10 @@ void hapticQueue::play(uint8_t tLen, uint8_t tPause, uint8_t tFlags) void hapticQueue::event(uint8_t e) { if (g_eeGeneral.hapticMode >= e_mode_nokeys || (g_eeGeneral.hapticMode >= e_mode_alarms && e <= AU_ERROR)) { +#if defined(SIMU) + simuHapticValue++; + return; +#endif if (e <= AU_ERROR) play(15, 3, PLAY_NOW); else if (e <= AU_MIX_WARNING_3) diff --git a/radio/src/targets/simu/simulib.cpp b/radio/src/targets/simu/simulib.cpp index d5ae5f222..82778f26d 100644 --- a/radio/src/targets/simu/simulib.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -47,6 +47,20 @@ #include #include +#include + +// Monotonic event counter incremented each time haptic fires. +// Host (WasmSimulatorInterface) compares against last-seen value to detect new events. +uint32_t simuHapticValue = 0; + +// Required stubs — called by firmware but no-ops in simulator +void hapticOn(uint32_t pwmPercent) { (void)pwmPercent; } +void hapticOff() {} + +// WASM export — polled by host every 50ms to detect haptic events +uint32_t WASM_EXPORT(simuGetHaptic)() { + return simuHapticValue; +} int g_snapshot_idx = 0; @@ -361,8 +375,6 @@ void boardOff() { } -void hapticOff() {} - #if defined(HAS_HARDWARE_OPTIONS) HardwareOptions hardwareOptions; #endif diff --git a/radio/src/targets/simu/simulib.h b/radio/src/targets/simu/simulib.h index ccebd0a23..b665dd0b5 100644 --- a/radio/src/targets/simu/simulib.h +++ b/radio/src/targets/simu/simulib.h @@ -95,6 +95,7 @@ uint8_t WASM_EXPORT(simuGetNumCustomSwitches)(); uint8_t WASM_EXPORT(simuGetCustomSwitchIndex)(uint8_t cfsIdx); bool WASM_EXPORT(simuGetCustomSwitchState)(uint8_t idx); uint32_t WASM_EXPORT(simuGetCustomSwitchColor)(uint8_t idx); +uint32_t WASM_EXPORT(simuGetHaptic)(); // Value-based trim setting: idx is mapped through inputMappingConvertMode. void WASM_EXPORT(simuSetTrimValue)(uint8_t idx, int32_t value); @@ -185,7 +186,7 @@ void WASM_IMPORT(simuAuxSerialStart)(uint8_t port_nr, uint32_t baudrate, void WASM_IMPORT(simuAuxSerialStop)(uint8_t port_nr); void WASM_IMPORT(simuAuxSerialSetBaudrate)(uint8_t port_nr, uint32_t baudrate); void WASM_IMPORT(simuAuxSerialSendBuffer)(uint8_t port_nr, const uint8_t* data, - uint32_t len); + uint32_t len); // -- Internal (not exported) -- void simuMain(); diff --git a/radio/src/targets/taranis/CMakeLists.txt b/radio/src/targets/taranis/CMakeLists.txt index 313350b8a..7cfd625ce 100644 --- a/radio/src/targets/taranis/CMakeLists.txt +++ b/radio/src/targets/taranis/CMakeLists.txt @@ -308,6 +308,7 @@ elseif(PCB STREQUAL X7) set(AUX_SERIAL ON) endif() elseif(PCBREV STREQUAL POCKET) + set(HAPTIC YES) set(DEFAULT_INTERNAL_MODULE CROSSFIRE CACHE STRING "Default internal module") set(INTERNAL_MODULE_SERIAL YES) set(MODULE_SIZE_SML YES)