diff --git a/.gitignore b/.gitignore index 71ff82340..c5576bb61 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ target_wrapper.* # QtCreator *.autosave +.qtcreator/ # QtCreator Qml *.qmlproject.user diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..2be88860e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,1079 @@ +cmake_minimum_required(VERSION 3.16) + +# --------------------------------------------------------------------------- +# Version & Git commit +# --------------------------------------------------------------------------- +set(VT_VERSION "7.00") +set(VT_INTRO_VERSION "1") +set(VT_CONFIG_VERSION "4") +# Set to 0 for stable releases, test version number for development builds +set(VT_IS_TEST_VERSION "2") + +execute_process( + COMMAND git rev-parse --short=8 HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE VT_GIT_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET +) +if(NOT VT_GIT_COMMIT) + set(VT_GIT_COMMIT "00000000") +endif() + +# Android version codes +set(VT_ANDROID_VERSION_ARMV7 190) +set(VT_ANDROID_VERSION_ARM64 191) +set(VT_ANDROID_VERSION_X86 192) + +project(vesc_tool VERSION ${VT_VERSION} LANGUAGES C CXX) + +# --------------------------------------------------------------------------- +# C++ standard +# --------------------------------------------------------------------------- +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# --------------------------------------------------------------------------- +# Qt6 – find all potentially-needed components up-front +# --------------------------------------------------------------------------- +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +# Core set of Qt modules always required +set(QT_COMPONENTS + Core + Gui + Widgets + Network + Quick + QuickControls2 + QuickWidgets + Svg + PrintSupport + OpenGL + OpenGLWidgets + Core5Compat + TaskTree + Graphs + Quick3D +) + +# --------------------------------------------------------------------------- +# User-toggleable options (mirror the old qmake CONFIG / DEFINES) +# --------------------------------------------------------------------------- +option(HAS_BLUETOOTH "Enable Bluetooth support" ON) +option(HAS_SERIALPORT "Enable serial port support" ON) +option(HAS_GAMEPAD "Enable gamepad support" ON) +option(HAS_CANBUS "Enable CAN bus (serialbus)" OFF) +option(HAS_POS "Enable positioning support" ON) +option(BUILD_MOBILE "Build the mobile (QML) GUI" OFF) +option(EXCLUDE_FW "Exclude built-in firmwares" ON) + +# Build tier (pick one; default = neutral) +set(BUILD_TIER "neutral" CACHE STRING "Build tier: original, platinum, gold, silver, bronze, free, neutral") +set_property(CACHE BUILD_TIER PROPERTY STRINGS original platinum gold silver bronze free neutral) + +# Platform-specific defaults +if(WIN32) + set(HAS_POS OFF) +endif() +if(ANDROID) + set(HAS_SERIALPORT OFF) + # HAS_GAMEPAD stays ON via QtGamepadLegacy +endif() +if(IOS) + set(HAS_SERIALPORT OFF) + set(BUILD_MOBILE ON) +endif() + +# Append optional Qt components +if(HAS_SERIALPORT) + list(APPEND QT_COMPONENTS SerialPort) +endif() +if(HAS_CANBUS) + list(APPEND QT_COMPONENTS SerialBus) +endif() +if(HAS_BLUETOOTH) + list(APPEND QT_COMPONENTS Bluetooth) +endif() +if(HAS_POS) + list(APPEND QT_COMPONENTS Positioning PositioningQuick Location) +endif() +find_package(Qt6 REQUIRED COMPONENTS ${QT_COMPONENTS}) +# We need GuiPrivate for some internals +find_package(Qt6 REQUIRED COMPONENTS GuiPrivate) + +# Qt6 dropped QtGamepad – use QtGamepadLegacy (pumphaus/qtgamepadlegacy). +# Build & install it into your Qt prefix first, then it becomes available +# as the "GamepadLegacy" component. +if(HAS_GAMEPAD) + find_package(Qt6 QUIET COMPONENTS GamepadLegacy) + if(NOT TARGET Qt6::GamepadLegacy) + message(WARNING + "Qt6::GamepadLegacy not found – gamepad support disabled.\n" + " To enable it, build & install https://github.com/pumphaus/qtgamepadlegacy\n" + " into your Qt prefix (${Qt6_DIR}/../../..) and re-run CMake.") + set(HAS_GAMEPAD OFF) + else() + message(STATUS "Found Qt6::GamepadLegacy – gamepad support enabled.") + endif() +endif() + +# --------------------------------------------------------------------------- +# Compile definitions shared across the whole target +# --------------------------------------------------------------------------- +set(VT_DEFINITIONS + VT_VERSION=${VT_VERSION} + VT_INTRO_VERSION=${VT_INTRO_VERSION} + VT_CONFIG_VERSION=${VT_CONFIG_VERSION} + VT_IS_TEST_VERSION=${VT_IS_TEST_VERSION} + VT_GIT_COMMIT=${VT_GIT_COMMIT} +) + +if(HAS_BLUETOOTH) + list(APPEND VT_DEFINITIONS HAS_BLUETOOTH) +endif() +if(HAS_SERIALPORT) + list(APPEND VT_DEFINITIONS HAS_SERIALPORT) +endif() +if(HAS_GAMEPAD) + list(APPEND VT_DEFINITIONS HAS_GAMEPAD) +endif() +if(HAS_CANBUS) + list(APPEND VT_DEFINITIONS HAS_CANBUS) +endif() +if(HAS_POS) + list(APPEND VT_DEFINITIONS HAS_POS) +endif() +if(BUILD_MOBILE) + list(APPEND VT_DEFINITIONS USE_MOBILE) +endif() +if(WIN32) + list(APPEND VT_DEFINITIONS _USE_MATH_DEFINES) +endif() + +# Build-tier defines & resources +if(BUILD_TIER STREQUAL "original") + list(APPEND VT_DEFINITIONS VER_ORIGINAL) + set(TIER_QRC res_original.qrc) +elseif(BUILD_TIER STREQUAL "platinum") + list(APPEND VT_DEFINITIONS VER_PLATINUM) + set(TIER_QRC res_platinum.qrc) +elseif(BUILD_TIER STREQUAL "gold") + list(APPEND VT_DEFINITIONS VER_GOLD) + set(TIER_QRC res_gold.qrc) +elseif(BUILD_TIER STREQUAL "silver") + list(APPEND VT_DEFINITIONS VER_SILVER) + set(TIER_QRC res_silver.qrc) +elseif(BUILD_TIER STREQUAL "bronze") + list(APPEND VT_DEFINITIONS VER_BRONZE) + set(TIER_QRC res_bronze.qrc) +elseif(BUILD_TIER STREQUAL "free") + list(APPEND VT_DEFINITIONS VER_FREE) + set(TIER_QRC res_free.qrc) +else() + list(APPEND VT_DEFINITIONS VER_NEUTRAL) + set(TIER_QRC res_neutral.qrc) +endif() + +# --------------------------------------------------------------------------- +# Sources – root +# --------------------------------------------------------------------------- +set(ROOT_SOURCES + main.cpp + codeloader.cpp + mainwindow.cpp + boardsetupwindow.cpp + packet.cpp + preferences.cpp + tcphub.cpp + udpserversimple.cpp + vbytearray.cpp + commands.cpp + configparams.cpp + configparam.cpp + vescinterface.cpp + parametereditor.cpp + digitalfiltering.cpp + setupwizardapp.cpp + setupwizardmotor.cpp + startupwizard.cpp + utility.cpp + tcpserversimple.cpp + hexfile.cpp + motorcomparisonhelper.cpp + sampleddatahelper.cpp + rtdatastore.cpp +) + +set(ROOT_HEADERS + mainwindow.h + codeloader.h + boardsetupwindow.h + packet.h + preferences.h + tcphub.h + udpserversimple.h + vbytearray.h + commands.h + datatypes.h + configparams.h + configparam.h + vescinterface.h + vesctasks.h + parametereditor.h + digitalfiltering.h + setupwizardapp.h + setupwizardmotor.h + startupwizard.h + utility.h + tcpserversimple.h + hexfile.h + motorcomparisonhelper.h + sampleddatahelper.h + rtdatastore.h +) + +set(ROOT_FORMS + mainwindow.ui + boardsetupwindow.ui + parametereditor.ui + preferences.ui +) + +# Platform-specific root sources +if(UNIX AND NOT IOS) + list(APPEND ROOT_HEADERS systemcommandexecutor.h) +endif() +if(HAS_BLUETOOTH) + list(APPEND ROOT_SOURCES bleuart.cpp) + list(APPEND ROOT_HEADERS bleuart.h) +else() + list(APPEND ROOT_SOURCES bleuartdummy.cpp) + list(APPEND ROOT_HEADERS bleuartdummy.h) +endif() + +# --------------------------------------------------------------------------- +# Sources – pages/ +# --------------------------------------------------------------------------- +set(PAGES_SOURCES + pages/pageapppas.cpp + pages/pagebms.cpp + pages/pagecananalyzer.cpp + pages/pageconnection.cpp + pages/pagecustomconfig.cpp + pages/pagedisplaytool.cpp + pages/pageespprog.cpp + pages/pagelisp.cpp + pages/pagemotor.cpp + pages/pagedebugprint.cpp + pages/pagebldc.cpp + pages/pageappgeneral.cpp + pages/pagedc.cpp + pages/pagefoc.cpp + pages/pagecontrollers.cpp + pages/pageappppm.cpp + pages/pageappadc.cpp + pages/pageappuart.cpp + pages/pageappnunchuk.cpp + pages/pageappnrf.cpp + pages/pagemotorcomparison.cpp + pages/pagescripting.cpp + pages/pageterminal.cpp + pages/pagefirmware.cpp + pages/pagertdata.cpp + pages/pagesampleddata.cpp + pages/pagevescpackage.cpp + pages/pagewelcome.cpp + pages/pagemotorsettings.cpp + pages/pageappsettings.cpp + pages/pagedataanalysis.cpp + pages/pagemotorinfo.cpp + pages/pagesetupcalculators.cpp + pages/pagegpd.cpp + pages/pageexperiments.cpp + pages/pageimu.cpp + pages/pageswdprog.cpp + pages/pageappimu.cpp + pages/pageloganalysis.cpp +) + +set(PAGES_HEADERS + pages/pageapppas.h + pages/pagebms.h + pages/pagecananalyzer.h + pages/pageconnection.h + pages/pagecustomconfig.h + pages/pagedisplaytool.h + pages/pageespprog.h + pages/pagelisp.h + pages/pagemotor.h + pages/pagedebugprint.h + pages/pagebldc.h + pages/pageappgeneral.h + pages/pagedc.h + pages/pagefoc.h + pages/pagecontrollers.h + pages/pageappppm.h + pages/pageappadc.h + pages/pageappuart.h + pages/pageappnunchuk.h + pages/pageappnrf.h + pages/pagemotorcomparison.h + pages/pagescripting.h + pages/pageterminal.h + pages/pagefirmware.h + pages/pagertdata.h + pages/pagesampleddata.h + pages/pagevescpackage.h + pages/pagewelcome.h + pages/pagemotorsettings.h + pages/pageappsettings.h + pages/pagedataanalysis.h + pages/pagemotorinfo.h + pages/pagesetupcalculators.h + pages/pagegpd.h + pages/pageexperiments.h + pages/pageimu.h + pages/pageswdprog.h + pages/pageappimu.h + pages/pageloganalysis.h +) + +set(PAGES_FORMS + pages/pageapppas.ui + pages/pagebms.ui + pages/pagecananalyzer.ui + pages/pageconnection.ui + pages/pagecustomconfig.ui + pages/pagedisplaytool.ui + pages/pageespprog.ui + pages/pagelisp.ui + pages/pagemotor.ui + pages/pagedebugprint.ui + pages/pagebldc.ui + pages/pageappgeneral.ui + pages/pagedc.ui + pages/pagefoc.ui + pages/pagecontrollers.ui + pages/pageappppm.ui + pages/pageappadc.ui + pages/pageappuart.ui + pages/pageappnunchuk.ui + pages/pageappnrf.ui + pages/pagemotorcomparison.ui + pages/pagescripting.ui + pages/pageterminal.ui + pages/pagefirmware.ui + pages/pagertdata.ui + pages/pagesampleddata.ui + pages/pagevescpackage.ui + pages/pagewelcome.ui + pages/pagemotorsettings.ui + pages/pageappsettings.ui + pages/pagedataanalysis.ui + pages/pagemotorinfo.ui + pages/pagesetupcalculators.ui + pages/pagegpd.ui + pages/pageexperiments.ui + pages/pageimu.ui + pages/pageswdprog.ui + pages/pageappimu.ui + pages/pageloganalysis.ui +) + +# --------------------------------------------------------------------------- +# Sources – widgets/ +# --------------------------------------------------------------------------- +set(WIDGETS_SOURCES + widgets/batttempplot.cpp + widgets/canlistitem.cpp + widgets/experimentplot.cpp + widgets/parameditbitfield.cpp + widgets/parameditbool.cpp + widgets/parameditdouble.cpp + widgets/parameditenum.cpp + widgets/parameditint.cpp + widgets/displaybar.cpp + widgets/displaypercentage.cpp + widgets/helpdialog.cpp + widgets/mrichtextedit.cpp + widgets/mtextedit.cpp + widgets/pagelistitem.cpp + widgets/paramtable.cpp + widgets/qcustomplot.cpp + widgets/detectbldc.cpp + widgets/batterycalculator.cpp + widgets/detectfoc.cpp + widgets/detectfocencoder.cpp + widgets/detectfochall.cpp + widgets/ppmmap.cpp + widgets/adcmap.cpp + widgets/rtdatatext.cpp + widgets/nrfpair.cpp + widgets/scripteditor.cpp + widgets/vtextbrowser.cpp + widgets/imagewidget.cpp + widgets/parameditstring.cpp + widgets/paramdialog.cpp + widgets/aspectimglabel.cpp + widgets/historylineedit.cpp + widgets/detectallfocdialog.cpp + widgets/dirsetup.cpp + widgets/vesc3dview.cpp + widgets/superslider.cpp +) + +set(WIDGETS_HEADERS + widgets/batttempplot.h + widgets/canlistitem.h + widgets/experimentplot.h + widgets/parameditbitfield.h + widgets/parameditbool.h + widgets/parameditdouble.h + widgets/parameditenum.h + widgets/parameditint.h + widgets/displaybar.h + widgets/displaypercentage.h + widgets/helpdialog.h + widgets/mrichtextedit.h + widgets/mtextedit.h + widgets/pagelistitem.h + widgets/paramtable.h + widgets/qcustomplot.h + widgets/detectbldc.h + widgets/batterycalculator.h + widgets/detectfoc.h + widgets/detectfocencoder.h + widgets/detectfochall.h + widgets/ppmmap.h + widgets/adcmap.h + widgets/rtdatatext.h + widgets/nrfpair.h + widgets/scripteditor.h + widgets/vtextbrowser.h + widgets/imagewidget.h + widgets/parameditstring.h + widgets/paramdialog.h + widgets/aspectimglabel.h + widgets/historylineedit.h + widgets/detectallfocdialog.h + widgets/dirsetup.h + widgets/vesc3dview.h + widgets/superslider.h +) + +set(WIDGETS_FORMS + widgets/experimentplot.ui + widgets/parameditbitfield.ui + widgets/parameditbool.ui + widgets/parameditdouble.ui + widgets/parameditenum.ui + widgets/parameditint.ui + widgets/mrichtextedit.ui + widgets/helpdialog.ui + widgets/detectbldc.ui + widgets/batterycalculator.ui + widgets/detectfoc.ui + widgets/detectfocencoder.ui + widgets/detectfochall.ui + widgets/ppmmap.ui + widgets/adcmap.ui + widgets/nrfpair.ui + widgets/parameditstring.ui + widgets/paramdialog.ui + widgets/detectallfocdialog.ui + widgets/dirsetup.ui + widgets/scripteditor.ui +) + +# --------------------------------------------------------------------------- +# Sources – mobile/ +# --------------------------------------------------------------------------- +set(MOBILE_SOURCES + mobile/logreader.cpp + mobile/logwriter.cpp + mobile/qmlui.cpp + mobile/fwhelper.cpp +) + +set(MOBILE_HEADERS + mobile/logreader.h + mobile/logwriter.h + mobile/qmlui.h + mobile/fwhelper.h +) + +# --------------------------------------------------------------------------- +# Sources – map/ +# --------------------------------------------------------------------------- +set(MAP_SOURCES + map/carinfo.cpp + map/copterinfo.cpp + map/locpoint.cpp + map/mapwidget.cpp + map/osmclient.cpp + map/osmtile.cpp + map/perspectivepixmap.cpp +) + +set(MAP_HEADERS + map/carinfo.h + map/copterinfo.h + map/locpoint.h + map/mapwidget.h + map/osmclient.h + map/osmtile.h + map/perspectivepixmap.h +) + +# --------------------------------------------------------------------------- +# Sources – lzokay/ +# --------------------------------------------------------------------------- +set(LZOKAY_SOURCES lzokay/lzokay.cpp) +set(LZOKAY_HEADERS lzokay/lzokay.hpp) + +# --------------------------------------------------------------------------- +# Sources – heatshrink/ (mixed C and C++) +# --------------------------------------------------------------------------- +set(HEATSHRINK_SOURCES + heatshrink/heatshrink_decoder.c + heatshrink/heatshrink_encoder.c + heatshrink/heatshrinkif.cpp +) + +set(HEATSHRINK_HEADERS + heatshrink/heatshrink_common.h + heatshrink/heatshrink_config.h + heatshrink/heatshrink_decoder.h + heatshrink/heatshrink_encoder.h + heatshrink/heatshrinkif.h +) + +# --------------------------------------------------------------------------- +# Sources – QCodeEditor/ +# --------------------------------------------------------------------------- +set(QCODEEDITOR_SOURCES + QCodeEditor/src/internal/LispHighlighter.cpp + QCodeEditor/src/internal/QCodeEditor.cpp + QCodeEditor/src/internal/QCXXHighlighter.cpp + QCodeEditor/src/internal/QFramedTextAttribute.cpp + QCodeEditor/src/internal/QGLSLCompleter.cpp + QCodeEditor/src/internal/QGLSLHighlighter.cpp + QCodeEditor/src/internal/QJSONHighlighter.cpp + QCodeEditor/src/internal/QLanguage.cpp + QCodeEditor/src/internal/QLineNumberArea.cpp + QCodeEditor/src/internal/QLispCompleter.cpp + QCodeEditor/src/internal/QLuaCompleter.cpp + QCodeEditor/src/internal/QLuaHighlighter.cpp + QCodeEditor/src/internal/QPythonCompleter.cpp + QCodeEditor/src/internal/QPythonHighlighter.cpp + QCodeEditor/src/internal/QStyleSyntaxHighlighter.cpp + QCodeEditor/src/internal/QSyntaxStyle.cpp + QCodeEditor/src/internal/QXMLHighlighter.cpp + QCodeEditor/src/internal/QmlHighlighter.cpp + QCodeEditor/src/internal/QVescCompleter.cpp +) + +set(QCODEEDITOR_HEADERS + QCodeEditor/include/internal/LispHighlighter.hpp + QCodeEditor/include/internal/QCodeEditor.hpp + QCodeEditor/include/internal/QCXXHighlighter.hpp + QCodeEditor/include/internal/QFramedTextAttribute.hpp + QCodeEditor/include/internal/QGLSLCompleter.hpp + QCodeEditor/include/internal/QGLSLHighlighter.hpp + QCodeEditor/include/internal/QHighlightBlockRule.hpp + QCodeEditor/include/internal/QHighlightRule.hpp + QCodeEditor/include/internal/QJSONHighlighter.hpp + QCodeEditor/include/internal/QLanguage.hpp + QCodeEditor/include/internal/QLineNumberArea.hpp + QCodeEditor/include/internal/QLispCompleter.hpp + QCodeEditor/include/internal/QLuaCompleter.hpp + QCodeEditor/include/internal/QLuaHighlighter.hpp + QCodeEditor/include/internal/QPythonCompleter.hpp + QCodeEditor/include/internal/QPythonHighlighter.hpp + QCodeEditor/include/internal/QStyleSyntaxHighlighter.hpp + QCodeEditor/include/internal/QSyntaxStyle.hpp + QCodeEditor/include/internal/QXMLHighlighter.hpp + QCodeEditor/include/internal/QmlHighlighter.hpp + QCodeEditor/include/internal/QVescCompleter.hpp +) + +# --------------------------------------------------------------------------- +# Sources – esp32/ (mixed C and C++) +# --------------------------------------------------------------------------- +set(ESP32_SOURCES + esp32/esp32flash.cpp + esp32/esp_loader.c + esp32/esp_targets.c + esp32/md5_hash.c + esp32/serial_comm.c +) + +set(ESP32_HEADERS + esp32/esp32flash.h + esp32/esp_loader.h + esp32/esp_targets.h + esp32/md5_hash.h + esp32/serial_comm.h + esp32/serial_comm_prv.h + esp32/serial_io.h +) + +# --------------------------------------------------------------------------- +# Sources – display_tool/ +# --------------------------------------------------------------------------- +set(DISPLAY_TOOL_SOURCES + display_tool/dispeditor.cpp + display_tool/displayedit.cpp + display_tool/imagewidgetdisp.cpp +) + +set(DISPLAY_TOOL_HEADERS + display_tool/dispeditor.h + display_tool/displayedit.h + display_tool/imagewidgetdisp.h +) + +set(DISPLAY_TOOL_FORMS + display_tool/dispeditor.ui +) + +# --------------------------------------------------------------------------- +# Sources – qmarkdowntextedit/ +# --------------------------------------------------------------------------- +set(QMARKDOWNTEXTEDIT_SOURCES + qmarkdowntextedit/markdownhighlighter.cpp + qmarkdowntextedit/qmarkdowntextedit.cpp + qmarkdowntextedit/qownlanguagedata.cpp + qmarkdowntextedit/qplaintexteditsearchwidget.cpp +) + +set(QMARKDOWNTEXTEDIT_HEADERS + qmarkdowntextedit/linenumberarea.h + qmarkdowntextedit/markdownhighlighter.h + qmarkdowntextedit/qmarkdowntextedit.h + qmarkdowntextedit/qownlanguagedata.h + qmarkdowntextedit/qplaintexteditsearchwidget.h +) + +set(QMARKDOWNTEXTEDIT_FORMS + qmarkdowntextedit/qplaintexteditsearchwidget.ui +) + +# --------------------------------------------------------------------------- +# Sources – maddy/ (header-only markdown parser) +# --------------------------------------------------------------------------- +set(MADDY_HEADERS + maddy/blockparser.h + maddy/breaklineparser.h + maddy/checklistparser.h + maddy/codeblockparser.h + maddy/emphasizedparser.h + maddy/headlineparser.h + maddy/horizontallineparser.h + maddy/htmlparser.h + maddy/imageparser.h + maddy/inlinecodeparser.h + maddy/italicparser.h + maddy/latexblockparser.h + maddy/orderedlistparser.h + maddy/paragraphparser.h + maddy/parserconfig.h + maddy/parser.h + maddy/quoteparser.h + maddy/strikethroughparser.h + maddy/strongparser.h + maddy/tableparser.h + maddy/unorderedlistparser.h + maddy/lineparser.h + maddy/linkparser.h +) + +# --------------------------------------------------------------------------- +# Sources – minimp3/ +# --------------------------------------------------------------------------- +set(MINIMP3_SOURCES minimp3/qminimp3.cpp) +set(MINIMP3_HEADERS + minimp3/minimp3.h + minimp3/minimp3_ex.h + minimp3/qminimp3.h +) + +# --------------------------------------------------------------------------- +# Sources – iOS-specific +# --------------------------------------------------------------------------- +if(IOS) + set(IOS_SOURCES ios/src/setIosParameters.mm) + set(IOS_HEADERS ios/src/setIosParameters.h) +else() + set(IOS_SOURCES) + set(IOS_HEADERS) +endif() + +# --------------------------------------------------------------------------- +# Resource files (.qrc) +# --------------------------------------------------------------------------- +set(RESOURCE_FILES + res.qrc + res_custom_module.qrc + res_lisp.qrc + res_qml.qrc + res/config/res_config.qrc + res_fw_bms.qrc + mobile/qml.qrc + qmarkdowntextedit/media.qrc + QCodeEditor/resources/qcodeeditor_resources.qrc + ${TIER_QRC} +) + +if(NOT EXCLUDE_FW) + list(APPEND RESOURCE_FILES res/firmwares/res_fw.qrc) +endif() + +# --------------------------------------------------------------------------- +# Collect all sources +# --------------------------------------------------------------------------- +set(ALL_SOURCES + ${ROOT_SOURCES} + ${PAGES_SOURCES} + ${WIDGETS_SOURCES} + ${MOBILE_SOURCES} + ${MAP_SOURCES} + ${LZOKAY_SOURCES} + ${HEATSHRINK_SOURCES} + ${QCODEEDITOR_SOURCES} + ${ESP32_SOURCES} + ${DISPLAY_TOOL_SOURCES} + ${QMARKDOWNTEXTEDIT_SOURCES} + ${MINIMP3_SOURCES} + ${IOS_SOURCES} +) + +set(ALL_HEADERS + ${ROOT_HEADERS} + ${PAGES_HEADERS} + ${WIDGETS_HEADERS} + ${MOBILE_HEADERS} + ${MAP_HEADERS} + ${LZOKAY_HEADERS} + ${HEATSHRINK_HEADERS} + ${QCODEEDITOR_HEADERS} + ${ESP32_HEADERS} + ${DISPLAY_TOOL_HEADERS} + ${QMARKDOWNTEXTEDIT_HEADERS} + ${MADDY_HEADERS} + ${MINIMP3_HEADERS} + ${IOS_HEADERS} +) + +set(ALL_FORMS + ${ROOT_FORMS} + ${PAGES_FORMS} + ${WIDGETS_FORMS} + ${DISPLAY_TOOL_FORMS} + ${QMARKDOWNTEXTEDIT_FORMS} +) + +# --------------------------------------------------------------------------- +# Target name (mirrors old qmake logic) +# --------------------------------------------------------------------------- +if(IOS OR APPLE) + set(APP_TARGET "VESC Tool") +elseif(ANDROID) + set(APP_TARGET "vesc_tool") +else() + set(APP_TARGET "vesc_tool_${VT_VERSION}") +endif() + +# --------------------------------------------------------------------------- +# Create the executable +# --------------------------------------------------------------------------- +qt_add_executable(${APP_TARGET} + ${ALL_SOURCES} + ${ALL_HEADERS} + ${ALL_FORMS} + ${RESOURCE_FILES} +) + +# --------------------------------------------------------------------------- +# QML module – exposes all QML_ELEMENT-annotated C++ types under "Vedder.vesc" +# This replaces the legacy qmlRegisterType() calls. +# QML files import it with: import Vedder.vesc +# --------------------------------------------------------------------------- +qt_add_qml_module(${APP_TARGET} + URI Vedder.vesc + VERSION 1.0 + NO_RESOURCE_TARGET_PATH +) + +# QTP0004: Allow QML files in subdirectories without separate qmldir files. +qt_policy(SET QTP0004 OLD) + +# --------------------------------------------------------------------------- +# Desktop QML files – compiled via qmlcachegen and embedded as resources. +# Source paths are desktop/*.qml; PREFIX / keeps them at qrc:/desktop/*.qml. +# --------------------------------------------------------------------------- +qt_target_qml_sources(${APP_TARGET} + PREFIX / + QML_FILES + desktop/main.qml + desktop/ParamEditors.qml + desktop/ParamEditDouble.qml + desktop/ParamEditInt.qml + desktop/ParamEditEnum.qml + desktop/ParamEditBool.qml + desktop/ParamEditString.qml + desktop/ParamEditBitfield.qml + desktop/ParamEditSeparator.qml + desktop/Vesc3DView.qml + desktop/ParamGroupPage.qml + desktop/WelcomePage.qml + desktop/ConnectionPage.qml + desktop/FirmwarePage.qml + desktop/PackagesPage.qml + desktop/TerminalPage.qml + desktop/LispPage.qml + desktop/BmsPage.qml + desktop/RtDataPage.qml + desktop/SampledDataPage.qml + desktop/ExperimentPlotPage.qml + desktop/ImuPage.qml + desktop/MotorGeneralPage.qml + desktop/FocPage.qml + desktop/ControllersPage.qml + desktop/MotorInfoPage.qml + desktop/ExperimentsPage.qml + desktop/AppGeneralPage.qml + desktop/AppPpmPage.qml + desktop/AppAdcPage.qml + desktop/AppUartPage.qml + desktop/AppNunchukPage.qml + desktop/AppNrfPage.qml + desktop/AppPasPage.qml + desktop/AppImuPage.qml + desktop/ScriptingPage.qml + desktop/LogAnalysisPage.qml + desktop/MotorComparisonPage.qml + desktop/CanAnalyzerPage.qml + desktop/DisplayToolPage.qml + desktop/PlotLegend.qml +) + +# --------------------------------------------------------------------------- +# Compile definitions +# --------------------------------------------------------------------------- +target_compile_definitions(${APP_TARGET} PRIVATE ${VT_DEFINITIONS}) + +# --------------------------------------------------------------------------- +# Include directories +# --------------------------------------------------------------------------- +target_include_directories(${APP_TARGET} PRIVATE + ${CMAKE_SOURCE_DIR} + ${CMAKE_SOURCE_DIR}/pages + ${CMAKE_SOURCE_DIR}/widgets + ${CMAKE_SOURCE_DIR}/mobile + ${CMAKE_SOURCE_DIR}/map + ${CMAKE_SOURCE_DIR}/lzokay + ${CMAKE_SOURCE_DIR}/heatshrink + ${CMAKE_SOURCE_DIR}/QCodeEditor/include + ${CMAKE_SOURCE_DIR}/esp32 + ${CMAKE_SOURCE_DIR}/display_tool + ${CMAKE_SOURCE_DIR}/qmarkdowntextedit + ${CMAKE_SOURCE_DIR}/maddy + ${CMAKE_SOURCE_DIR}/minimp3 + # AUTOUIC needs to find generated ui_*.h files + ${CMAKE_CURRENT_BINARY_DIR} +) + +# --------------------------------------------------------------------------- +# Link Qt libraries +# --------------------------------------------------------------------------- +set(QT_LINK_LIBS + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Network + Qt6::Quick + Qt6::QuickControls2 + Qt6::QuickWidgets + Qt6::Svg + Qt6::GuiPrivate + Qt6::OpenGL + Qt6::OpenGLWidgets + Qt6::Core5Compat + Qt6::TaskTree + Qt6::Graphs + Qt6::Quick3D +) + +if(NOT IOS) + list(APPEND QT_LINK_LIBS Qt6::PrintSupport) +endif() + +if(HAS_SERIALPORT) + list(APPEND QT_LINK_LIBS Qt6::SerialPort) +endif() +if(HAS_CANBUS) + list(APPEND QT_LINK_LIBS Qt6::SerialBus) +endif() +if(HAS_BLUETOOTH) + list(APPEND QT_LINK_LIBS Qt6::Bluetooth) +endif() +if(HAS_POS) + list(APPEND QT_LINK_LIBS Qt6::Positioning Qt6::PositioningQuick Qt6::Location) +endif() +if(HAS_GAMEPAD) + list(APPEND QT_LINK_LIBS Qt6::GamepadLegacy) +endif() + +target_link_libraries(${APP_TARGET} PRIVATE ${QT_LINK_LIBS}) + +# --------------------------------------------------------------------------- +# Compiler flags +# --------------------------------------------------------------------------- +if(NOT MSVC) + target_compile_options(${APP_TARGET} PRIVATE + -Wno-deprecated-copy + ) +endif() + +if(IOS) + target_compile_options(${APP_TARGET} PRIVATE + $<$:-Wall> + ) +endif() + +# --------------------------------------------------------------------------- +# AUTOUIC search paths (so AUTOUIC can find .ui files in subdirectories) +# --------------------------------------------------------------------------- +set_target_properties(${APP_TARGET} PROPERTIES + AUTOUIC_SEARCH_PATHS + "${CMAKE_SOURCE_DIR};${CMAKE_SOURCE_DIR}/pages;${CMAKE_SOURCE_DIR}/widgets;${CMAKE_SOURCE_DIR}/display_tool;${CMAKE_SOURCE_DIR}/qmarkdowntextedit" +) + +# --------------------------------------------------------------------------- +# Android-specific configuration +# --------------------------------------------------------------------------- +if(ANDROID) + # Page size alignment (required for some Android ABIs) + target_link_options(${APP_TARGET} PRIVATE + -Wl,-z,max-page-size=16384 + -Wl,-z,common-page-size=16384 + ) + + # Determine android version code + if(ANDROID_ABI STREQUAL "x86") + set(VT_ANDROID_VERSION ${VT_ANDROID_VERSION_X86}) + elseif(ANDROID_ABI STREQUAL "arm64-v8a") + set(VT_ANDROID_VERSION ${VT_ANDROID_VERSION_ARM64}) + elseif(ANDROID_ABI STREQUAL "armeabi-v7a") + set(VT_ANDROID_VERSION ${VT_ANDROID_VERSION_ARMV7}) + else() + set(VT_ANDROID_VERSION ${VT_ANDROID_VERSION_X86}) + endif() + + # Generate AndroidManifest.xml from template + configure_file( + ${CMAKE_SOURCE_DIR}/android/AndroidManifest.xml.in + ${CMAKE_SOURCE_DIR}/android/AndroidManifest.xml + @ONLY + ) + + set_target_properties(${APP_TARGET} PROPERTIES + QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_SOURCE_DIR}/android" + ) +endif() + +# --------------------------------------------------------------------------- +# Linux-specific (static libstdc++ for portable binaries) +# --------------------------------------------------------------------------- +if(UNIX AND NOT APPLE AND NOT ANDROID) + option(STATIC_LIBCXX "Statically link libstdc++ and libgcc" OFF) + if(STATIC_LIBCXX) + target_link_options(${APP_TARGET} PRIVATE + -static-libstdc++ -static-libgcc + ) + endif() +endif() + +# --------------------------------------------------------------------------- +# macOS-specific +# --------------------------------------------------------------------------- +if(APPLE AND NOT IOS) + set_target_properties(${APP_TARGET} PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/macos/Info.plist" + MACOSX_BUNDLE_ICON_FILE "appIcon" + ) + + # Install the icon into the bundle + set(MACOS_ICON "${CMAKE_SOURCE_DIR}/macos/appIcon.icns") + set_source_files_properties(${MACOS_ICON} PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) + target_sources(${APP_TARGET} PRIVATE ${MACOS_ICON}) + + # Universal binary (x86_64 + arm64) + set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "macOS architectures") +endif() + +# --------------------------------------------------------------------------- +# iOS-specific +# --------------------------------------------------------------------------- +if(IOS) + set_target_properties(${APP_TARGET} PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/ios/Info.plist" + ) + + target_compile_definitions(${APP_TARGET} PRIVATE QT_NO_PRINTER) + + # Asset catalog + set_target_properties(${APP_TARGET} PROPERTIES + XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon" + ) + + # Launch screen storyboard + set(IOS_STORYBOARD "${CMAKE_SOURCE_DIR}/ios/MyLaunchScreen.storyboard") + if(EXISTS ${IOS_STORYBOARD}) + set_source_files_properties(${IOS_STORYBOARD} PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) + target_sources(${APP_TARGET} PRIVATE ${IOS_STORYBOARD}) + endif() + + # iTunes artwork + file(GLOB IOS_ARTWORK "${CMAKE_SOURCE_DIR}/ios/iTunesArtwork*") + if(IOS_ARTWORK) + set_source_files_properties(${IOS_ARTWORK} PROPERTIES + MACOSX_PACKAGE_LOCATION "Resources" + ) + target_sources(${APP_TARGET} PRIVATE ${IOS_ARTWORK}) + endif() + + # Target device family: 1=iPhone, 2=iPad, 1,2=Universal + set_target_properties(${APP_TARGET} PROPERTIES + XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2" + XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO" + ) +endif() + +# --------------------------------------------------------------------------- +# Windows-specific +# --------------------------------------------------------------------------- +if(WIN32) + set_target_properties(${APP_TARGET} PROPERTIES + WIN32_EXECUTABLE TRUE + ) +endif() + +# --------------------------------------------------------------------------- +# Install rules (basic) +# --------------------------------------------------------------------------- +include(GNUInstallDirs) +install(TARGETS ${APP_TARGET} + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..6c71a2598 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,80 @@ +{ + "version": 6, + "cmakeMinimumRequired": { + "major": 3, + "minor": 16, + "patch": 0 + }, + "configurePresets": [ + { + "name": "default", + "displayName": "Default (Debug)", + "binaryDir": "${sourceDir}/build/default", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release-linux", + "displayName": "Release Linux", + "binaryDir": "${sourceDir}/build/lin", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "STATIC_LIBCXX": "ON" + } + }, + { + "name": "release-windows", + "displayName": "Release Windows", + "binaryDir": "${sourceDir}/build/win", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "release-macos", + "displayName": "Release macOS", + "binaryDir": "${sourceDir}/build/macos", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64" + } + }, + { + "name": "release-android-arm64", + "displayName": "Release Android arm64-v8a", + "binaryDir": "${sourceDir}/build/android-arm64", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "ANDROID_ABI": "arm64-v8a" + } + }, + { + "name": "release-android-armv7", + "displayName": "Release Android armeabi-v7a", + "binaryDir": "${sourceDir}/build/android-armv7", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "ANDROID_ABI": "armeabi-v7a" + } + } + ], + "buildPresets": [ + { + "name": "default", + "configurePreset": "default" + }, + { + "name": "release-linux", + "configurePreset": "release-linux" + }, + { + "name": "release-windows", + "configurePreset": "release-windows" + }, + { + "name": "release-macos", + "configurePreset": "release-macos" + } + ] +} diff --git a/QCodeEditor/src/internal/QCodeEditor.cpp b/QCodeEditor/src/internal/QCodeEditor.cpp index f529938cf..5a6abf42e 100644 --- a/QCodeEditor/src/internal/QCodeEditor.cpp +++ b/QCodeEditor/src/internal/QCodeEditor.cpp @@ -829,7 +829,7 @@ void QCodeEditor::keyPressEvent(QKeyEvent* e) { // Shortcut for moving line to left if (m_replaceTab && e->key() == Qt::Key_Backtab) { - indentationLevel = std::min(indentationLevel, m_tabReplace.size()); + indentationLevel = std::min(indentationLevel, static_cast(m_tabReplace.size())); auto cursor = textCursor(); diff --git a/application/template/main.qml b/application/template/main.qml index f1ebac74d..3843dd331 100644 --- a/application/template/main.qml +++ b/application/template/main.qml @@ -15,9 +15,9 @@ along with this program. If not, see . */ -import QtQuick 2.7 -import QtQuick.Controls 2.0 -import QtQuick.Layouts 1.3 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import Vedder.vesc.vescinterface 1.0 import Vedder.vesc.commands 1.0 @@ -119,7 +119,7 @@ ApplicationWindow { Connections { target: mCommands - onValuesReceived: { + function onValuesReceived(values, mask) { valText.text = "Battery : " + parseFloat(values.v_in).toFixed(2) + " V\n" + "I Battery : " + parseFloat(values.current_in).toFixed(2) + " A\n" + diff --git a/bleuart.cpp b/bleuart.cpp index 77514b402..b5909b8c3 100644 --- a/bleuart.cpp +++ b/bleuart.cpp @@ -73,28 +73,29 @@ void BleUart::startConnect(QString addr) // a controller using a devices address is not supported on macOS or iOS. QBluetoothDeviceInfo deviceInfo = QBluetoothDeviceInfo(); deviceInfo.setDeviceUuid(QBluetoothUuid(addr)); - mControl = new QLowEnergyController(deviceInfo); + mControl = QLowEnergyController::createCentral(deviceInfo, this); #else - mControl = new QLowEnergyController(QBluetoothAddress(addr)); + QBluetoothDeviceInfo deviceInfo(QBluetoothAddress(addr), QString(), 0); + mControl = QLowEnergyController::createCentral(deviceInfo, this); #endif mControl->setRemoteAddressType(QLowEnergyController::RandomAddress); - connect(mControl, SIGNAL(serviceDiscovered(QBluetoothUuid)), - this, SLOT(serviceDiscovered(QBluetoothUuid))); - connect(mControl, SIGNAL(discoveryFinished()), - this, SLOT(serviceScanDone())); - connect(mControl, SIGNAL(error(QLowEnergyController::Error)), - this, SLOT(controllerError(QLowEnergyController::Error))); - connect(mControl, SIGNAL(connected()), - this, SLOT(deviceConnected())); - connect(mControl, SIGNAL(disconnected()), - this, SLOT(deviceDisconnected())); - connect(mControl, SIGNAL(stateChanged(QLowEnergyController::ControllerState)), - this, SLOT(controlStateChanged(QLowEnergyController::ControllerState))); - connect(mControl, SIGNAL(connectionUpdated(QLowEnergyConnectionParameters)), - this, SLOT(connectionUpdated(QLowEnergyConnectionParameters))); + connect(mControl, &QLowEnergyController::serviceDiscovered, + this, &BleUart::serviceDiscovered); + connect(mControl, &QLowEnergyController::discoveryFinished, + this, &BleUart::serviceScanDone); + connect(mControl, &QLowEnergyController::errorOccurred, + this, &BleUart::controllerError); + connect(mControl, &QLowEnergyController::connected, + this, &BleUart::deviceConnected); + connect(mControl, &QLowEnergyController::disconnected, + this, &BleUart::deviceDisconnected); + connect(mControl, &QLowEnergyController::stateChanged, + this, &BleUart::controlStateChanged); + connect(mControl, &QLowEnergyController::connectionUpdated, + this, &BleUart::connectionUpdated); mControl->connectToDevice(); mConnectTimeoutTimer.start(10000); @@ -237,14 +238,12 @@ void BleUart::serviceScanDone() qDebug() << "Connecting to BLE UART service"; mService = mControl->createServiceObject(QBluetoothUuid(QUuid(mServiceUuid)), this); - connect(mService, SIGNAL(stateChanged(QLowEnergyService::ServiceState)), - this, SLOT(serviceStateChanged(QLowEnergyService::ServiceState))); - connect(mService, SIGNAL(error(QLowEnergyService::ServiceError)), - this, SLOT(serviceError(QLowEnergyService::ServiceError))); - connect(mService, SIGNAL(characteristicChanged(QLowEnergyCharacteristic,QByteArray)), - this, SLOT(updateData(QLowEnergyCharacteristic,QByteArray))); - connect(mService, SIGNAL(descriptorWritten(QLowEnergyDescriptor,QByteArray)), - this, SLOT(confirmedDescriptorWrite(QLowEnergyDescriptor,QByteArray))); + connect(mService, &QLowEnergyService::stateChanged, + this, &BleUart::serviceStateChanged); + connect(mService, &QLowEnergyService::errorOccurred, + this, &BleUart::serviceError); + connect(mService, &QLowEnergyService::characteristicChanged, this, &BleUart::updateData); + connect(mService, &QLowEnergyService::descriptorWritten, this, &BleUart::confirmedDescriptorWrite); mService->discoverDetails(); } else { @@ -283,7 +282,7 @@ void BleUart::serviceStateChanged(QLowEnergyService::ServiceState s) emit unintentionalDisconnect(); break; } - case QLowEnergyService::ServiceDiscovered: { + case QLowEnergyService::RemoteServiceDiscovered: { //looking for the TX characteristic const QLowEnergyCharacteristic txChar = mService->characteristic( QBluetoothUuid(QUuid(mTxUuid))); @@ -304,7 +303,7 @@ void BleUart::serviceStateChanged(QLowEnergyService::ServiceState s) // Bluetooth LE spec Where a characteristic can be notified, a Client Characteristic Configuration descriptor // shall be included in that characteristic as required by the Bluetooth Core Specification // Tx notify is enabled - mNotificationDescTx = txChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); + mNotificationDescTx = txChar.descriptor(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration); if (mNotificationDescTx.isValid()) { // enable notification @@ -374,11 +373,11 @@ void BleUart::init() mDeviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); - connect(mDeviceDiscoveryAgent, SIGNAL(deviceDiscovered(const QBluetoothDeviceInfo&)), - this, SLOT(addDevice(const QBluetoothDeviceInfo&))); - connect(mDeviceDiscoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), - this, SLOT(deviceScanError(QBluetoothDeviceDiscoveryAgent::Error))); - connect(mDeviceDiscoveryAgent, SIGNAL(finished()), this, SLOT(scanFinished())); + connect(mDeviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, + this, &BleUart::addDevice); + connect(mDeviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, + this, &BleUart::deviceScanError); + connect(mDeviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BleUart::scanFinished); mInitDone = true; diff --git a/bleuart.h b/bleuart.h index 12f3aa838..28046b404 100644 --- a/bleuart.h +++ b/bleuart.h @@ -27,10 +27,12 @@ #include #include #include +#include class BleUart : public QObject { Q_OBJECT + QML_ELEMENT public: explicit BleUart(QObject *parent = nullptr); ~BleUart(); diff --git a/bleuartdummy.h b/bleuartdummy.h index b881d7022..0ea1799f2 100644 --- a/bleuartdummy.h +++ b/bleuartdummy.h @@ -22,10 +22,12 @@ #include #include +#include class BleUartDummy : public QObject { Q_OBJECT + QML_NAMED_ELEMENT(BleUart) public: explicit BleUartDummy(QObject *parent = nullptr); diff --git a/boardsetupwindow.cpp b/boardsetupwindow.cpp index 2cb969a42..8849e24c1 100644 --- a/boardsetupwindow.cpp +++ b/boardsetupwindow.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -35,18 +34,14 @@ BoardSetupWindow::BoardSetupWindow(QWidget *parent) : mKeyRight = false; - connect(mTimer, SIGNAL(timeout()), - this, SLOT(timerSlot())); - connect(mVesc, SIGNAL(statusMessage(QString,bool)), - this, SLOT(showStatusInfo(QString,bool))); - connect(mVesc, SIGNAL(messageDialog(QString,QString,bool,bool)), - this, SLOT(showMessageDialog(QString,QString,bool,bool))); - connect(mVesc->commands(), SIGNAL(pingCanRx(QVector,bool)), - this, SLOT(pingCanRx(QVector,bool))); - connect(mVesc, SIGNAL(fwUploadStatus(QString,double,bool)), - this, SLOT(fwUploadStatus(QString,double,bool))); - connect(mVesc->commands(), SIGNAL(valuesReceived(MC_VALUES,unsigned int)), - this, SLOT(valuesReceived(MC_VALUES,unsigned int))); + connect(mTimer, &QTimer::timeout, this, &BoardSetupWindow::timerSlot); + connect(mVesc, &VescInterface::statusMessage, this, &BoardSetupWindow::showStatusInfo); + connect(mVesc, &VescInterface::messageDialog, this, &BoardSetupWindow::showMessageDialog); + connect(mVesc->commands(), &Commands::pingCanRx, + this, &BoardSetupWindow::pingCanRx); + connect(mVesc, &VescInterface::fwUploadStatus, this, &BoardSetupWindow::fwUploadStatus); + connect(mVesc->commands(), &Commands::valuesReceived, + this, &BoardSetupWindow::valuesReceived); mTimer->start(20); @@ -357,7 +352,7 @@ bool BoardSetupWindow::trySerialConnect(){ mVesc->commands()->setSendCan(false); res = mVesc->connectSerial(ui->serialPortBox->currentData().toString(), 115200); if(res){ - Utility::waitSignal(mVesc, SIGNAL(fwRxChanged(bool, bool)), 5000); + Utility::waitSignal(mVesc, &VescInterface::fwRxChanged, 5000); } if(mVesc->isPortConnected()){ ui->usbConnectLabel->setStyleSheet("QLabel { background-color : lightGreen; color : black; }"); @@ -373,7 +368,7 @@ bool BoardSetupWindow::tryCANScan(){ bool res = false; if(mVesc->isPortConnected()){ mVesc->commands()->pingCan(); - Utility::waitSignal(mVesc->commands(), SIGNAL(pingCanRx(QVector, bool)), 10000); + Utility::waitSignal(mVesc->commands(), &Commands::pingCanRx, 10000); } if(CAN_Timeout){ res = false; @@ -384,7 +379,7 @@ bool BoardSetupWindow::tryCANScan(){ res = true; mVesc->commands()->setSendCan(false); mVesc->commands()->getAppConf(); - if(!Utility::waitSignal(mVesc->appConfig(), SIGNAL(updated()), 5000)){ + if(!Utility::waitSignal(mVesc->appConfig(), &ConfigParams::updated, 5000)){ ui->CANScanLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to read app config during CAN Scan."; return false; @@ -513,7 +508,7 @@ bool BoardSetupWindow::tryFOCCalibration(){ mVesc->commands()->setSendCan(false); mVesc->ignoreCanChange(true); mVesc->commands()->getAppConf(); - if(!Utility::waitSignal(mVesc->appConfig(), SIGNAL(updated()), 5000)){ + if(!Utility::waitSignal(mVesc->appConfig(), &ConfigParams::updated, 5000)){ ui->motorDetectionLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to read app config during motor setup routine."; return false; @@ -522,7 +517,7 @@ bool BoardSetupWindow::tryFOCCalibration(){ mVesc->appConfig()->updateParamEnum("app_to_use",0); // set to use no app Utility::sleepWithEventLoop(100); mVesc->commands()->setAppConf(); - if(!Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 2000)){ + if(!Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 2000)){ ui->appSetupLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to write app config during motor routine."; return false; @@ -542,12 +537,12 @@ bool BoardSetupWindow::tryFOCCalibration(){ ui->motorDetectionLabel->setText("Writing Default Configs"); mVesc->commands()->setSendCan(false); mVesc->commands()->setMcconf(false); - Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 2000); + Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 2000); for(int i = 0; i < CAN_IDs.size(); i++){ mVesc->commands()->setSendCan(true, CAN_IDs.at(i)); mVesc->commands()->setMcconf(); - Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 2000); + Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 2000); } mVesc->ignoreCanChange(false); mVesc->commands()->setSendCan(false); @@ -586,7 +581,7 @@ bool BoardSetupWindow::tryFOCCalibration(){ mVesc->commands()->setSendCan(true, CAN_IDs.at(i)); } mVesc->commands()->getMcconf(); - Utility::waitSignal(mVesc->mcConfig(), SIGNAL(updated()), 5000); + Utility::waitSignal(mVesc->mcConfig(), &ConfigParams::updated, 5000); if(mcConfigOutsideParamBounds("foc_motor_r",tolerance )){ ui->motorDetectionLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); @@ -652,7 +647,7 @@ bool BoardSetupWindow::tryMotorDirection(){ } mVesc->commands()->getValues(); - if(!Utility::waitSignal(mVesc->commands(), SIGNAL(valuesReceived(MC_VALUES, unsigned int)), 2000)){ + if(!Utility::waitSignal(mVesc->commands(), &Commands::valuesReceived, 2000)){ ui->motorDirectionLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to read tachometer value during motor direction routine."; return false; @@ -667,7 +662,7 @@ bool BoardSetupWindow::tryMotorDirection(){ msgBox->setText(directionStatus); msgBox->addButton(QMessageBox::Cancel); msgBox->setModal( true ); - msgBox->open(this, SLOT(msgBoxClosed(QAbstractButton*))); + msgBox->open(); mVesc->ignoreCanChange(true); while(!allHaveTurned){ directionStatus = tr("Spin each motor in the forward direction to continue.\n"); @@ -681,7 +676,7 @@ bool BoardSetupWindow::tryMotorDirection(){ } Utility::sleepWithEventLoop(10); mVesc->commands()->getValues(); - if(Utility::waitSignal(mVesc->commands(), SIGNAL(valuesReceived(MC_VALUES, unsigned int)), 100)){ + if(Utility::waitSignal(mVesc->commands(), &Commands::valuesReceived, 100)){ tach_end[i+1] = values_now.tachometer; } int tachDiff = tach_start[i+1] - tach_end[i+1]; @@ -724,7 +719,7 @@ bool BoardSetupWindow::tryMotorDirection(){ } Utility::sleepWithEventLoop(100); mVesc->commands()->getMcconf(); - if(!Utility::waitSignal(mVesc->mcConfig(), SIGNAL(updated()), 5000)){ + if(!Utility::waitSignal(mVesc->mcConfig(), &ConfigParams::updated, 5000)){ ui->motorDirectionLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to read config during motor direction routine."; return false; @@ -738,7 +733,7 @@ bool BoardSetupWindow::tryMotorDirection(){ mVesc->mcConfig()->updateParamDouble("l_in_current_max", motor_current_in_max); mVesc->commands()->setMcconf(false); - if(!Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 2000)){ + if(!Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 2000)){ ui->motorDirectionLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to write config during during motor direction routine."; return false; @@ -759,14 +754,14 @@ bool BoardSetupWindow::tryTestMotorParameters(){ if(!ui->appCheckBox->isChecked()){ mVesc->commands()->setSendCan(false); mVesc->commands()->getAppConf(); - if(!Utility::waitSignal(mVesc->appConfig(), SIGNAL(updated()), 5000)){ + if(!Utility::waitSignal(mVesc->appConfig(), &ConfigParams::updated, 5000)){ ui->motorDetectionLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to read app config after motor setup routine."; return false; } mVesc->appConfig()->updateParamEnum("app_to_use",app_enum_old); // set to use no app mVesc->commands()->setAppConf(); - if(!Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 2000)){ + if(!Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 2000)){ ui->appSetupLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to write app config after motor routine."; return false; @@ -783,7 +778,7 @@ bool BoardSetupWindow::tryApplySlaveAppSettings(){ int master_ID = 0; mVesc->commands()->setSendCan(false); mVesc->commands()->getAppConf(); - if(!Utility::waitSignal(mVesc->appConfig(), SIGNAL(updated()), 5000)){ + if(!Utility::waitSignal(mVesc->appConfig(), &ConfigParams::updated, 5000)){ ui->appSetupLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to read app config during slave setup routine."; return false; @@ -791,7 +786,7 @@ bool BoardSetupWindow::tryApplySlaveAppSettings(){ master_ID = mVesc->appConfig()->getParamInt("controller_id"); mVesc->appConfig()->updateParamEnum("app_to_use",0); // set to use uart mVesc->commands()->setAppConf(); - if(!Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 5000)){ + if(!Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 5000)){ ui->appSetupLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to write master config during slave app routine."; return false; @@ -833,7 +828,7 @@ bool BoardSetupWindow::tryApplySlaveAppSettings(){ mVesc->appConfig()->updateParamInt("controller_id", CAN_IDs.at(i)); Utility::sleepWithEventLoop(100); mVesc->commands()->setAppConf(); - if(!Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 2000)){ + if(!Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 2000)){ ui->appSetupLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to write config during slave app routine."; return false; @@ -849,7 +844,7 @@ bool BoardSetupWindow::tryApplyMasterAppSettings(){ int master_ID = 0; mVesc->commands()->setSendCan(false); mVesc->commands()->getAppConf(); - if(!Utility::waitSignal(mVesc->appConfig(), SIGNAL(updated()), 5000)){ + if(!Utility::waitSignal(mVesc->appConfig(), &ConfigParams::updated, 5000)){ ui->appSetupLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to read app config during slave setup routine."; return false; @@ -891,7 +886,7 @@ bool BoardSetupWindow::tryApplyMasterAppSettings(){ mVesc->commands()->setSendCan(false); mVesc->commands()->setAppConf(); - if(!Utility::waitSignal(mVesc->commands(), SIGNAL(ackReceived(QString)), 2000)){ + if(!Utility::waitSignal(mVesc->commands(), &Commands::ackReceived, 2000)){ ui->appSetupLabel->setStyleSheet("QLabel { background-color : red; color : black; }"); testResultMsg = "Failed to write config during master app routine."; return false; @@ -974,7 +969,7 @@ void BoardSetupWindow::on_serialRefreshButton_clicked() if (mVesc) { ui->serialPortBox->clear(); auto ports = mVesc->listSerialPorts(); - foreach(auto &info, ports) { + for (auto &info : ports) { auto port = info.value(); ui->serialPortBox->addItem(port.name, port.systemPath); } @@ -1054,7 +1049,7 @@ void BoardSetupWindow::loadAppConfXML(QString path){ switch(mAppConfig_Target->getParamEnum("app_to_use")){ case 4: //ppm and uart ui->appTab->addParamRow(mAppConfig_Target, "app_uart_baudrate"); - [[clang::fallthrough]]; + [[fallthrough]]; case 1: //ppm ui->appTab->addParamRow(mAppConfig_Target, "app_ppm_conf.ctrl_type"); ui->appTab->addParamRow(mAppConfig_Target, "app_ppm_conf.pulse_start"); @@ -1106,7 +1101,7 @@ void BoardSetupWindow::loadAppConfXML(QString path){ QString str; ui->appTab->addParamRow(mAppConfig_Target, "app_to_use"); - foreach(QObject *p, ui->appTab->children().at(0)->children()){ + for (auto *p : ui->appTab->children().at(0)->children()) { p->setProperty("enabled",false); } ui->appCheckBox->setCheckable(true); @@ -1158,7 +1153,7 @@ void BoardSetupWindow::loadMotorConfXML(QString path){ ui->motorTab->addParamRow(mMcConfig_Target, "l_battery_cut_end"); ui->motorConfigEdit->setText(path); - foreach(QObject *p, ui->motorTab->children().at(0)->children()){ + for (auto *p : ui->motorTab->children().at(0)->children()) { p->setProperty("enabled",false); } ui->motorDetectionCheckBox->setCheckable(true); diff --git a/codeloader.cpp b/codeloader.cpp index fd820aff1..cfcc98713 100644 --- a/codeloader.cpp +++ b/codeloader.cpp @@ -20,14 +20,13 @@ #include "codeloader.h" #include "qqmlcontext.h" #include "utility.h" -#include +#include "vesctasks.h" #include #include #include #include #include #include -#include #include #include #include @@ -73,21 +72,12 @@ bool CodeLoader::lispErase(int size) auto waitEraseRes = [this]() { int res = -10; - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(8000); - auto conn = connect(mVesc->commands(), &Commands::lispEraseCodeRx, - [&res,&loop](bool erRes) { - res = erRes ? 1 : -1; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(8000); + task.connectSignal(mVesc->commands(), &Commands::lispEraseCodeRx, + [&res](bool erRes) { res = erRes ? 1 : -1; }); + return SetupResult::Continue; + })}); return res; }; @@ -115,7 +105,7 @@ QString CodeLoader::reduceLispFile(QString fileData) QString res; auto lines = fileData.split("\n"); - foreach (auto line, lines) { + for (auto line : lines) { bool insideString = false; int indComment = -1; @@ -202,7 +192,7 @@ QByteArray CodeLoader::lispPackImports(QString codeStr, QString editorPath, bool auto lines = codeStr.split("\n"); int line_num = 0; - foreach (auto line, lines) { + for (const auto &line : lines) { line_num++; QString path; @@ -263,7 +253,7 @@ QByteArray CodeLoader::lispPackImports(QString codeStr, QString editorPath, bool files.append(qMakePair(tag, importData)); } else { bool found = false; - foreach (auto i, imports.second) { + for (const auto &i : imports.second) { if (i.first == pkgImportName) { auto importData = i.second; importData.append('\0'); // Pad with 0 in case it is a text file @@ -394,22 +384,12 @@ bool CodeLoader::lispUpload(VByteArray vb) auto waitWriteRes = [this]() { int res = -10; - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(1000); - auto conn = connect(mVesc->commands(), &Commands::lispWriteCodeRx, - [&res,&loop](bool erRes, quint32 offset) { - (void)offset; - res = erRes ? 1 : -1; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(1000); + task.connectSignal(mVesc->commands(), &Commands::lispWriteCodeRx, + [&res](bool erRes, quint32) { res = erRes ? 1 : -1; }); + return SetupResult::Continue; + })}); return res; }; @@ -495,22 +475,12 @@ bool CodeLoader::lispStream(VByteArray vb, qint8 mode) auto waitWriteRes = [this]() { int res = -10; - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(4000); - auto conn = connect(mVesc->commands(), &Commands::lispStreamCodeRx, - [&res,&loop](qint32 offset, qint16 result) { - (void)offset; - res = result; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(4000); + task.connectSignal(mVesc->commands(), &Commands::lispStreamCodeRx, + [&res](qint32, qint16 result) { res = result; }); + return SetupResult::Continue; + })}); return res; }; @@ -558,7 +528,7 @@ QString CodeLoader::lispRead(QWidget *parent, QString &lispPath) for (int j = 0;j < tries;j++) { mVesc->commands()->lispReadCode(size, offset); - res = Utility::waitSignal(mVesc->commands(), SIGNAL(lispReadCodeRx(int,int,QByteArray)), timeout); + res = Utility::waitSignal(mVesc->commands(), &Commands::lispReadCodeRx, timeout); if (res) { break; } @@ -596,7 +566,7 @@ QString CodeLoader::lispRead(QWidget *parent, QString &lispPath) QMessageBox::Yes | QMessageBox::No); QMap importPaths; - foreach (auto line, res.split('\n')) { + for (const auto &line : res.split('\n')) { QString path; QString tag; bool isInvalid; @@ -620,7 +590,7 @@ QString CodeLoader::lispRead(QWidget *parent, QString &lispPath) } } - foreach (auto i, unpacked.second) { + for (const auto &i : unpacked.second) { QString fileName = dirName + "/" + i.first + ".bin"; if (importPaths.contains(i.first)) { @@ -697,21 +667,12 @@ bool CodeLoader::qmlErase(int size) auto waitEraseRes = [this]() { int res = -10; - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(6000); - auto conn = connect(mVesc->commands(), &Commands::eraseQmluiResReceived, - [&res,&loop](bool erRes) { - res = erRes ? 1 : -1; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(6000); + task.connectSignal(mVesc->commands(), &Commands::eraseQmluiResReceived, + [&res](bool erRes) { res = erRes ? 1 : -1; }); + return SetupResult::Continue; + })}); return res; }; @@ -745,22 +706,12 @@ bool CodeLoader::qmlUpload(QByteArray script, bool isFullscreen) { auto waitWriteRes = [this]() { int res = -10; - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(1000); - auto conn = connect(mVesc->commands(), &Commands::writeQmluiResReceived, - [&res,&loop](bool erRes, quint32 offset) { - (void)offset; - res = erRes ? 1 : -1; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(1000); + task.connectSignal(mVesc->commands(), &Commands::writeQmluiResReceived, + [&res](bool erRes, quint32) { res = erRes ? 1 : -1; }); + return SetupResult::Continue; + })}); return res; }; @@ -1099,31 +1050,19 @@ bool CodeLoader::downloadPackageArchive() QFile file(path); if (file.open(QIODevice::WriteOnly)) { - QUrl url("http://home.vedder.se/vesc_pkg/vesc_pkg_all.rcc"); - QNetworkAccessManager manager; - QNetworkRequest request(url); - QNetworkReply *reply = manager.get(request); - - connect(reply, &QNetworkReply::downloadProgress, [&file, reply, this](qint64 bytesReceived, qint64 bytesTotal) { - emit downloadProgress(bytesReceived, bytesTotal); - file.write(reply->read(reply->size())); - }); - - QEventLoop loop; - connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); - loop.exec(); - - if (reply->error() == QNetworkReply::NoError) { - file.write(reply->readAll()); - res = true; - - // Remove image cache - // QString cacheLoc = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); - // QDir(cacheLoc + "/img/").removeRecursively(); - } - - reply->abort(); - reply->deleteLater(); + runTree(Group{NetworkReplyTaskItem([this, &file](NetworkReplyTask &task) { + task.setUrl(QUrl("http://home.vedder.se/vesc_pkg/vesc_pkg_all.rcc")); + task.setOutputDevice(&file); + task.setProgressCallback([this](qint64 bytesReceived, qint64 bytesTotal) { + emit downloadProgress(bytesReceived, bytesTotal); + }); + return SetupResult::Continue; + }, [&res](const NetworkReplyTask &task, DoneWith doneWith) { + if (doneWith == DoneWith::Success) { + res = true; + } + return DoneResult::Success; + })}); file.close(); diff --git a/codeloader.h b/codeloader.h index 918c9c8e2..9a4e8b8b5 100644 --- a/codeloader.h +++ b/codeloader.h @@ -22,13 +22,14 @@ #include #include -#include "qqmlengine.h" +#include #include "vescinterface.h" #include "datatypes.h" class CodeLoader : public QObject { Q_OBJECT + QML_ELEMENT public: explicit CodeLoader(QObject *parent = nullptr); diff --git a/commands.cpp b/commands.cpp index cb2f32916..f7b595af9 100755 --- a/commands.cpp +++ b/commands.cpp @@ -18,9 +18,9 @@ */ #include "commands.h" +#include "vesctasks.h" #include "qelapsedtimer.h" #include -#include Commands::Commands(QObject *parent) : QObject(parent) { @@ -57,7 +57,7 @@ Commands::Commands(QObject *parent) : QObject(parent) mFilePercentage = 0.0; mFileSpeed = 0.0; - connect(mTimer, SIGNAL(timeout()), this, SLOT(timerSlot())); + connect(mTimer, &QTimer::timeout, this, &Commands::timerSlot); } void Commands::setLimitedMode(bool is_limited) @@ -1619,7 +1619,7 @@ void Commands::sendCustomHwData(QByteArray data) emitData(vb); } -void Commands::setChukData(chuck_data &data) +void Commands::setChukData(const chuck_data &data) { VByteArray vb; vb.vbAppendInt8(COMM_SET_CHUCK_DATA); @@ -2426,22 +2426,17 @@ QVariantList Commands::fileBlockList(QString path) bool res = false; bool more = false; - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(1500); - auto conn = connect(this, &Commands::fileListRx, - [&res,&loop,&filesNow,&more](bool hasMore, QList files) { - filesNow.append(files); - res = true; - more = hasMore; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); + runTree(Group{SignalWaitTaskItem([this, &res, &filesNow, &more](SignalWaitTask &task) { + task.setTimeout(1500); + task.connectSignal(this, &Commands::fileListRx, + [&res, &filesNow, &more](bool hasMore, QList files) { + filesNow.append(files); + res = true; + more = hasMore; + }); + return SetupResult::Continue; + })}); - disconnect(conn); return qMakePair(res, more); }; @@ -2482,7 +2477,7 @@ QVariantList Commands::fileBlockList(QString path) }); QVariantList retVal; - foreach (auto f, files) { + for (const auto &f : files) { retVal.append(QVariant::fromValue(f)); } @@ -2494,28 +2489,22 @@ QByteArray Commands::fileBlockRead(QString path) mFileShouldCancel = false; auto waitRes = [this](QByteArray &dataNow, qint32 offsetNow) { - qint32 sizeRet = -1.0; - - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(1500); - auto conn = connect(this, &Commands::fileReadRx, - [&sizeRet,&loop,&dataNow,&offsetNow] - (qint32 offset, qint32 size, QByteArray data) { - if (offset == offsetNow) { - sizeRet = size; - dataNow.append(data); - } else { - qWarning() << "Wrong offset"; - } - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); + qint32 sizeRet = -1; + + runTree(Group{SignalWaitTaskItem([this, &sizeRet, &dataNow, &offsetNow](SignalWaitTask &task) { + task.setTimeout(1500); + task.connectSignal(this, &Commands::fileReadRx, + [&sizeRet, &dataNow, &offsetNow](qint32 offset, qint32 size, QByteArray data) { + if (offset == offsetNow) { + sizeRet = size; + dataNow.append(data); + } else { + qWarning() << "Wrong offset"; + } + }); + return SetupResult::Continue; + })}); - disconnect(conn); return sizeRet; }; @@ -2564,21 +2553,12 @@ bool Commands::fileBlockWrite(QString path, QByteArray data) auto waitRes = [this]() { bool res = false; - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(1500); - auto conn = connect(this, &Commands::fileWriteRx, - [&res,&loop] - (qint32 offset, bool ok) { - (void)offset; - res = ok; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(1500); + task.connectSignal(this, &Commands::fileWriteRx, + [&res](qint32, bool ok) { res = ok; }); + return SetupResult::Continue; + })}); return res; }; @@ -2632,19 +2612,12 @@ bool Commands::fileBlockMkdir(QString path) auto waitRes = [this]() { bool res = false; - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(1500); - auto conn = connect(this, &Commands::fileMkdirRx, - [&res,&loop](bool ok) { - res = ok; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(1500); + task.connectSignal(this, &Commands::fileMkdirRx, + [&res](bool ok) { res = ok; }); + return SetupResult::Continue; + })}); return res; }; @@ -2671,19 +2644,12 @@ bool Commands::fileBlockRemove(QString path) auto waitRes = [this]() { bool res = false; - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(1500); - auto conn = connect(this, &Commands::fileRemoveRx, - [&res,&loop](bool ok) { - res = ok; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); - disconnect(conn); + runTree(Group{SignalWaitTaskItem([this, &res](SignalWaitTask &task) { + task.setTimeout(1500); + task.connectSignal(this, &Commands::fileRemoveRx, + [&res](bool ok) { res = ok; }); + return SetupResult::Continue; + })}); return res; }; @@ -2790,21 +2756,16 @@ QByteArray Commands::bmReadMemWait(uint32_t addr, quint16 size, int timeoutMs) int res = -10; QByteArray resData; - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(timeoutMs); - auto conn = connect(this, &Commands::bmReadMemRes, [&res,&resData,&loop] - (int rdRes, QByteArray data) { - res = rdRes; - resData = data; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); + runTree(Group{SignalWaitTaskItem([this, &res, &resData, timeoutMs](SignalWaitTask &task) { + task.setTimeout(timeoutMs); + task.connectSignal(this, &Commands::bmReadMemRes, + [&res, &resData](int rdRes, QByteArray data) { + res = rdRes; + resData = data; + }); + return SetupResult::Continue; + })}); - disconnect(conn); return resData; } @@ -2814,34 +2775,28 @@ int Commands::bmWriteMemWait(uint32_t addr, QByteArray data, int timeoutMs) int res = -10; - QEventLoop loop; - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.start(timeoutMs); - auto conn = connect(this, &Commands::bmWriteFlashRes, [&res,&loop](int wrRes) { - res = wrRes; - loop.quit(); - }); - - connect(&timeoutTimer, SIGNAL(timeout()), &loop, SLOT(quit())); - loop.exec(); + runTree(Group{SignalWaitTaskItem([this, &res, timeoutMs](SignalWaitTask &task) { + task.setTimeout(timeoutMs); + task.connectSignal(this, &Commands::bmWriteFlashRes, + [&res](int wrRes) { res = wrRes; }); + return SetupResult::Continue; + })}); - disconnect(conn); return res; } void Commands::setAppConfig(ConfigParams *appConfig) { mAppConfig = appConfig; - connect(mAppConfig, SIGNAL(updateRequested()), this, SLOT(getAppConf())); - connect(mAppConfig, SIGNAL(updateRequestDefault()), this, SLOT(getAppConfDefault())); + connect(mAppConfig, &ConfigParams::updateRequested, this, &Commands::getAppConf); + connect(mAppConfig, &ConfigParams::updateRequestDefault, this, &Commands::getAppConfDefault); } void Commands::setMcConfig(ConfigParams *mcConfig) { mMcConfig = mcConfig; - connect(mMcConfig, SIGNAL(updateRequested()), this, SLOT(getMcconf())); - connect(mMcConfig, SIGNAL(updateRequestDefault()), this, SLOT(getMcconfDefault())); + connect(mMcConfig, &ConfigParams::updateRequested, this, &Commands::getMcconf); + connect(mMcConfig, &ConfigParams::updateRequestDefault, this, &Commands::getMcconfDefault); } void Commands::checkMcConfig() diff --git a/commands.h b/commands.h index ff21cb23a..fb246ad17 100644 --- a/commands.h +++ b/commands.h @@ -25,12 +25,14 @@ #include #include #include +#include #include "datatypes.h" #include "configparams.h" class Commands : public QObject { Q_OBJECT + QML_ELEMENT public: explicit Commands(QObject *parent = nullptr); @@ -94,7 +96,7 @@ class Commands : public QObject signals: void sampleDataQmlStarted(int sample_len); - void dataToSend(QByteArray &data); + void dataToSend(QByteArray data); void fwVersionReceived(FW_RX_PARAMS params); void eraseNewAppResReceived(bool ok); @@ -177,7 +179,7 @@ class Commands : public QObject public slots: void processPacket(QByteArray data); - void getFwVersion(); + Q_INVOKABLE void getFwVersion(); void eraseNewApp(bool fwdCan, quint32 fwSize, HW_TYPE hwType, QString hwName); void eraseBootloader(bool fwdCan, HW_TYPE hwType, QString hwName); void writeNewAppData(QByteArray data, quint32 offset, bool fwdCan, HW_TYPE hwType, QString hwName); @@ -216,7 +218,7 @@ public slots: void sendCustomAppData(QByteArray data); void sendCustomAppData(unsigned char *data, unsigned int len); void sendCustomHwData(QByteArray data); - void setChukData(chuck_data &data); + void setChukData(const chuck_data &data); void pairNrf(int ms); void gpdSetFsw(float fsw); void getGpdBufferSizeLeft(); diff --git a/configparams.cpp b/configparams.cpp index 9861b74c2..a49ac6d46 100644 --- a/configparams.cpp +++ b/configparams.cpp @@ -19,6 +19,7 @@ #include "configparams.h" #include +#include #include "widgets/parameditdouble.h" #include "widgets/parameditint.h" #include "widgets/parameditstring.h" @@ -1160,7 +1161,7 @@ bool ConfigParams::saveXml(QString fileName, QString configName) emit savingXml(); QXmlStreamWriter stream(&file); - stream.setCodec("UTF-8"); + // Qt6: QXmlStreamWriter always uses UTF-8, setCodec() removed stream.setAutoFormatting(true); getXML(stream, configName); @@ -1201,24 +1202,22 @@ QString ConfigParams::saveCompressed(QString configName) QByteArray data; QXmlStreamWriter stream(&data); - stream.setCodec("UTF-8"); + // Qt6: QXmlStreamWriter always uses UTF-8, setCodec() removed stream.setAutoFormatting(true); getXML(stream, configName); std::size_t outMaxSize = lzokay::compress_worst_size(data.size()); - unsigned char *out = new unsigned char[outMaxSize]; + std::vector out(outMaxSize); std::size_t outLen = 0; - lzokay::EResult error = lzokay::compress((const uint8_t*)data.constData(), data.size(), out, outMaxSize, outLen); + lzokay::EResult error = lzokay::compress((const uint8_t*)data.constData(), data.size(), out.data(), outMaxSize, outLen); if (error == lzokay::EResult::Success) { - QByteArray data((char*)out, outLen); + QByteArray data((char*)out.data(), outLen); result = data.toBase64(); } else { qWarning() << "Could not compress data."; } - delete[] out; - return result; } @@ -1229,12 +1228,11 @@ bool ConfigParams::loadCompressed(QString data, QString configName) QByteArray in = QByteArray::fromBase64(data.toLocal8Bit()); std::size_t outMaxSize = 2 * 1024 * 1024; - unsigned char *out = new unsigned char[outMaxSize]; + std::vector out(outMaxSize); std::size_t outLen = 0; - lzokay::EResult error = lzokay::decompress((const uint8_t*)in.constData(), in.size(), out, outMaxSize, outLen); - QByteArray xmlData((const char*)out, outLen); - delete[] out; + lzokay::EResult error = lzokay::decompress((const uint8_t*)in.constData(), in.size(), out.data(), outMaxSize, outLen); + QByteArray xmlData((const char*)out.data(), outLen); if (error == lzokay::EResult::Success) { QXmlStreamReader stream(xmlData); @@ -1521,7 +1519,7 @@ bool ConfigParams::saveParamsXml(QString fileName) } QXmlStreamWriter stream(&file); - stream.setCodec("UTF-8"); + // Qt6: QXmlStreamWriter always uses UTF-8, setCodec() removed stream.setAutoFormatting(true); getParamsXML(stream); @@ -1553,7 +1551,7 @@ QByteArray ConfigParams::getCompressedParamsXml() { QByteArray res; QXmlStreamWriter stream(&res); - stream.setCodec("UTF-8"); + // Qt6: QXmlStreamWriter always uses UTF-8, setCodec() removed stream.setAutoFormatting(true); getParamsXML(stream); return qCompress(res, 9); diff --git a/configparams.h b/configparams.h index 5427411dc..2a646a7c5 100644 --- a/configparams.h +++ b/configparams.h @@ -25,12 +25,14 @@ #include #include #include +#include #include "configparam.h" #include "vbytearray.h" class ConfigParams : public QObject { Q_OBJECT + QML_ELEMENT public: explicit ConfigParams(QObject *parent = nullptr); Q_INVOKABLE void addParam(const QString &name, ConfigParam param); @@ -88,8 +90,8 @@ class ConfigParams : public QObject void setSerializeOrder(const QStringList &serializeOrder); void clearSerializeOrder(); - Q_INVOKABLE void serialize(VByteArray &vb); - Q_INVOKABLE bool deSerialize(VByteArray &vb); + void serialize(VByteArray &vb); + bool deSerialize(VByteArray &vb); void getXML(QXmlStreamWriter &stream, QString configName); bool setXML(QXmlStreamReader &stream, QString configName); diff --git a/convert_signal_slot2.py b/convert_signal_slot2.py new file mode 100644 index 000000000..1453b54ec --- /dev/null +++ b/convert_signal_slot2.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +""" +Phase 2: Convert remaining multi-line SIGNAL/SLOT connections. +Character-based parser with comprehensive class mapping. +""" + +import os + +def guess_this_class(filepath, context=''): + """Guess class for 'this' based on filepath and surrounding code context.""" + basename = os.path.basename(filepath).replace('.cpp', '') + + class_map = { + 'vescinterface': 'VescInterface', 'mainwindow': 'MainWindow', + 'boardsetupwindow': 'BoardSetupWindow', 'commands': 'Commands', + 'codeloader': 'CodeLoader', 'utility': 'Utility', + 'tcpserversimple': 'TcpServerSimple', 'udpserversimple': 'UdpServerSimple', + 'tcphub': 'TcpHub', 'packet': 'Packet', 'preferences': 'Preferences', + 'parametereditor': 'ParameterEditor', 'bleuart': 'BleUart', + 'mrichtextedit': 'MRichTextEdit', 'mapwidget': 'MapWidget', + 'osmclient': 'OsmClient', 'pagefirmware': 'PageFirmware', + 'pageconnection': 'PageConnection', 'pageswdprog': 'PageSwdProg', + 'pagesampleddata': 'PageSampledData', 'pagedisplaytool': 'PageDisplayTool', + 'pageterminal': 'PageTerminal', 'pageexperiments': 'PageExperiments', + 'pagewelcome': 'PageWelcome', 'pageimu': 'PageImu', + 'pageappimu': 'PageAppImu', 'pagertdata': 'PageRtData', + 'pagebms': 'PageBms', 'pageespprog': 'PageEspProg', + 'pagecananalyzer': 'PageCanAnalyzer', 'pagecustomconfig': 'PageCustomConfig', + 'pageappnunchuk': 'PageAppNunchuk', 'pageappadc': 'PageAppAdc', + 'pageappppm': 'PageAppPpm', 'pagemotorinfo': 'PageMotorInfo', + 'pagemotorcomparison': 'PageMotorComparison', 'experimentplot': 'ExperimentPlot', + 'detectfoc': 'DetectFoc', 'detectfocencoder': 'DetectFocEncoder', + 'detectfochall': 'DetectFocHall', 'detectbldc': 'DetectBldc', + 'detectallfocdialog': 'DetectAllFocDialog', 'nrfpair': 'NrfPair', + 'ppmmap': 'PpmMap', 'adcmap': 'AdcMap', + 'parameditdouble': 'ParamEditDouble', 'parameditint': 'ParamEditInt', + 'parameditenum': 'ParamEditEnum', 'parameditbool': 'ParamEditBool', + 'parameditstring': 'ParamEditString', 'parameditbitfield': 'ParamEditBitfield', + 'startupwizard': 'StartupWizard', + } + + cls = class_map.get(basename) + if cls is not None: + return cls + + # For setupwizardapp.cpp / setupwizardmotor.cpp: multiple inner classes + # Use context to determine which class + return None + +def guess_class(obj, method_name, method_params, filepath, context=''): + """Guess class from object expression, method, and filepath.""" + obj = obj.strip() + + # this + if obj == 'this': + cls = guess_this_class(filepath, context) + if cls is None: + # Fallback for wizard files: check signal/slot names + if method_name == 'currentIdChanged': + return 'QWizard' + if method_name in ('rejected', 'accepted'): + return 'QDialog' + if method_name in ('timerSlot', 'ended', 'nrfPairStartRes'): + # These are slots on setupwizard inner classes - need the class + return None + return cls + + # &loop + if obj == '&loop': + return 'QEventLoop' + + # Timers + if 'Timer' in obj or 'mTimer' in obj: + return 'QTimer' + + # Serial/network + if 'mSerialPort' in obj or 'mVictronPort' in obj: + return 'QSerialPort' + if 'mTcpSocket' in obj: + return 'QTcpSocket' + if 'mUdpSocket' in obj: + return 'QUdpSocket' + if 'mTcpServer' in obj or 'mTcpHubServer' in obj: + return 'QTcpServer' + + # BLE + if 'mBleUart' in obj or 'bleDevice()' in obj: + return 'BleUart' + if 'mService' in obj: + return 'QLowEnergyService' + + # VESC types + if 'mPacket' in obj: + return 'Packet' + if 'mCommands' in obj or 'commands()' in obj: + return 'Commands' + if any(x in obj for x in ['mMcConfig', 'mcConfig()', 'mAppConfig', 'appConfig()', 'pMc', 'pApp', 'pCustom']): + return 'ConfigParams' + if 'mConfig' in obj: + return 'ConfigParams' + if obj == 'mVesc' or (obj.startswith('mVesc') and '->' not in obj): + return 'VescInterface' + if obj == 'vesc' or (obj.startswith('vesc') and '->' not in obj): + return 'VescInterface' + + # CAN + if 'mCanDevice' in obj: + return 'QCanBusDevice' + + # Network + if 'reply' in obj.lower() and method_name == 'finished': + return 'QNetworkReply' + if 'mWebCtrl' in obj: + return 'QNetworkAccessManager' + if 'mOsm' in obj: + return 'OsmClient' + + # Clipboard + if 'clipboard()' in obj: + return 'QClipboard' + + # Scrollbar + if 'ScrollBar()' in obj: + return 'QScrollBar' + + # Document + if 'document()' in obj: + return 'QTextDocument' + + # Process + if 'process' in obj.lower() and method_name == 'finished': + return 'QProcess' + + # Text edit + if 'f_textedit' in obj and '->' not in obj: + return 'QTextEdit' + + # Lists + if obj in ('mCanFwdList', 'mInputList'): + return 'QListWidget' + if obj == 'mSensorMode': + return 'QComboBox' + if obj == 'mWriteButton': + return 'QPushButton' + + # Boxes + if 'mPercentageBox' in obj: + return 'QSlider' + if 'Box' in obj: + if method_params == 'double': + return 'QDoubleSpinBox' + elif method_params == 'int': + return 'QSpinBox' + + # f_* (mrichtextedit toolbar buttons) + if obj.startswith('f_'): + if method_name in ('clicked', 'toggled'): + return 'QToolButton' + if method_name == 'triggered': + return 'QAction' + if method_name == 'activated': + return 'QComboBox' + if method_name == 'setEnabled': + return 'QWidget' + if obj in ('removeFormat', 'removeAllFormat', 'textsource'): + return 'QAction' + + # Page objects + if 'mPageMotorSettings' in obj: + return 'PageMotorSettings' + if 'mPageWelcome' in obj: + return 'PageWelcome' + + # qApp + if obj == 'qApp': + return 'QApplication' + + # ui-> widgets + if obj.startswith('ui->'): + if method_name in ('clicked', 'toggled'): + return 'QAbstractButton' + if method_name == 'valueChanged': + return 'QDoubleSpinBox' if method_params == 'double' else 'QSpinBox' + if method_name == 'currentIndexChanged': + return 'QComboBox' + if method_name == 'currentRowChanged': + return 'QListWidget' + if method_name == 'textChanged': + return 'QLineEdit' + if method_name == 'currentFontChanged': + return 'QFontComboBox' + if method_name == 'triggered': + return 'QAction' + + # mBrowser + if 'mBrowser' in obj: + if 'ScrollBar' in obj: + return 'QScrollBar' + return 'QTextBrowser' + + return None + +NEEDS_QOVERLOAD = { + ('QSpinBox', 'valueChanged'), ('QDoubleSpinBox', 'valueChanged'), + ('QComboBox', 'activated'), ('QComboBox', 'currentIndexChanged'), + ('QProcess', 'finished'), + ('QAbstractButton', 'clicked'), ('QToolButton', 'clicked'), + ('QAction', 'triggered'), ('QPushButton', 'clicked'), +} + +def build_pmf(cls, method, params): + if (cls, method) in NEEDS_QOVERLOAD: + if params: + return f"qOverload<{params}>(&{cls}::{method})" + else: + return f"qOverload<>(&{cls}::{method})" + return f"&{cls}::{method}" + +def extract_macro(text, macro): + idx = text.find(macro + '(') + if idx == -1: + return None + start = idx + len(macro) + 1 + depth = 1 + i = start + while i < len(text) and depth > 0: + if text[i] == '(': + depth += 1 + elif text[i] == ')': + depth -= 1 + i += 1 + return text[start:i-1].strip() + +def split_top_level_commas(text): + parts = [] + depth = 0 + angle = 0 + current = '' + for ch in text: + if ch in '({': + depth += 1 + current += ch + elif ch in ')}': + depth -= 1 + current += ch + elif ch == '<': + angle += 1 + current += ch + elif ch == '>': + if angle > 0: + angle -= 1 + current += ch + elif ch == ',' and depth == 0 and angle == 0: + parts.append(current) + current = '' + else: + current += ch + if current: + parts.append(current) + return parts + +def parse_sig(content): + """Parse 'methodName(params)' -> (name, params)""" + idx = content.index('(') + return content[:idx], content[idx+1:-1].strip() + +def try_convert_call(func_name, inner, filepath): + """Try to convert connect/disconnect inner args to pointer-based.""" + + args = split_top_level_commas(inner) + if len(args) != 4: + return None + + sender = args[0].strip() + sig_content = extract_macro(args[1], 'SIGNAL') + if sig_content is None: + return None + + receiver = args[2].strip() + # Remove leading whitespace/newlines from receiver + receiver = receiver.lstrip() + + slot_content = extract_macro(args[3], 'SLOT') + if slot_content is None: + return None + + sig_name, sig_params = parse_sig(sig_content) + slot_name, slot_params = parse_sig(slot_content) + + sender_cls = guess_class(sender, sig_name, sig_params, filepath) + receiver_cls = guess_class(receiver, slot_name, slot_params, filepath) + + if sender_cls is None or receiver_cls is None: + return None + + sig_pmf = build_pmf(sender_cls, sig_name, sig_params) + slot_pmf = build_pmf(receiver_cls, slot_name, slot_params) + + # Detect indentation from original formatting + if '\n' in inner: + # Find whitespace before receiver in original + # The receiver was args[2] which may have leading whitespace/newlines + raw_receiver = args[2] + indent = '' + for ch in raw_receiver: + if ch in ' \t\n\r': + if ch == '\n': + indent = '' + else: + indent += ch + else: + break + new_inner = f"{sender}, {sig_pmf},\n{indent}{receiver}, {slot_pmf}" + else: + new_inner = f"{sender}, {sig_pmf}, {receiver}, {slot_pmf}" + + return f"{func_name}({new_inner})" + +def process_file(filepath): + with open(filepath, 'r') as f: + content = f.read() + + result = [] + i = 0 + changes = 0 + + while i < len(content): + matched_func = None + for func in ['disconnect', 'connect']: + flen = len(func) + if content[i:i+flen+1] == func + '(': + # Ensure not part of another identifier + if i > 0 and (content[i-1].isalnum() or content[i-1] == '_'): + continue + matched_func = func + break + + if matched_func is None: + result.append(content[i]) + i += 1 + continue + + func = matched_func + flen = len(func) + start = i + paren_start = i + flen # index of '(' + + # Find balanced closing paren + depth = 0 + j = paren_start + while j < len(content): + if content[j] == '(': + depth += 1 + elif content[j] == ')': + depth -= 1 + if depth == 0: + break + j += 1 + + if depth != 0: + result.append(content[i]) + i += 1 + continue + + inner = content[paren_start+1:j] + + # Skip if no SIGNAL macro or if this is a waitSignal context + if 'SIGNAL(' not in inner: + result.append(content[i]) + i += 1 + continue + + # Check context before for waitSignal + ctx_before = content[max(0, start-30):start] + if 'waitSignal' in ctx_before: + result.append(content[i]) + i += 1 + continue + + converted = try_convert_call(func, inner, filepath) + if converted is not None: + result.append(converted) + changes += 1 + i = j + 1 # skip past closing paren + else: + result.append(content[i]) + i += 1 + + if changes > 0: + new_content = ''.join(result) + with open(filepath, 'w') as f: + f.write(new_content) + print(f" {filepath}: {changes} conversions") + + return changes + +def main(): + root = '/home/robocup/vesc_tool' + skip_dirs = {'build', 'QCodeEditor', 'qmarkdowntextedit', 'application', '.git'} + + total = 0 + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = [d for d in dirnames if d not in skip_dirs] + for fn in sorted(filenames): + if not fn.endswith('.cpp') or fn == 'qcustomplot.cpp': + continue + fpath = os.path.join(dirpath, fn) + with open(fpath, 'r') as f: + c = f.read() + if 'SIGNAL(' not in c: + continue + n = process_file(fpath) + total += n + + print(f"\nTotal: {total} additional conversions") + +if __name__ == '__main__': + main() diff --git a/datatypes.h b/datatypes.h index 1863f9a05..d8f97887b 100644 --- a/datatypes.h +++ b/datatypes.h @@ -214,9 +214,32 @@ struct MC_VALUES { } bool operator==(const MC_VALUES &other) const { - (void)other; - // compare members - return true; + return v_in == other.v_in && + temp_mos == other.temp_mos && + temp_mos_1 == other.temp_mos_1 && + temp_mos_2 == other.temp_mos_2 && + temp_mos_3 == other.temp_mos_3 && + temp_motor == other.temp_motor && + current_motor == other.current_motor && + current_in == other.current_in && + id == other.id && + iq == other.iq && + rpm == other.rpm && + duty_now == other.duty_now && + amp_hours == other.amp_hours && + amp_hours_charged == other.amp_hours_charged && + watt_hours == other.watt_hours && + watt_hours_charged == other.watt_hours_charged && + tachometer == other.tachometer && + tachometer_abs == other.tachometer_abs && + position == other.position && + fault_code == other.fault_code && + vesc_id == other.vesc_id && + fault_str == other.fault_str && + vd == other.vd && + vq == other.vq && + has_timeout == other.has_timeout && + kill_sw_active == other.kill_sw_active; } bool operator!=(MC_VALUES const &other) const { @@ -307,9 +330,29 @@ struct SETUP_VALUES { } bool operator==(const SETUP_VALUES &other) const { - (void)other; - // compare members - return true; + return temp_mos == other.temp_mos && + temp_motor == other.temp_motor && + current_motor == other.current_motor && + current_in == other.current_in && + duty_now == other.duty_now && + rpm == other.rpm && + speed == other.speed && + v_in == other.v_in && + battery_level == other.battery_level && + amp_hours == other.amp_hours && + amp_hours_charged == other.amp_hours_charged && + watt_hours == other.watt_hours && + watt_hours_charged == other.watt_hours_charged && + tachometer == other.tachometer && + tachometer_abs == other.tachometer_abs && + position == other.position && + fault_code == other.fault_code && + vesc_id == other.vesc_id && + num_vescs == other.num_vescs && + battery_wh == other.battery_wh && + fault_str == other.fault_str && + odometer == other.odometer && + uptime_ms == other.uptime_ms; } bool operator!=(SETUP_VALUES const &other) const { @@ -386,9 +429,23 @@ struct IMU_VALUES { } bool operator==(const IMU_VALUES &other) const { - (void)other; - // compare members - return true; + return roll == other.roll && + pitch == other.pitch && + yaw == other.yaw && + accX == other.accX && + accY == other.accY && + accZ == other.accZ && + gyroX == other.gyroX && + gyroY == other.gyroY && + gyroZ == other.gyroZ && + magX == other.magX && + magY == other.magY && + magZ == other.magZ && + q0 == other.q0 && + q1 == other.q1 && + q2 == other.q2 && + q3 == other.q3 && + vesc_id == other.vesc_id; } bool operator!=(IMU_VALUES const &other) const { @@ -452,9 +509,17 @@ struct STAT_VALUES { } bool operator==(const STAT_VALUES &other) const { - (void)other; - // compare members - return true; + return speed_avg == other.speed_avg && + speed_max == other.speed_max && + power_avg == other.power_avg && + power_max == other.power_max && + temp_motor_avg == other.temp_motor_avg && + temp_motor_max == other.temp_motor_max && + temp_mos_avg == other.temp_mos_avg && + temp_mos_max == other.temp_mos_max && + current_avg == other.current_avg && + current_max == other.current_max && + count_time == other.count_time; } bool operator!=(STAT_VALUES const &other) const { diff --git a/desktop/AppAdcPage.qml b/desktop/AppAdcPage.qml new file mode 100644 index 000000000..9efdd4daf --- /dev/null +++ b/desktop/AppAdcPage.qml @@ -0,0 +1,277 @@ +/* + Desktop AppAdcPage — exact parity with PageAppAdc widget. + Layout: TabWidget (General | Mapping | Throttle Curve) + General: ParamTable of "ADC" / "general" + Mapping: ParamTable of "ADC" / "mapping" + ADC Voltage Mapping GroupBox + Throttle: ParamTable (fixed height ~100) + throttle curve plot +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtGraphs +import Vedder.vesc + +Item { + id: root + + property ConfigParams mAppConf: VescIf.appConfig() + property Commands mCommands: VescIf.commands() + property var _dynamicItems: [] + property bool _mapResetDone: true + property double _minCh1: 0.0 + property double _maxCh1: 0.0 + property double _centerCh1: 0.0 + property double _minCh2: 0.0 + property double _maxCh2: 0.0 + + ParamEditors { id: editors } + + function updateThrottleCurve() { + var mode = mAppConf.getParamEnum("app_adc_conf.throttle_exp_mode") + var valAcc = mAppConf.getParamDouble("app_adc_conf.throttle_exp") + var valBrk = mAppConf.getParamDouble("app_adc_conf.throttle_exp_brake") + + for (var si = throttlePlot.count - 1; si >= 0; si--) + throttlePlot.removeSeries(throttlePlot.seriesAt(si)) + + var series = throttlePlot.createSeries(GraphsView.SeriesTypeLine, "Throttle Curve") + series.color = Utility.getAppHexColor("plot_graph1") + series.width = 1.5 + + for (var i = -1.0; i <= 1.0001; i += 0.002) + series.append(i, Utility.throttle_curve(i, valAcc, valBrk, mode)) + + throttleAxisX.min = -1.0; throttleAxisX.max = 1.0 + var yMin = -1.0, yMax = 1.0 + for (var j = 0; j < series.count; j++) { + var pt = series.at(j) + if (pt.y < yMin) yMin = pt.y + if (pt.y > yMax) yMax = pt.y + } + var pad = (yMax - yMin) * 0.05 + throttleAxisY.min = yMin - pad + throttleAxisY.max = yMax + pad + } + + Connections { + target: mAppConf + function onParamChangedDouble(src, name, newParam) { + if (name === "app_adc_conf.throttle_exp" || name === "app_adc_conf.throttle_exp_brake") + updateThrottleCurve() + } + function onParamChangedEnum(src, name, newParam) { + if (name === "app_adc_conf.throttle_exp_mode") + updateThrottleCurve() + } + } + + Connections { + target: mCommands + function onDecodedAdcReceived(value, voltage, value2, voltage2) { + // CH1 display + var p1 = dualBox.checked ? (value - 0.5) * 200.0 : value * 100.0 + adc1Bar.value = Math.max(-100, Math.min(100, p1)) / 100.0 + adc1Label.text = voltage.toFixed(dualBox.checked ? 2 : 4) + " V (" + p1.toFixed(1) + " %)" + + // CH2 display + var p2 = dualBox.checked ? (value2 - 0.5) * 200.0 : value2 * 100.0 + adc2Bar.value = Math.max(-100, Math.min(100, p2)) / 100.0 + adc2Label.text = voltage2.toFixed(4) + " V (" + p2.toFixed(1) + " %)" + + // Auto min/max + if (_mapResetDone) { + _mapResetDone = false + _minCh1 = voltage; _maxCh1 = voltage + _minCh2 = voltage2; _maxCh2 = voltage2 + } else { + if (voltage < _minCh1) _minCh1 = voltage + if (voltage > _maxCh1) _maxCh1 = voltage + if (voltage2 < _minCh2) _minCh2 = voltage2 + if (voltage2 > _maxCh2) _maxCh2 = voltage2 + } + + var range = _maxCh1 - _minCh1 + var pos = voltage - _minCh1 + if (pos > range / 4.0 && pos < (3.0 * range) / 4.0) + _centerCh1 = voltage + else + _centerCh1 = range / 2.0 + _minCh1 + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TabBar { + id: tabBar + Layout.fillWidth: true + TabButton { text: "General"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Mapping"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Throttle Curve"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: tabBar.currentIndex + + // Tab 0: General + Flickable { + clip: true; contentWidth: width; contentHeight: genCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + ColumnLayout { id: genCol; width: parent.width; spacing: 4; Item { Layout.preferredHeight: 1 } } + } + + // Tab 1: Mapping + Flickable { + clip: true; contentWidth: width; contentHeight: mapLayout.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + ColumnLayout { + id: mapLayout + width: parent.width + spacing: 4 + + Item { Layout.preferredHeight: 1 } + + ColumnLayout { + id: mapCol + Layout.fillWidth: true + spacing: 4 + } + + // ADC Voltage Mapping GroupBox + GroupBox { + title: "ADC Voltage Mapping" + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + spacing: 4 + + RowLayout { + spacing: 4 + CheckBox { + id: dualBox + text: "Use Centered Control" + ToolTip.text: "Show centered graph, which is how the centered control modes interpret the throttle." + ToolTip.visible: dualHover.hovered; ToolTip.delay: 500 + HoverHandler { id: dualHover } + } + Button { + text: "Reset" + ToolTip.text: "Reset min and max"; ToolTip.visible: hovered + onClicked: { _mapResetDone = true; _minCh1 = 0; _maxCh1 = 0; _minCh2 = 0; _maxCh2 = 0 } + } + Button { + icon.source: "qrc" + Utility.getThemePath() + "icons/Ok-96.png" + text: "Apply" + ToolTip.text: "Apply min and max to configuration"; ToolTip.visible: hovered + onClicked: { + if (_maxCh1 > 1e-10) { + mAppConf.updateParamDouble("app_adc_conf.voltage_start", _minCh1) + mAppConf.updateParamDouble("app_adc_conf.voltage_end", _maxCh1) + mAppConf.updateParamDouble("app_adc_conf.voltage_center", _centerCh1) + mAppConf.updateParamDouble("app_adc_conf.voltage2_start", _minCh2) + mAppConf.updateParamDouble("app_adc_conf.voltage2_end", _maxCh2) + VescIf.emitStatusMessage("Start, End and Center ADC Voltages Applied", true) + } else { + VescIf.emitStatusMessage("Applying Voltages Failed", false) + } + } + } + } + + GridLayout { + columns: 5 + Layout.fillWidth: true + columnSpacing: 2; rowSpacing: 2 + + // CH1 row + Label { text: "CH1" } + Label { text: "Min: " + _minCh1.toFixed(2); padding: 4 + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } } + Label { text: "Max: " + _maxCh1.toFixed(2); padding: 4 + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } } + Label { text: "Center: " + _centerCh1.toFixed(2); padding: 4 + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + ProgressBar { id: adc1Bar; Layout.fillWidth: true; from: -1; to: 1; value: 0; Layout.preferredHeight: 20 } + Label { id: adc1Label; text: "-- V (-- %)"; font.pointSize: 11; color: Utility.getAppHexColor("lightText") } + } + + // CH2 row + Label { text: "CH2" } + Label { text: "Min: " + _minCh2.toFixed(2); padding: 4 + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } } + Label { text: "Max: " + _maxCh2.toFixed(2); padding: 4 + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } } + Item { } // no center for CH2 + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + ProgressBar { id: adc2Bar; Layout.fillWidth: true; from: -1; to: 1; value: 0; Layout.preferredHeight: 20 } + Label { id: adc2Label; text: "-- V (-- %)"; font.pointSize: 11; color: Utility.getAppHexColor("lightText") } + } + } + } + } + } + } + + // Tab 2: Throttle Curve + ColumnLayout { + spacing: 0 + + Flickable { + Layout.fillWidth: true + Layout.preferredHeight: 100; Layout.maximumHeight: 100 + clip: true; contentWidth: width; contentHeight: tcCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + ColumnLayout { id: tcCol; width: parent.width; spacing: 4; Item { Layout.preferredHeight: 1 } } + } + + GraphsView { + id: throttlePlot + Layout.fillWidth: true + Layout.fillHeight: true + + theme: GraphsTheme { + colorScheme: Utility.isDarkMode() ? GraphsTheme.ColorScheme.Dark : GraphsTheme.ColorScheme.Light + plotAreaBackgroundColor: Utility.getAppHexColor("plotBackground") + grid.mainColor: Utility.isDarkMode() ? "#444444" : "#cccccc" + } + + axisX: ValueAxis { id: throttleAxisX; min: -1; max: 1; titleText: "Throttle Value" } + axisY: ValueAxis { id: throttleAxisY; min: -1; max: 1; titleText: "Output Value" } + } + } + } + } + + function loadSubgroup(parentCol, subgroup) { + var params = mAppConf.getParamsFromSubgroup("ADC", subgroup) + for (var p = 0; p < params.length; p++) { + var paramName = params[p] + if (paramName.indexOf("::sep::") === 0) { + var sep = editors.createSeparator(parentCol, paramName.substring(7)) + if (sep) _dynamicItems.push(sep) + continue + } + var e = editors.createEditorApp(parentCol, paramName) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: { + loadSubgroup(genCol, "general") + loadSubgroup(mapCol, "mapping") + loadSubgroup(tcCol, "throttle curve") + } +} diff --git a/desktop/AppGeneralPage.qml b/desktop/AppGeneralPage.qml new file mode 100644 index 000000000..1149d8b2f --- /dev/null +++ b/desktop/AppGeneralPage.qml @@ -0,0 +1,111 @@ +/* + Desktop AppGeneralPage — exact parity with PageAppGeneral widget. + Layout: TabWidget (General | Tools) + General: ParamTable of "general" / "general" + Tools: Servo Output GroupBox with slider + Center button, then spacer +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Vedder.vesc + +Item { + id: root + + property ConfigParams mAppConf: VescIf.appConfig() + property Commands mCommands: VescIf.commands() + property var _dynamicItems: [] + + ParamEditors { id: editors } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TabBar { + id: tabBar + Layout.fillWidth: true + TabButton { text: "General"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Tools"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: tabBar.currentIndex + + // Tab 0: General params + Flickable { + clip: true + contentWidth: width + contentHeight: genCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + ColumnLayout { + id: genCol + width: parent.width + spacing: 4 + Item { Layout.preferredHeight: 1 } + } + } + + // Tab 1: Tools (Servo Output) + ColumnLayout { + spacing: 0 + + GroupBox { + title: "Servo Output" + Layout.fillWidth: true + Layout.margins: 0 + + ColumnLayout { + anchors.fill: parent + spacing: 4 + + Slider { + id: servoSlider + Layout.fillWidth: true + from: 0; to: 1000; value: 500; stepSize: 1 + snapMode: Slider.SnapOnRelease + onValueChanged: mCommands.setServoPos(value / 1000.0) + } + + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + Button { + text: "Center" + onClicked: servoSlider.value = 500 + } + Item { Layout.fillWidth: true } + } + } + } + + Item { Layout.fillHeight: true } + } + } + } + + function loadParams() { + for (var d = 0; d < _dynamicItems.length; d++) { + if (_dynamicItems[d]) _dynamicItems[d].destroy() + } + _dynamicItems = [] + + var params = mAppConf.getParamsFromSubgroup("General", "general") + for (var p = 0; p < params.length; p++) { + var paramName = params[p] + if (paramName.indexOf("::sep::") === 0) { + var sep = editors.createSeparator(genCol, paramName.substring(7)) + if (sep) _dynamicItems.push(sep) + continue + } + var e = editors.createEditorApp(genCol, paramName) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: loadParams() +} diff --git a/desktop/AppImuPage.qml b/desktop/AppImuPage.qml new file mode 100644 index 000000000..87ce481ae --- /dev/null +++ b/desktop/AppImuPage.qml @@ -0,0 +1,316 @@ +/* + Desktop AppImuPage — exact parity with PageAppImu widget. + Layout: Vertical SplitView + Top: ParamTable for "imu"/"general" + Bottom: Horizontal SplitView + Left: TabBar (bottom) with RPY / Accel / Gyro — each with rolling 500-sample GraphsView + Right: "Use Yaw" checkbox + Vesc3DView + Timer-driven replot at 20 ms. +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtGraphs +import Vedder.vesc + +Item { + id: root + + property Commands mCommands: VescIf.commands() + property ConfigParams mAppConf: VescIf.appConfig() + property var _dynamicItems: [] + + // Rolling data buffers (500 samples max) + property var _seconds: [] + property var _rollVec: []; property var _pitchVec: []; property var _yawVec: [] + property var _accXVec: []; property var _accYVec: []; property var _accZVec: [] + property var _gyroXVec: []; property var _gyroYVec: []; property var _gyroZVec: [] + + property double _secondCounter: 0.0 + property bool _updatePlots: false + + // Latest values for axis labels + property double _curRoll: 0; property double _curPitch: 0; property double _curYaw: 0 + property double _curAccX: 0; property double _curAccY: 0; property double _curAccZ: 0 + property double _curGyroX: 0; property double _curGyroY: 0; property double _curGyroZ: 0 + + readonly property int _maxS: 500 + + ParamEditors { id: editors } + + function appendAndTrunc(arr, val) { + arr.push(val) + if (arr.length > _maxS) arr.splice(0, arr.length - _maxS) + } + + Connections { + target: RtDataStore + function onImuDataAppended(time, roll, pitch, yaw, accX, accY, accZ, gyroX, gyroY, gyroZ) { + appendAndTrunc(_seconds, time) + appendAndTrunc(_rollVec, roll) + appendAndTrunc(_pitchVec, pitch) + appendAndTrunc(_yawVec, yaw) + appendAndTrunc(_accXVec, accX) + appendAndTrunc(_accYVec, accY) + appendAndTrunc(_accZVec, accZ) + appendAndTrunc(_gyroXVec, gyroX) + appendAndTrunc(_gyroYVec, gyroY) + appendAndTrunc(_gyroZVec, gyroZ) + + _secondCounter = time + + // Update 3D view (convert degrees back to radians) + var rollRad = roll * Math.PI / 180.0 + var pitchRad = pitch * Math.PI / 180.0 + var yawRad = yaw * Math.PI / 180.0 + view3d.setRotation(rollRad, pitchRad, + useYawBox.checked ? yawRad : 0.0) + + _curRoll = roll; _curPitch = pitch; _curYaw = yaw + _curAccX = accX; _curAccY = accY; _curAccZ = accZ + _curGyroX = gyroX; _curGyroY = gyroY; _curGyroZ = gyroZ + + _updatePlots = true + } + } + + Timer { + interval: 20; running: true; repeat: true + onTriggered: { + if (!_updatePlots) return + _updatePlots = false + rebuildRpy() + rebuildAccel() + rebuildGyro() + } + } + + // ── Rebuild helpers ───────────────────────────────────────── + function updateSeries(series, xVec, yVec) { + series.clear() + for (var i = 0; i < xVec.length; i++) + series.append(xVec[i], yVec[i]) + } + + function updateAxes(axX, axY, xVec, vecs, xLabel, yLabel) { + var xMin = 1e18, xMax = -1e18 + var yMin = 1e18, yMax = -1e18 + for (var i = 0; i < xVec.length; i++) { + if (xVec[i] < xMin) xMin = xVec[i] + if (xVec[i] > xMax) xMax = xVec[i] + for (var k = 0; k < vecs.length; k++) { + if (vecs[k][i] < yMin) yMin = vecs[k][i] + if (vecs[k][i] > yMax) yMax = vecs[k][i] + } + } + if (xMin >= xMax) { xMin = 0; xMax = 1 } + axX.min = xMin; axX.max = xMax + axX.titleText = xLabel + if (yMin >= yMax) { yMin = -1; yMax = 1 } + var pad = (yMax - yMin) * 0.05 + axY.min = yMin - pad; axY.max = yMax + pad + axY.titleText = yLabel + } + + function rebuildRpy() { + updateSeries(rpySeries0, _seconds, _rollVec) + updateSeries(rpySeries1, _seconds, _pitchVec) + updateSeries(rpySeries2, _seconds, _yawVec) + updateAxes(rpyAxisX, rpyAxisY, _seconds, [_rollVec, _pitchVec, _yawVec], + "Seconds (s)\nRoll: " + _curRoll.toFixed(3) + " Pitch: " + _curPitch.toFixed(3) + " Yaw: " + _curYaw.toFixed(3), + "Angle (Deg)") + } + + function rebuildAccel() { + updateSeries(accelSeries0, _seconds, _accXVec) + updateSeries(accelSeries1, _seconds, _accYVec) + updateSeries(accelSeries2, _seconds, _accZVec) + updateAxes(accelAxisX, accelAxisY, _seconds, [_accXVec, _accYVec, _accZVec], + "Seconds (s)\nX: " + _curAccX.toFixed(3) + " Y: " + _curAccY.toFixed(3) + " Z: " + _curAccZ.toFixed(3), + "Acceleration (G)") + } + + function rebuildGyro() { + updateSeries(gyroSeries0, _seconds, _gyroXVec) + updateSeries(gyroSeries1, _seconds, _gyroYVec) + updateSeries(gyroSeries2, _seconds, _gyroZVec) + updateAxes(gyroAxisX, gyroAxisY, _seconds, [_gyroXVec, _gyroYVec, _gyroZVec], + "Seconds (s)\nX: " + _curGyroX.toFixed(3) + " Y: " + _curGyroY.toFixed(3) + " Z: " + _curGyroZ.toFixed(3), + "Angular Velocity (Deg/s)") + } + + // ── Shared plot theme ─────────────────────────────────────── + property GraphsTheme _plotTheme: GraphsTheme { + colorScheme: Utility.isDarkMode() ? GraphsTheme.ColorScheme.Dark : GraphsTheme.ColorScheme.Light + plotAreaBackgroundColor: Utility.getAppHexColor("plotBackground") + grid.mainColor: Utility.isDarkMode() ? "#444444" : "#cccccc" + } + + // ───────────────────────────────────────────────────────────── + // MAIN LAYOUT + // ───────────────────────────────────────────────────────────── + SplitView { + anchors.fill: parent + orientation: Qt.Vertical + + // ── Top: param table ──────────────────────────────────── + Flickable { + SplitView.preferredHeight: parent.height * 0.35 + SplitView.minimumHeight: 100 + clip: true; contentWidth: width; contentHeight: paramCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + ColumnLayout { + id: paramCol + width: parent.width + spacing: 4 + Item { Layout.preferredHeight: 1 } + } + } + + // ── Bottom: plots + 3D view ──────────────────────────── + SplitView { + orientation: Qt.Horizontal + SplitView.fillHeight: true + + // Left: tab bar at bottom with 3 plot tabs + ColumnLayout { + SplitView.fillWidth: true + SplitView.minimumWidth: 300 + spacing: 0 + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: plotTabBar.currentIndex + + // RPY Tab + Item { + ColumnLayout { + anchors.fill: parent + spacing: 2 + GraphsView { + id: rpyPlot + Layout.fillWidth: true + Layout.fillHeight: true + theme: root._plotTheme + axisX: ValueAxis { id: rpyAxisX; min: 0; max: 1; titleText: "Seconds (s)" } + axisY: ValueAxis { id: rpyAxisY; min: -180; max: 180; titleText: "Angle (Deg)" } + LineSeries { id: rpySeries0; color: Utility.getAppHexColor("blue"); width: 2; name: "Roll" } + LineSeries { id: rpySeries1; color: Utility.getAppHexColor("green"); width: 2; name: "Pitch" } + LineSeries { id: rpySeries2; color: Utility.getAppHexColor("red"); width: 2; name: "Yaw" } + } + PlotLegend { graphsView: rpyPlot } + } + } + + // Accel Tab + Item { + ColumnLayout { + anchors.fill: parent + spacing: 2 + GraphsView { + id: accelPlot + Layout.fillWidth: true + Layout.fillHeight: true + theme: root._plotTheme + axisX: ValueAxis { id: accelAxisX; min: 0; max: 1; titleText: "Seconds (s)" } + axisY: ValueAxis { id: accelAxisY; min: -2; max: 2; titleText: "Acceleration (G)" } + LineSeries { id: accelSeries0; color: Utility.getAppHexColor("blue"); width: 2; name: "X" } + LineSeries { id: accelSeries1; color: Utility.getAppHexColor("green"); width: 2; name: "Y" } + LineSeries { id: accelSeries2; color: Utility.getAppHexColor("red"); width: 2; name: "Z" } + } + PlotLegend { graphsView: accelPlot } + } + } + + // Gyro Tab + Item { + ColumnLayout { + anchors.fill: parent + spacing: 2 + GraphsView { + id: gyroPlot + Layout.fillWidth: true + Layout.fillHeight: true + theme: root._plotTheme + axisX: ValueAxis { id: gyroAxisX; min: 0; max: 1; titleText: "Seconds (s)" } + axisY: ValueAxis { id: gyroAxisY; min: -500; max: 500; titleText: "Angular Velocity (Deg/s)" } + LineSeries { id: gyroSeries0; color: Utility.getAppHexColor("blue"); width: 2; name: "X" } + LineSeries { id: gyroSeries1; color: Utility.getAppHexColor("green"); width: 2; name: "Y" } + LineSeries { id: gyroSeries2; color: Utility.getAppHexColor("red"); width: 2; name: "Z" } + } + PlotLegend { graphsView: gyroPlot } + } + } + } + + TabBar { + id: plotTabBar + Layout.fillWidth: true + TabButton { text: "RPY"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Accel"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Gyro"; topPadding: 9; bottomPadding: 9 } + } + } + + // Right: Use Yaw checkbox + 3D view + ColumnLayout { + SplitView.preferredWidth: 250 + SplitView.minimumWidth: 180 + spacing: 4 + + CheckBox { + id: useYawBox + text: "Use Yaw (will drift)" + Layout.fillWidth: true + } + + Vesc3DView { + id: view3d + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 200 + } + } + } + } + + function loadSubgroup(parentCol, subgroup) { + var params = mAppConf.getParamsFromSubgroup("imu", subgroup) + for (var p = 0; p < params.length; p++) { + var paramName = params[p] + if (paramName.indexOf("::sep::") === 0) { + var sep = editors.createSeparator(parentCol, paramName.substring(7)) + if (sep) _dynamicItems.push(sep) + continue + } + var e = editors.createEditorApp(parentCol, paramName) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: { + loadSubgroup(paramCol, "general") + view3d.setRotation(20 * Math.PI / 180, 20 * Math.PI / 180, 0) + + // Restore IMU data from persistent C++ store + var imuNames = ["roll", "pitch", "yaw", "accX", "accY", "accZ", "gyroX", "gyroY", "gyroZ"] + var imuVecs = [_rollVec, _pitchVec, _yawVec, _accXVec, _accYVec, _accZVec, + _gyroXVec, _gyroYVec, _gyroZVec] + var rollPts = RtDataStore.imuSeriesPoints("roll") + for (var i = 0; i < rollPts.length; i++) + _seconds.push(rollPts[i].x) + + for (var n = 0; n < imuNames.length; n++) { + var pts = (n === 0) ? rollPts : RtDataStore.imuSeriesPoints(imuNames[n]) + for (var j = 0; j < pts.length; j++) + imuVecs[n].push(pts[j].y) + } + _secondCounter = RtDataStore.imuSecondCounter + if (_seconds.length > 0) { + _updatePlots = true + } + } +} diff --git a/desktop/AppNrfPage.qml b/desktop/AppNrfPage.qml new file mode 100644 index 000000000..8cb0b1813 --- /dev/null +++ b/desktop/AppNrfPage.qml @@ -0,0 +1,138 @@ +/* + Desktop AppNrfPage — exact parity with PageAppNrf widget. + Layout: VBoxLayout + ParamTable for "nrf" / "general" + NRF Pairing GroupBox at bottom +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Vedder.vesc + +Item { + id: root + + property ConfigParams mAppConf: VescIf.appConfig() + property Commands mCommands: VescIf.commands() + property var _dynamicItems: [] + + // NRF pairing state + property double _pairCnt: 0.0 + property bool _pairRunning: false + + ParamEditors { id: editors } + + Connections { + target: mCommands + function onNrfPairingRes(res) { + if (!_pairRunning) return + if (res === 0) { + _pairCnt = nrfTimeBox.realValue + nrfStartBtn.enabled = false + } else if (res === 1) { + nrfStartBtn.enabled = true + _pairCnt = 0 + VescIf.emitStatusMessage("Pairing NRF Successful", true) + _pairRunning = false + } else if (res === 2) { + nrfStartBtn.enabled = true + _pairCnt = 0 + VescIf.emitStatusMessage("Pairing NRF Timed Out", false) + _pairRunning = false + } + } + } + + Timer { + interval: 100; running: true; repeat: true + onTriggered: { + if (_pairCnt > 0.01) { + _pairCnt -= 0.1 + if (_pairCnt <= 0.01) { + nrfStartBtn.enabled = true + _pairCnt = 0 + } + } + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true; contentWidth: width; contentHeight: genCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + ColumnLayout { + id: genCol + width: parent.width + spacing: 4 + Item { Layout.preferredHeight: 1 } + } + } + + // NRF Pairing GroupBox (at bottom, matching PageAppNrf layout) + GroupBox { + title: "NRF Pairing" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + spacing: 4 + + Button { + id: nrfStartBtn + icon.source: "qrc" + Utility.getThemePath() + "icons/Circled Play-96.png" + ToolTip.text: "Start Pairing"; ToolTip.visible: hovered + onClicked: { + mCommands.pairNrf(Math.round(nrfTimeBox.realValue * 1000)) + _pairRunning = true + } + } + + SpinBox { + id: nrfTimeBox + from: 10; to: 1000; value: 100; stepSize: 10 + editable: true + property double realValue: value / 10.0 + textFromValue: function(v) { return "Time: " + (v / 10.0).toFixed(1) + " s" } + valueFromText: function(t) { return Math.round(parseFloat(t.replace("Time: ", "").replace(" s", "")) * 10) || 100 } + ToolTip.text: "Stay in pairing mode for this amount of time" + ToolTip.visible: nrfTimeHover.hovered; ToolTip.delay: 500 + HoverHandler { id: nrfTimeHover } + } + + Label { + text: _pairCnt.toFixed(1) + font.bold: true + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: 40 + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } + padding: 4 + } + } + } + } + + function loadSubgroup(parentCol, subgroup) { + var params = mAppConf.getParamsFromSubgroup("nrf", subgroup) + for (var p = 0; p < params.length; p++) { + var paramName = params[p] + if (paramName.indexOf("::sep::") === 0) { + var sep = editors.createSeparator(parentCol, paramName.substring(7)) + if (sep) _dynamicItems.push(sep) + continue + } + var e = editors.createEditorApp(parentCol, paramName) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: { + loadSubgroup(genCol, "general") + } +} diff --git a/desktop/AppNunchukPage.qml b/desktop/AppNunchukPage.qml new file mode 100644 index 000000000..49b2765d4 --- /dev/null +++ b/desktop/AppNunchukPage.qml @@ -0,0 +1,264 @@ +/* + Desktop AppNunchukPage — exact parity with PageAppNunchuk widget. + Layout: VBoxLayout + TabWidget (General | Throttle Curve) + General: ParamTable of "VESC Remote" / "general" + DisplayPercentage (h=30) + Throttle: ParamTable (fixed h~100) + throttle curve plot + NrfPair GroupBox at bottom +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtGraphs +import Vedder.vesc + +Item { + id: root + + property ConfigParams mAppConf: VescIf.appConfig() + property Commands mCommands: VescIf.commands() + property var _dynamicItems: [] + + // NRF pairing state + property double _pairCnt: 0.0 + property bool _pairRunning: false + + ParamEditors { id: editors } + + function updateThrottleCurve() { + var mode = mAppConf.getParamEnum("app_chuk_conf.throttle_exp_mode") + var valAcc = mAppConf.getParamDouble("app_chuk_conf.throttle_exp") + var valBrk = mAppConf.getParamDouble("app_chuk_conf.throttle_exp_brake") + + for (var si = throttlePlot.count - 1; si >= 0; si--) + throttlePlot.removeSeries(throttlePlot.seriesAt(si)) + + var series = throttlePlot.createSeries(GraphsView.SeriesTypeLine, "Throttle Curve") + series.color = Utility.getAppHexColor("plot_graph1") + series.width = 1.5 + + for (var i = -1.0; i <= 1.0001; i += 0.002) + series.append(i, Utility.throttle_curve(i, valAcc, valBrk, mode)) + + throttleAxisX.min = -1.0; throttleAxisX.max = 1.0 + var yMin = -1.0, yMax = 1.0 + for (var j = 0; j < series.count; j++) { + var pt = series.at(j) + if (pt.y < yMin) yMin = pt.y + if (pt.y > yMax) yMax = pt.y + } + var pad = (yMax - yMin) * 0.05 + throttleAxisY.min = yMin - pad + throttleAxisY.max = yMax + pad + } + + Connections { + target: mAppConf + function onParamChangedDouble(src, name, newParam) { + if (name === "app_chuk_conf.throttle_exp" || name === "app_chuk_conf.throttle_exp_brake") + updateThrottleCurve() + } + function onParamChangedEnum(src, name, newParam) { + if (name === "app_chuk_conf.throttle_exp_mode") + updateThrottleCurve() + } + } + + Connections { + target: mCommands + function onDecodedChukReceived(value) { + var p = value * 100.0 + chukBar.value = p / 100.0 + chukLabel.text = p.toFixed(1) + " %" + } + + function onNrfPairingRes(res) { + if (!_pairRunning) return + if (res === 0) { // NRF_PAIR_STARTED + _pairCnt = nrfTimeBox.value + nrfStartBtn.enabled = false + } else if (res === 1) { // NRF_PAIR_OK + nrfStartBtn.enabled = true + _pairCnt = 0 + VescIf.emitStatusMessage("Pairing NRF Successful", true) + _pairRunning = false + } else if (res === 2) { // NRF_PAIR_FAIL + nrfStartBtn.enabled = true + _pairCnt = 0 + VescIf.emitStatusMessage("Pairing NRF Timed Out", false) + _pairRunning = false + } + } + } + + Timer { + interval: 100; running: true; repeat: true + onTriggered: { + if (_pairCnt > 0.01) { + _pairCnt -= 0.1 + if (_pairCnt <= 0.01) { + nrfStartBtn.enabled = true + _pairCnt = 0 + } + } + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TabBar { + id: tabBar + Layout.fillWidth: true + TabButton { text: "General"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Throttle Curve"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: tabBar.currentIndex + + // Tab 0: General + DisplayPercentage + Flickable { + clip: true; contentWidth: width; contentHeight: genLayout.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + ColumnLayout { + id: genLayout + width: parent.width + spacing: 4 + + Item { Layout.preferredHeight: 1 } + + ColumnLayout { + id: genCol + Layout.fillWidth: true + spacing: 4 + } + + // DisplayPercentage equivalent (dual, min height 30) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 30 + color: "transparent" + border.width: 1 + border.color: palette.mid + radius: 2 + + RowLayout { + anchors.fill: parent + anchors.margins: 2 + + ProgressBar { + id: chukBar + Layout.fillWidth: true + from: -1; to: 1; value: 0 + } + + Label { + id: chukLabel + text: "-- %" + font.pointSize: 12 + horizontalAlignment: Text.AlignRight + Layout.preferredWidth: 80 + } + } + } + } + } + + // Tab 1: Throttle Curve + ColumnLayout { + spacing: 0 + + Flickable { + Layout.fillWidth: true + Layout.preferredHeight: 100; Layout.maximumHeight: 100 + clip: true; contentWidth: width; contentHeight: tcCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + ColumnLayout { id: tcCol; width: parent.width; spacing: 4; Item { Layout.preferredHeight: 1 } } + } + + GraphsView { + id: throttlePlot + Layout.fillWidth: true + Layout.fillHeight: true + + theme: GraphsTheme { + colorScheme: Utility.isDarkMode() ? GraphsTheme.ColorScheme.Dark : GraphsTheme.ColorScheme.Light + plotAreaBackgroundColor: Utility.getAppHexColor("plotBackground") + grid.mainColor: Utility.isDarkMode() ? "#444444" : "#cccccc" + } + + axisX: ValueAxis { id: throttleAxisX; min: -1; max: 1; titleText: "Throttle Value" } + axisY: ValueAxis { id: throttleAxisY; min: -1; max: 1; titleText: "Output Value" } + } + } + } + + // NRF Pairing GroupBox (below TabWidget, matching original layout) + GroupBox { + title: "NRF Pairing" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + spacing: 4 + + Button { + id: nrfStartBtn + icon.source: "qrc" + Utility.getThemePath() + "icons/Circled Play-96.png" + ToolTip.text: "Start Pairing"; ToolTip.visible: hovered + onClicked: { + mCommands.pairNrf(Math.round(nrfTimeBox.value * 1000)) + _pairRunning = true + } + } + + SpinBox { + id: nrfTimeBox + from: 10; to: 1000; value: 100; stepSize: 10 + editable: true + property double realValue: value / 10.0 + textFromValue: function(v) { return "Time: " + (v / 10.0).toFixed(1) + " s" } + valueFromText: function(t) { return Math.round(parseFloat(t.replace("Time: ", "").replace(" s", "")) * 10) || 100 } + ToolTip.text: "Stay in pairing mode for this amount of time" + ToolTip.visible: nrfTimeHover.hovered; ToolTip.delay: 500 + HoverHandler { id: nrfTimeHover } + } + + Label { + text: _pairCnt.toFixed(1) + font.bold: true + horizontalAlignment: Text.AlignHCenter + Layout.preferredWidth: 40 + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } + padding: 4 + } + } + } + } + + function loadSubgroup(parentCol, subgroup) { + var params = mAppConf.getParamsFromSubgroup("VESC Remote", subgroup) + for (var p = 0; p < params.length; p++) { + var paramName = params[p] + if (paramName.indexOf("::sep::") === 0) { + var sep = editors.createSeparator(parentCol, paramName.substring(7)) + if (sep) _dynamicItems.push(sep) + continue + } + var e = editors.createEditorApp(parentCol, paramName) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: { + loadSubgroup(genCol, "general") + loadSubgroup(tcCol, "throttle curve") + } +} diff --git a/desktop/AppPasPage.qml b/desktop/AppPasPage.qml new file mode 100644 index 000000000..28cda690a --- /dev/null +++ b/desktop/AppPasPage.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Controls +import Vedder.vesc + +ParamGroupPage { + configParams: VescIf.appConfig() + groupName: "PAS" +} diff --git a/desktop/AppPpmPage.qml b/desktop/AppPpmPage.qml new file mode 100644 index 000000000..03be8d142 --- /dev/null +++ b/desktop/AppPpmPage.qml @@ -0,0 +1,292 @@ +/* + Desktop AppPpmPage — exact parity with PageAppPpm widget. + Layout: TabWidget (General | Mapping | Throttle Curve) + General: ParamTable of "PPM" / "general" + Mapping: ParamTable of "PPM" / "mapping" + PPM Mapping GroupBox + Throttle: ParamTable (fixed height ~100) + throttle curve plot +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtGraphs +import Vedder.vesc + +Item { + id: root + + property ConfigParams mAppConf: VescIf.appConfig() + property Commands mCommands: VescIf.commands() + property var _dynamicItems: [] + property bool _mapResetDone: true + property double _mapMinVal: 0.0 + property double _mapMaxVal: 0.0 + property double _mapCenterVal: 0.0 + + ParamEditors { id: editors } + + function updateThrottleCurve() { + var mode = mAppConf.getParamEnum("app_ppm_conf.throttle_exp_mode") + var valAcc = mAppConf.getParamDouble("app_ppm_conf.throttle_exp") + var valBrk = mAppConf.getParamDouble("app_ppm_conf.throttle_exp_brake") + + for (var si = throttlePlot.count - 1; si >= 0; si--) + throttlePlot.removeSeries(throttlePlot.seriesAt(si)) + + var series = throttlePlot.createSeries(GraphsView.SeriesTypeLine, "Throttle Curve") + series.color = Utility.getAppHexColor("plot_graph1") + series.width = 1.5 + + for (var i = -1.0; i <= 1.0001; i += 0.002) + series.append(i, Utility.throttle_curve(i, valAcc, valBrk, mode)) + + // Auto-rescale + throttleAxisX.min = -1.0; throttleAxisX.max = 1.0 + var yMin = -1.0, yMax = 1.0 + for (var j = 0; j < series.count; j++) { + var pt = series.at(j) + if (pt.y < yMin) yMin = pt.y + if (pt.y > yMax) yMax = pt.y + } + var pad = (yMax - yMin) * 0.05 + throttleAxisY.min = yMin - pad + throttleAxisY.max = yMax + pad + } + + Connections { + target: mAppConf + function onParamChangedDouble(src, name, newParam) { + if (name === "app_ppm_conf.throttle_exp" || name === "app_ppm_conf.throttle_exp_brake") + updateThrottleCurve() + } + function onParamChangedEnum(src, name, newParam) { + if (name === "app_ppm_conf.throttle_exp_mode") + updateThrottleCurve() + } + } + + Connections { + target: mCommands + function onDecodedPpmReceived(value, lastLen) { + // PPM mapping display update + var minNow = mAppConf.getParamDouble("app_ppm_conf.pulse_start") + var maxNow = mAppConf.getParamDouble("app_ppm_conf.pulse_end") + var centerNow = mAppConf.getParamDouble("app_ppm_conf.pulse_center") + + // VESC Tool preview (using current config mapping) + var p + if (dualBox.checked) { + if (lastLen < centerNow) + p = Utility.map(lastLen, minNow, centerNow, -100.0, 0.0) + else + p = Utility.map(lastLen, centerNow, maxNow, 0.0, 100.0) + } else { + p = Utility.map(lastLen, minNow, maxNow, 0.0, 100.0) + } + ppmToolBar.value = Math.max(-100, Math.min(100, p)) / 100.0 + ppmToolLabel.text = lastLen.toFixed(4) + " ms (" + p.toFixed(1) + " %)" + + // VESC Firmware preview + var p2 = dualBox.checked ? value * 100.0 : (value + 1.0) * 50.0 + ppmFwBar.value = Math.max(-100, Math.min(100, p2)) / 100.0 + ppmFwLabel.text = lastLen.toFixed(4) + " ms (" + p2.toFixed(1) + " %)" + + // Auto min/max tracking + if (_mapResetDone) { + _mapResetDone = false + _mapMinVal = lastLen + _mapMaxVal = lastLen + } else { + if (lastLen < _mapMinVal && lastLen > 1e-3) _mapMinVal = lastLen + if (lastLen > _mapMaxVal) _mapMaxVal = lastLen + } + + var range = _mapMaxVal - _mapMinVal + var pos = lastLen - _mapMinVal + if (pos > range / 4.0 && pos < (3.0 * range) / 4.0) + _mapCenterVal = lastLen + else + _mapCenterVal = range / 2.0 + _mapMinVal + } + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TabBar { + id: tabBar + Layout.fillWidth: true + TabButton { text: "General"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Mapping"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Throttle Curve"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: tabBar.currentIndex + + // Tab 0: General + Flickable { + clip: true; contentWidth: width; contentHeight: genCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + ColumnLayout { id: genCol; width: parent.width; spacing: 4; Item { Layout.preferredHeight: 1 } } + } + + // Tab 1: Mapping + Flickable { + clip: true; contentWidth: width; contentHeight: mapLayout.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + ColumnLayout { + id: mapLayout + width: parent.width + spacing: 4 + + Item { Layout.preferredHeight: 1 } + + // Dynamic params inserted here via mapCol + ColumnLayout { + id: mapCol + Layout.fillWidth: true + spacing: 4 + } + + // PPM Pulselength Mapping GroupBox + GroupBox { + title: "PPM Pulselength Mapping" + Layout.fillWidth: true + + ColumnLayout { + anchors.fill: parent + spacing: 4 + + RowLayout { + spacing: 4 + CheckBox { + id: dualBox + text: "Use Centered Control" + ToolTip.text: "Show centered graph, which is how the centered control modes interpret the throttle." + ToolTip.visible: dualHover.hovered; ToolTip.delay: 500 + HoverHandler { id: dualHover } + } + Button { + text: "Reset" + ToolTip.text: "Reset min and max"; ToolTip.visible: hovered + onClicked: { _mapResetDone = true; _mapMinVal = 0; _mapMaxVal = 0 } + } + Button { + icon.source: "qrc" + Utility.getThemePath() + "icons/Ok-96.png" + text: "Apply" + ToolTip.text: "Apply min, max and center to VESC Tool configuration"; ToolTip.visible: hovered + onClicked: { + if (_mapMaxVal > 1e-10) { + mAppConf.updateParamDouble("app_ppm_conf.pulse_start", _mapMinVal) + mAppConf.updateParamDouble("app_ppm_conf.pulse_end", _mapMaxVal) + mAppConf.updateParamDouble("app_ppm_conf.pulse_center", _mapCenterVal) + VescIf.emitStatusMessage("Start, End and Center Pulselengths Applied", true) + } else { + VescIf.emitStatusMessage("Applying Pulselengths Failed", false) + } + } + } + } + + RowLayout { + spacing: 2 + Label { text: "Min: " + _mapMinVal.toFixed(4) + " ms"; Layout.fillWidth: true + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } + padding: 4 + } + Label { text: "Max: " + _mapMaxVal.toFixed(4) + " ms"; Layout.fillWidth: true + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } + padding: 4 + } + Label { text: "Center: " + _mapCenterVal.toFixed(4) + " ms"; Layout.fillWidth: true + background: Rectangle { border.width: 1; border.color: palette.mid; radius: 2; color: "transparent" } + padding: 4 + } + } + + GridLayout { + columns: 2 + Layout.fillWidth: true + columnSpacing: 4; rowSpacing: 2 + + Label { text: "VESC Tool" } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + ProgressBar { id: ppmToolBar; Layout.fillWidth: true; from: -1; to: 1; value: 0; Layout.preferredHeight: 25 } + Label { id: ppmToolLabel; text: "-- ms (-- %)"; font.pointSize: 11; color: Utility.getAppHexColor("lightText") } + } + + Label { text: "VESC Firmware" } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + ProgressBar { id: ppmFwBar; Layout.fillWidth: true; from: -1; to: 1; value: 0; Layout.preferredHeight: 25 } + Label { id: ppmFwLabel; text: "-- ms (-- %)"; font.pointSize: 11; color: Utility.getAppHexColor("lightText") } + } + } + } + } + } + } + + // Tab 2: Throttle Curve + ColumnLayout { + spacing: 0 + + // Params (fixed height ~100, matching original) + Flickable { + Layout.fillWidth: true + Layout.preferredHeight: 100 + Layout.maximumHeight: 100 + clip: true; contentWidth: width; contentHeight: tcCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + ColumnLayout { id: tcCol; width: parent.width; spacing: 4; Item { Layout.preferredHeight: 1 } } + } + + // Throttle curve plot + GraphsView { + id: throttlePlot + Layout.fillWidth: true + Layout.fillHeight: true + + theme: GraphsTheme { + colorScheme: Utility.isDarkMode() ? GraphsTheme.ColorScheme.Dark : GraphsTheme.ColorScheme.Light + plotAreaBackgroundColor: Utility.getAppHexColor("plotBackground") + grid.mainColor: Utility.isDarkMode() ? "#444444" : "#cccccc" + } + + axisX: ValueAxis { id: throttleAxisX; min: -1; max: 1; titleText: "Throttle Value" } + axisY: ValueAxis { id: throttleAxisY; min: -1; max: 1; titleText: "Output Value" } + } + } + } + } + + function loadSubgroup(parentCol, subgroup) { + var params = mAppConf.getParamsFromSubgroup("PPM", subgroup) + for (var p = 0; p < params.length; p++) { + var paramName = params[p] + if (paramName.indexOf("::sep::") === 0) { + var sep = editors.createSeparator(parentCol, paramName.substring(7)) + if (sep) _dynamicItems.push(sep) + continue + } + var e = editors.createEditorApp(parentCol, paramName) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: { + loadSubgroup(genCol, "general") + loadSubgroup(mapCol, "mapping") + loadSubgroup(tcCol, "throttle curve") + } +} diff --git a/desktop/AppUartPage.qml b/desktop/AppUartPage.qml new file mode 100644 index 000000000..136805942 --- /dev/null +++ b/desktop/AppUartPage.qml @@ -0,0 +1,8 @@ +import QtQuick +import QtQuick.Controls +import Vedder.vesc + +ParamGroupPage { + configParams: VescIf.appConfig() + groupName: "UART" +} diff --git a/desktop/BmsPage.qml b/desktop/BmsPage.qml new file mode 100644 index 000000000..5ab45b9e9 --- /dev/null +++ b/desktop/BmsPage.qml @@ -0,0 +1,201 @@ +/* + Desktop BmsPage — native implementation matching the original widget page. + Features: Cell voltage bar chart, temperature bar chart, value table, + control buttons (balance on/off, charge enable/disable, reset counters). +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtGraphs +import Vedder.vesc + +Item { + id: bmsPage + + property Commands mCommands: VescIf.commands() + + SplitView { + anchors.fill: parent + orientation: Qt.Horizontal + + // Left: Charts + ColumnLayout { + SplitView.fillWidth: true + SplitView.minimumWidth: 400 + spacing: 4 + + TabBar { + id: bmsTabBar + Layout.fillWidth: true + TabButton { text: "Cell Voltages"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Temperatures"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: bmsTabBar.currentIndex + + // Cell voltage chart + GraphsView { + id: cellChart + theme: GraphsTheme { + colorScheme: Utility.isDarkMode() ? GraphsTheme.ColorScheme.Dark : GraphsTheme.ColorScheme.Light + plotAreaBackgroundColor: Utility.getAppHexColor("plotBackground") + grid.mainColor: Utility.isDarkMode() ? "#444444" : "#cccccc" + } + + axisX: ValueAxis { id: cellAxisX; min: 0; max: 13; titleText: "Cell" } + axisY: ValueAxis { id: cellAxisY; min: 2.5; max: 4.4; titleText: "Voltage (V)" } + + BarSeries { + id: cellBarSeries + barWidth: 0.6 + } + } + + // Temperature chart + GraphsView { + id: tempChart + theme: GraphsTheme { + colorScheme: Utility.isDarkMode() ? GraphsTheme.ColorScheme.Dark : GraphsTheme.ColorScheme.Light + plotAreaBackgroundColor: Utility.getAppHexColor("plotBackground") + grid.mainColor: Utility.isDarkMode() ? "#444444" : "#cccccc" + } + + axisX: ValueAxis { id: tempAxisX; min: 0; max: 6; titleText: "Sensor" } + axisY: ValueAxis { id: tempAxisY; min: -20; max: 90; titleText: "Temperature (°C)" } + + BarSeries { + id: tempBarSeries + barWidth: 0.6 + } + } + } + + // Control buttons + RowLayout { + Layout.fillWidth: true + Layout.margins: 4 + spacing: 4 + + Button { text: "Balance On"; onClicked: mCommands.bmsForceBalance(true) } + Button { text: "Balance Off"; onClicked: mCommands.bmsForceBalance(false) } + Button { text: "Charge Enable"; onClicked: mCommands.bmsSetChargeAllowed(true) } + Button { text: "Charge Disable"; onClicked: mCommands.bmsSetChargeAllowed(false) } + Button { text: "Zero Current"; onClicked: mCommands.bmsZeroCurrentOffset() } + Button { text: "Reset Ah"; onClicked: mCommands.bmsResetCounters(true, false) } + Button { text: "Reset Wh"; onClicked: mCommands.bmsResetCounters(false, true) } + } + } + + // Right: Value table + ScrollView { + SplitView.preferredWidth: 280 + SplitView.minimumWidth: 200 + clip: true + + ColumnLayout { + width: parent.width + spacing: 2 + + Label { + text: "BMS Values" + font.bold: true + font.pointSize: 14 + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + Layout.bottomMargin: 4 + } + + Repeater { + id: valRepeater + model: ListModel { + id: valModel + ListElement { label: "V Total"; val: "--" } + ListElement { label: "V Cell Min"; val: "--" } + ListElement { label: "V Cell Max"; val: "--" } + ListElement { label: "V Cell Diff"; val: "--" } + ListElement { label: "V Charge"; val: "--" } + ListElement { label: "Current"; val: "--" } + ListElement { label: "Current IC"; val: "--" } + ListElement { label: "Ah Counter"; val: "--" } + ListElement { label: "Wh Counter"; val: "--" } + ListElement { label: "Power"; val: "--" } + ListElement { label: "SoC"; val: "--" } + ListElement { label: "SoH"; val: "--" } + ListElement { label: "T Cell Max"; val: "--" } + ListElement { label: "T PCB Max"; val: "--" } + ListElement { label: "Humidity"; val: "--" } + ListElement { label: "Pressure"; val: "--" } + ListElement { label: "Ah Chg Total"; val: "--" } + ListElement { label: "Wh Chg Total"; val: "--" } + ListElement { label: "Ah Dis Total"; val: "--" } + ListElement { label: "Wh Dis Total"; val: "--" } + ListElement { label: "Status"; val: "--" } + } + + RowLayout { + Layout.fillWidth: true + spacing: 4 + Label { + text: model.label + ":" + font.pointSize: 11 + Layout.preferredWidth: 100 + color: Utility.getAppHexColor("lightText") + } + Label { + text: model.val + font.pointSize: 11 + font.family: "DejaVu Sans Mono" + Layout.fillWidth: true + color: Utility.getAppHexColor("lightText") + } + } + } + } + } + } + + Connections { + target: mCommands + function onBmsValuesRx(val) { + // Update value table + var vcMin = 0, vcMax = 0 + if (val.v_cells.length > 0) { + vcMin = val.v_cells[0] + vcMax = vcMin + for (var i = 0; i < val.v_cells.length; i++) { + if (val.v_cells[i] < vcMin) vcMin = val.v_cells[i] + if (val.v_cells[i] > vcMax) vcMax = val.v_cells[i] + } + } + + var idx = 0 + valModel.setProperty(idx++, "val", val.v_tot.toFixed(2) + " V") + valModel.setProperty(idx++, "val", vcMin.toFixed(3) + " V") + valModel.setProperty(idx++, "val", vcMax.toFixed(3) + " V") + valModel.setProperty(idx++, "val", (vcMax - vcMin).toFixed(3) + " V") + valModel.setProperty(idx++, "val", val.v_charge.toFixed(2) + " V") + valModel.setProperty(idx++, "val", val.i_in.toFixed(2) + " A") + valModel.setProperty(idx++, "val", val.i_in_ic.toFixed(2) + " A") + valModel.setProperty(idx++, "val", val.ah_cnt.toFixed(3) + " Ah") + valModel.setProperty(idx++, "val", val.wh_cnt.toFixed(3) + " Wh") + valModel.setProperty(idx++, "val", (val.i_in_ic * val.v_tot).toFixed(3) + " W") + valModel.setProperty(idx++, "val", (val.soc * 100).toFixed(0) + " %") + valModel.setProperty(idx++, "val", (val.soh * 100).toFixed(0) + " %") + valModel.setProperty(idx++, "val", val.temp_cells_highest.toFixed(2) + " °C") + valModel.setProperty(idx++, "val", val.temp_hum_sensor.toFixed(2) + " °C") + valModel.setProperty(idx++, "val", val.humidity.toFixed(2) + " % (" + val.temp_hum_sensor.toFixed(2) + " °C)") + valModel.setProperty(idx++, "val", val.pressure.toFixed(0) + " Pa") + valModel.setProperty(idx++, "val", val.ah_cnt_chg_total.toFixed(3) + " Ah") + valModel.setProperty(idx++, "val", val.wh_cnt_chg_total.toFixed(3) + " Wh") + valModel.setProperty(idx++, "val", val.ah_cnt_dis_total.toFixed(3) + " Ah") + valModel.setProperty(idx++, "val", val.wh_cnt_dis_total.toFixed(3) + " Wh") + valModel.setProperty(idx++, "val", val.status) + + // Update cell chart axis + cellAxisX.max = val.v_cells.length + 1 + } + } +} diff --git a/desktop/CanAnalyzerPage.qml b/desktop/CanAnalyzerPage.qml new file mode 100644 index 000000000..255d32ab0 --- /dev/null +++ b/desktop/CanAnalyzerPage.qml @@ -0,0 +1,261 @@ +/* + Desktop CanAnalyzerPage — faithful recreation of the original PageCanAnalyzer widget. + Features: CAN baudrate update, CAN mode/baudrate params, message table, + send frame controls (ID + 8 data bytes), clear/auto-scroll. +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Vedder.vesc + +Item { + id: canAnalyzerPage + + property Commands mCommands: VescIf.commands() + property ConfigParams mAppConf: VescIf.appConfig() + property var _dynamicItems: [] + + ParamEditors { + id: editors + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 4 + + // Update CAN Baudrate group + GroupBox { + title: "Update CAN Baudrate" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + spacing: 8 + + Label { text: "KBits/sec" } + ComboBox { + id: canBaudBox + textRole: "display" + model: Utility.stringListModel(["125", "250", "500", "1000", "10", "20", "50", "75", "100"]) + currentIndex: 2 + } + Button { + text: "Start Update" + onClicked: { + VescIf.emitMessageDialog("Update CAN Baudrate", + "This is going to update the CAN-bus baudrate on all connected " + + "VESC-based devices. This is only supported in firmware 6.06 or " + + "later. If the update fails the CAN-bus might become unusable until " + + "it is reconfigured manually. Do you want to continue?", + false, false) + } + } + Item { Layout.fillWidth: true } + } + } + + // CAN mode/baudrate params + Flickable { + Layout.fillWidth: true + Layout.preferredHeight: paramCol.height + 8 + Layout.maximumHeight: 120 + clip: true; contentWidth: width; contentHeight: paramCol.height + 8 + flickableDirection: Flickable.VerticalFlick + ColumnLayout { + id: paramCol; width: parent.width; spacing: 4 + Item { Layout.preferredHeight: 1 } + } + } + + // Send CAN Frame group + GroupBox { + title: "Send CAN Frame" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + spacing: 4 + + Label { text: "ID:" } + TextField { + id: sendIdEdit + Layout.preferredWidth: 100 + text: "0x00000000" + placeholderText: "0x or decimal" + } + + ComboBox { + id: sendExtBox + textRole: "display" + model: Utility.stringListModel(["Standard", "Extended"]) + currentIndex: 0 + } + + SpinBox { id: d0Box; from: -1; to: 255; value: 0; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + SpinBox { id: d1Box; from: -1; to: 255; value: -1; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + SpinBox { id: d2Box; from: -1; to: 255; value: -1; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + SpinBox { id: d3Box; from: -1; to: 255; value: -1; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + SpinBox { id: d4Box; from: -1; to: 255; value: -1; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + SpinBox { id: d5Box; from: -1; to: 255; value: -1; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + SpinBox { id: d6Box; from: -1; to: 255; value: -1; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + SpinBox { id: d7Box; from: -1; to: 255; value: -1; editable: true; Layout.preferredWidth: 70 + textFromValue: function(v) { return v < 0 ? "--" : "0x" + v.toString(16).toUpperCase().padStart(2, "0") } + valueFromText: function(t) { if (t === "--") return -1; return parseInt(t.replace(/0[xX]/, ""), 16) } } + + Button { + text: "Send" + onClicked: { + var vals = [d0Box.value, d1Box.value, d2Box.value, d3Box.value, + d4Box.value, d5Box.value, d6Box.value, d7Box.value] + var bytes = [] + for (var i = 0; i < 8; i++) { + if (vals[i] >= 0) bytes.push(vals[i]) + else break + } + + var idTxt = sendIdEdit.text.toLowerCase().replace(" ", "") + var id = 0 + var ok = false + if (idTxt.startsWith("0x")) { + id = parseInt(idTxt.substring(2), 16) + ok = !isNaN(id) + } else { + id = parseInt(idTxt, 10) + ok = !isNaN(id) + } + + if (ok) { + mCommands.forwardCanFrame(bytes, id, sendExtBox.currentIndex === 1) + } else { + VescIf.emitMessageDialog("Send CAN", + "Unable to parse ID. ID must be a decimal number, or " + + "a hexadecimal number starting with 0x", false, false) + } + } + } + } + } + + // Message Table controls + RowLayout { + Layout.fillWidth: true + spacing: 8 + CheckBox { + id: autoScrollBox + text: "Auto-scroll" + checked: true + } + Button { + text: "Clear" + onClicked: messageModel.clear() + } + Item { Layout.fillWidth: true } + Label { + text: messageModel.count + " messages" + color: Utility.getAppHexColor("disabledText") + } + } + + // Message list + ListView { + id: msgListView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: ListModel { id: messageModel } + headerPositioning: ListView.OverlayHeader + + header: Rectangle { + width: msgListView.width + height: 28 + z: 2 + color: Utility.getAppHexColor("normalBackground") + RowLayout { + anchors.fill: parent + anchors.leftMargin: 4 + spacing: 0 + Label { text: "Ext"; Layout.preferredWidth: 50; font.bold: true; font.pointSize: 11 } + Label { text: "ID"; Layout.preferredWidth: 110; font.bold: true; font.pointSize: 11 } + Label { text: "Len"; Layout.preferredWidth: 40; font.bold: true; font.pointSize: 11 } + Label { text: "Data"; Layout.fillWidth: true; font.bold: true; font.pointSize: 11 } + } + } + + delegate: Rectangle { + width: msgListView.width + height: 24 + color: index % 2 === 0 ? "transparent" : Utility.getAppHexColor("normalBackground") + RowLayout { + anchors.fill: parent + anchors.leftMargin: 4 + spacing: 0 + Label { text: model.ext; Layout.preferredWidth: 50; font.pointSize: 11 } + Label { text: model.canId; Layout.preferredWidth: 110; font.pointSize: 11; font.family: "monospace" } + Label { text: model.len; Layout.preferredWidth: 40; font.pointSize: 11 } + Label { text: model.data; Layout.fillWidth: true; font.pointSize: 11; font.family: "monospace" } + } + } + + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + } + } + + Connections { + target: mCommands + function onCanFrameRx(data, id, isExtended) { + if (messageModel.count > 3999) { + messageModel.remove(0) + } + // data is a QByteArray exposed as ArrayBuffer in QML + var view = new Uint8Array(data) + var hexBytes = [] + for (var i = 0; i < view.length; i++) { + hexBytes.push("0x" + view[i].toString(16).toUpperCase().padStart(2, "0")) + } + messageModel.append({ + ext: isExtended ? "Yes" : "No", + canId: "0x" + id.toString(16).toUpperCase().padStart(8, "0"), + len: view.length.toString(), + data: hexBytes.join(" ") + }) + if (autoScrollBox.checked) { + msgListView.positionViewAtEnd() + } + } + } + + function reloadParams() { + for (var d = 0; d < _dynamicItems.length; d++) { + if (_dynamicItems[d]) _dynamicItems[d].destroy() + } + _dynamicItems = [] + + var paramNames = ["can_mode", "can_baud_rate"] + for (var p = 0; p < paramNames.length; p++) { + var e = editors.createEditorApp(paramCol, paramNames[p]) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: reloadParams() + + Connections { + target: mAppConf + function onUpdated() { reloadParams() } + } +} diff --git a/desktop/ConnectionPage.qml b/desktop/ConnectionPage.qml new file mode 100644 index 000000000..203a00e44 --- /dev/null +++ b/desktop/ConnectionPage.qml @@ -0,0 +1,1157 @@ +/* + Desktop ConnectionPage — faithful replica of PageConnection (widgets). + Tabs: "(USB-)Serial", "CAN bus", "TCP", "UDP", "Bluetooth LE", "TCP Hub" + Below tabs: CAN Forward group, Autoconnect button, status label. +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Vedder.vesc + +Item { + id: root + + property Commands mCommands: VescIf.commands() + + // ---- Helper: icon-only button (Fixed size, icon only, no text) ---- + component IconButton: Button { + implicitWidth: implicitHeight + display: AbstractButton.IconOnly + } + + // ---- 20ms timer matching original timerSlot ---- + Timer { + id: pollTimer + interval: 20 + running: true + repeat: true + onTriggered: { + // Update status label + var portName = VescIf.getConnectedPortName() + if (portName !== statusLabel.text) { + statusLabel.text = portName + } + + // Sync CAN fwd button with commands state + if (canFwdButton.checked !== mCommands.getSendCan()) { + canFwdButton.checked = mCommands.getSendCan() + } + + // TCP server info + var tcpIpTxt = "Server IPs\n" + var tcpClientTxt = "Connected Clients\n" + if (VescIf.tcpServerIsRunning()) { + var addrs = Utility.getNetworkAddresses() + for (var i = 0; i < addrs.length; i++) { + tcpIpTxt += addrs[i] + "\n" + } + if (VescIf.tcpServerIsClientConnected()) { + tcpClientTxt += VescIf.tcpServerClientIp() + } + } else { + tcpServerPortBox.enabled = true + } + if (tcpServerAddressesEdit.text !== tcpIpTxt) { + tcpServerAddressesEdit.text = tcpIpTxt + } + if (tcpServerClientsEdit.text !== tcpClientTxt) { + tcpServerClientsEdit.text = tcpClientTxt + } + + // UDP server info + var udpIpTxt = "Server IPs\n" + var udpClientTxt = "Connected Clients\n" + if (VescIf.udpServerIsRunning()) { + var uAddrs = Utility.getNetworkAddresses() + for (var j = 0; j < uAddrs.length; j++) { + udpIpTxt += uAddrs[j] + "\n" + } + if (VescIf.udpServerIsClientConnected()) { + udpClientTxt += VescIf.udpServerClientIp() + } + } else { + udpServerPortBox.enabled = true + } + if (udpServerAddressesEdit.text !== udpIpTxt) { + udpServerAddressesEdit.text = udpIpTxt + } + if (udpServerClientsEdit.text !== udpClientTxt) { + udpServerClientsEdit.text = udpClientTxt + } + } + } + + // ---- Connections ---- + Connections { + target: mCommands + function onPingCanRx(devs, isTimeout) { + canRefreshButton.enabled = true + canFwdBox.model = [] + var items = [] + for (var i = 0; i < devs.length; i++) { + items.push({"text": "VESC " + devs[i], "value": devs[i]}) + } + canFwdModel.clear() + for (var k = 0; k < items.length; k++) { + canFwdModel.append(items[k]) + } + } + } + + Connections { + target: VescIf.bleDevice() + function onScanDone(devs, done) { + if (done) { + bleScanButton.enabled = true + } + + bleDevBox.model = [] + var vescItems = [] + var otherItems = [] + + for (var addr in devs) { + var devName = devs[addr] + var setName = VescIf.getBleName(addr) + var displayName + + if (setName !== "") { + displayName = setName + " [" + addr + "]" + vescItems.unshift({"text": displayName, "addr": addr}) + } else if (devName.indexOf("VESC") >= 0) { + displayName = devName + " [" + addr + "]" + vescItems.unshift({"text": displayName, "addr": addr}) + } else { + displayName = devName + " [" + addr + "]" + otherItems.push({"text": displayName, "addr": addr}) + } + } + + bleDevModel.clear() + for (var i = 0; i < vescItems.length; i++) { + bleDevModel.append(vescItems[i]) + } + for (var j = 0; j < otherItems.length; j++) { + bleDevModel.append(otherItems[j]) + } + bleDevBox.currentIndex = 0 + } + } + + Connections { + target: VescIf + ignoreUnknownSignals: true + function onCANbusNewNode(node) { + canbusTargetModel.append({"text": node.toString(), "value": node}) + } + function onCANbusInterfaceListUpdated() { + canbusInterfaceBox.model = Utility.stringListModel(VescIf.listCANbusInterfaceNames()) + canbusInterfaceBox.currentIndex = 0 + } + function onPairingListUpdated() { + updatePairedList() + } + } + + function updatePairedList() { + pairedModel.clear() + var uuids = VescIf.getPairedUuids() + for (var i = 0; i < uuids.length; i++) { + pairedModel.append({"uuid": uuids[i]}) + } + if (pairedListView.count > 0) { + pairedListView.currentIndex = 0 + } + } + + // ---- Models ---- + ListModel { id: canFwdModel } + ListModel { id: bleDevModel } + ListModel { id: canbusTargetModel } + ListModel { id: pairedModel } + + // ---- Main layout ---- + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 4 + + TabBar { + id: connTabBar + Layout.fillWidth: true + + TabButton { text: "(USB-)Serial"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "CAN bus"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "TCP"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "UDP"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Bluetooth LE"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "TCP Hub"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: connTabBar.currentIndex + + // ========================================== + // Tab 0: (USB-)Serial + // ========================================== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 4 + + RowLayout { + Layout.fillWidth: true + + Label { text: "Port" } + + ComboBox { + id: serialPortBox + Layout.fillWidth: true + textRole: "name" + valueRole: "systemPath" + model: VescIf.listSerialPorts() + } + + SpinBox { + id: serialBaudBox + from: 0 + to: 3000000 + value: VescIf.getLastSerialBaud() + editable: true + property string prefix: "Baud: " + property string suffix: " bps" + textFromValue: function(value, locale) { + return prefix + value + suffix + } + valueFromText: function(text, locale) { + return parseInt(text.replace(prefix, "").replace(suffix, "")) + } + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Refresh-96.png" + ToolTip.visible: hovered + ToolTip.text: "Refresh serial port list" + onClicked: serialPortBox.model = VescIf.listSerialPorts() + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Disconnected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Disconnect" + onClicked: VescIf.disconnectPort() + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Connected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Connect" + onClicked: { + if (serialPortBox.currentValue !== undefined && serialPortBox.currentValue !== "") { + VescIf.connectSerial(serialPortBox.currentValue, serialBaudBox.value) + } + } + } + } + + Item { Layout.fillHeight: true } + } + } + + // ========================================== + // Tab 1: CAN bus + // ========================================== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 4 + + RowLayout { + Layout.fillWidth: true + + Label { text: "Interface" } + + ComboBox { + id: canbusInterfaceBox + textRole: "display" + model: Utility.stringListModel(VescIf.listCANbusInterfaceNames()) + } + + Label { text: "VESC ID" } + + ComboBox { + id: canbusTargetIdBox + Layout.fillWidth: true + model: canbusTargetModel + textRole: "text" + ToolTip.visible: hovered + ToolTip.text: "Discovered VESC nodes in the bus" + } + + SpinBox { + id: canbusBitrateBox + enabled: false + from: 0 + to: 1000 + value: typeof VescIf.getLastCANbusBitrate === "function" ? VescIf.getLastCANbusBitrate() : 500 + editable: false + property string prefix: "Bit rate: " + property string suffix: " kbit/s" + textFromValue: function(value, locale) { + return prefix + value + suffix + } + valueFromText: function(text, locale) { + return parseInt(text.replace(prefix, "").replace(suffix, "")) + } + ToolTip.visible: hovered + ToolTip.text: "This configuration is not supported by the socketcan backend. " + + "However it is possible to set the rate when configuring the CAN " + + "network interface using the ip link command." + } + + Button { + text: "Scan" + icon.source: "qrc" + Utility.getThemePath() + "icons/Refresh-96.png" + ToolTip.visible: hovered + ToolTip.text: "Discover VESC nodes in the CAN bus" + onClicked: { + VescIf.connectCANbus("socketcan", + canbusInterfaceBox.currentText, + canbusBitrateBox.value) + canbusTargetModel.clear() + VescIf.scanCANbus() + } + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Disconnected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Disconnect" + onClicked: VescIf.disconnectPort() + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Connected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Connect to a VESC node" + onClicked: { + if (canbusTargetIdBox.currentIndex >= 0) { + var node = canbusTargetModel.get(canbusTargetIdBox.currentIndex).value + VescIf.setCANbusReceiverID(node) + VescIf.connectCANbus("socketcan", + canbusInterfaceBox.currentText, + canbusBitrateBox.value) + } + } + } + } + + Item { Layout.fillHeight: true } + } + } + + // ========================================== + // Tab 2: TCP + // ========================================== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 4 + + GroupBox { + title: "TCP Client" + Layout.fillWidth: true + + GridLayout { + anchors.fill: parent + columns: 5 + columnSpacing: 6 + rowSpacing: 2 + + // Row 0: Address | serverEdit | portSpinBox | disconnect | connect + Label { text: "Address" } + TextField { + id: tcpServerEdit + Layout.fillWidth: true + text: VescIf.getLastTcpServer() + } + SpinBox { + id: tcpPortBox + from: 0; to: 65535 + value: VescIf.getLastTcpPort() + editable: true + property string prefix: "Port: " + textFromValue: function(v, l) { return prefix + v } + valueFromText: function(t, l) { return parseInt(t.replace(prefix, "")) } + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Disconnected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Disconnect" + onClicked: VescIf.disconnectPort() + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Connected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Connect" + onClicked: VescIf.connectTcp(tcpServerEdit.text, tcpPortBox.value) + } + + // Row 1: Detected Devices | detectBox | detectDisconnect | detectConnect + Label { text: "Detected Devices" } + ComboBox { + id: tcpDetectBox + Layout.fillWidth: true + Layout.columnSpan: 2 + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Disconnected-96.png" + onClicked: VescIf.disconnectPort() + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Connected-96.png" + onClicked: { + if (tcpDetectBox.currentIndex >= 0 && tcpDetectData.length > 0) { + var d = tcpDetectData[tcpDetectBox.currentIndex] + if (d) { + VescIf.connectTcp(d.ip, parseInt(d.port)) + } + } + } + } + } + } + + GroupBox { + title: "TCP Server" + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + + RowLayout { + CheckBox { + id: tcpServerEnableBox + text: "Enable TCP Server" + onToggled: { + if (checked) { + VescIf.tcpServerStart(tcpServerPortBox.value) + tcpServerPortBox.enabled = false + } else { + VescIf.tcpServerStop() + } + } + } + SpinBox { + id: tcpServerPortBox + Layout.fillWidth: true + from: 0; to: 65535 + value: 65102 + editable: true + property string prefix: "TCP Port: " + textFromValue: function(v, l) { return prefix + v } + valueFromText: function(t, l) { return parseInt(t.replace(prefix, "")) } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 80 + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + TextArea { + id: tcpServerAddressesEdit + readOnly: true + wrapMode: TextArea.Wrap + } + } + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + TextArea { + id: tcpServerClientsEdit + readOnly: true + wrapMode: TextArea.Wrap + } + } + } + } + } + } + } + + // ========================================== + // Tab 3: UDP + // ========================================== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 4 + + GroupBox { + title: "UDP Client" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + + Label { text: "Address" } + TextField { + id: udpServerEdit + Layout.fillWidth: true + text: VescIf.getLastUdpServer() + } + SpinBox { + id: udpPortBox + from: 0; to: 65535 + value: VescIf.getLastUdpPort() + editable: true + property string prefix: "Port: " + textFromValue: function(v, l) { return prefix + v } + valueFromText: function(t, l) { return parseInt(t.replace(prefix, "")) } + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Disconnected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Disconnect" + onClicked: VescIf.disconnectPort() + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Connected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Connect" + onClicked: VescIf.connectUdp(udpServerEdit.text, udpPortBox.value) + } + } + } + + GroupBox { + title: "UDP Server" + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + + RowLayout { + CheckBox { + id: udpServerEnableBox + text: "Enable UDP Server" + onToggled: { + if (checked) { + VescIf.udpServerStart(udpServerPortBox.value) + udpServerPortBox.enabled = false + } else { + VescIf.udpServerStop() + } + } + } + SpinBox { + id: udpServerPortBox + Layout.fillWidth: true + from: 0; to: 65535 + value: 65102 + editable: true + property string prefix: "UDP Port: " + textFromValue: function(v, l) { return prefix + v } + valueFromText: function(t, l) { return parseInt(t.replace(prefix, "")) } + } + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumHeight: 80 + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + TextArea { + id: udpServerAddressesEdit + readOnly: true + wrapMode: TextArea.Wrap + } + } + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + TextArea { + id: udpServerClientsEdit + readOnly: true + wrapMode: TextArea.Wrap + } + } + } + } + } + } + } + + // ========================================== + // Tab 4: Bluetooth LE + // ========================================== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 4 + + // BLE Device row + RowLayout { + Layout.fillWidth: true + + Label { text: "BLE Device" } + + ComboBox { + id: bleDevBox + Layout.fillWidth: true + model: bleDevModel + textRole: "text" + } + + IconButton { + id: bleScanButton + icon.source: "qrc" + Utility.getThemePath() + "icons/Refresh-96.png" + ToolTip.visible: hovered + ToolTip.text: "Refresh serial port list" + onClicked: { + VescIf.bleDevice().startScan() + bleScanButton.enabled = false + } + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Disconnected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Disconnect" + onClicked: VescIf.disconnectPort() + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Connected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Connect" + onClicked: { + if (bleDevBox.currentIndex >= 0 && bleDevModel.count > 0) { + VescIf.connectBle(bleDevModel.get(bleDevBox.currentIndex).addr) + } + } + } + } + + // Set Device Name row + RowLayout { + Layout.fillWidth: true + + Label { text: "Set Device Name" } + + TextField { + id: bleNameEdit + Layout.fillWidth: true + } + + Button { + text: "Set" + icon.source: "qrc" + Utility.getThemePath() + "icons/Ok-96.png" + onClicked: { + if (bleNameEdit.text !== "" && bleDevBox.currentIndex >= 0 && bleDevModel.count > 0) { + var addr = bleDevModel.get(bleDevBox.currentIndex).addr + VescIf.storeBleName(addr, bleNameEdit.text) + var newName = bleNameEdit.text + " [" + addr + "]" + bleDevModel.set(bleDevBox.currentIndex, {"text": newName, "addr": addr}) + } + } + } + } + + // VESC Devices group + GroupBox { + title: "VESC Devices" + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + anchors.fill: parent + + ListView { + id: pairedListView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: pairedModel + currentIndex: 0 + highlight: Rectangle { + color: Utility.getAppHexColor("lightAccent") + opacity: 0.3 + } + + delegate: ItemDelegate { + width: pairedListView.width + text: "UUID: " + uuid + highlighted: pairedListView.currentIndex === index + onClicked: pairedListView.currentIndex = index + } + + ScrollBar.vertical: ScrollBar {} + } + + ColumnLayout { + spacing: 3 + + Button { + text: "Pair" + icon.source: "qrc" + Utility.getThemePath() + "icons/Circled Play-96.png" + Layout.fillWidth: true + ToolTip.visible: hovered + ToolTip.text: "Pair the connected VESC" + onClicked: { + if (!VescIf.isPortConnected()) { + VescIf.emitMessageDialog("Pair VESC", + "You are not connected to the VESC. Connect in order to pair it.", + false, false) + return + } + if (mCommands.isLimitedMode()) { + VescIf.emitMessageDialog("Pair VESC", + "The fiwmare must be updated to pair this VESC.", + false, false) + return + } + pairConfirmDialog.open() + } + } + Button { + text: "Add" + icon.source: "qrc" + Utility.getThemePath() + "icons/Plus Math-96.png" + Layout.fillWidth: true + ToolTip.visible: hovered + ToolTip.text: "Add the connected VESC without pairing it." + onClicked: { + if (VescIf.isPortConnected()) { + VescIf.addPairedUuid(VescIf.getConnectedUuid()) + VescIf.storeSettings() + } else { + VescIf.emitMessageDialog("Add UUID", + "You are not connected to the VESC. Connect in order to add it.", + false, false) + } + } + } + Button { + text: "Add UUID" + icon.source: "qrc" + Utility.getThemePath() + "icons/Plus Math-96.png" + Layout.fillWidth: true + ToolTip.visible: hovered + ToolTip.text: "Manually add UUID to this instance of VESC Tool" + onClicked: addUuidDialog.open() + } + Button { + text: "Unpair" + icon.source: "qrc" + Utility.getThemePath() + "icons/Restart-96.png" + Layout.fillWidth: true + ToolTip.visible: hovered + ToolTip.text: "Unpair this VESC, and make it possible to connect to it from any VESC Tool instance." + onClicked: { + if (!VescIf.isPortConnected()) { + VescIf.emitMessageDialog("Unpair VESC", + "You are not connected to the VESC. Connect in order to unpair it.", + false, false) + return + } + if (mCommands.isLimitedMode()) { + VescIf.emitMessageDialog("Unpair VESC", + "The fiwmare must be updated on this VESC first.", + false, false) + return + } + if (pairedListView.currentIndex >= 0) { + unpairConfirmDialog.open() + } + } + } + Button { + text: "Delete" + icon.source: "qrc" + Utility.getThemePath() + "icons/Delete-96.png" + Layout.fillWidth: true + onClicked: { + if (pairedListView.currentIndex >= 0) { + deleteConfirmDialog.open() + } + } + } + Button { + text: "Clear" + icon.source: "qrc" + Utility.getThemePath() + "icons/Delete-96.png" + Layout.fillWidth: true + onClicked: { + if (pairedModel.count > 0) { + clearConfirmDialog.open() + } + } + } + Item { Layout.fillHeight: true } + } + } + } + } + } + + // ========================================== + // Tab 5: TCP Hub + // ========================================== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 4 + + GroupBox { + title: "Login Details" + Layout.fillWidth: true + + GridLayout { + anchors.fill: parent + columns: 6 + columnSpacing: 6 + rowSpacing: 2 + + // Row 0: Address | serverEdit | defaultBtn | portSpinBox | disconnect | connect + Label { text: "Address" } + TextField { + id: tcpHubServerEdit + Layout.fillWidth: true + text: VescIf.getLastTcpHubServer() + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Restart-96.png" + ToolTip.visible: hovered + ToolTip.text: "Restore default" + onClicked: { + tcpHubServerEdit.text = "veschub.vedder.se" + tcpHubPortBox.value = 65101 + } + } + SpinBox { + id: tcpHubPortBox + from: 0; to: 65535 + value: VescIf.getLastTcpHubPort() + editable: true + property string prefix: "Port: " + textFromValue: function(v, l) { return prefix + v } + valueFromText: function(t, l) { return parseInt(t.replace(prefix, "")) } + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Disconnected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Disconnect" + onClicked: { + if (hubClientButton.checked) { + VescIf.disconnectPort() + } else { + VescIf.tcpServerStop() + } + } + } + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Connected-96.png" + ToolTip.visible: hovered + ToolTip.text: "Connect" + onClicked: { + var server = tcpHubServerEdit.text + var port = tcpHubPortBox.value + var vescId = tcpHubVescIdEdit.text.replace(/ /g, "").replace(/:/g, "").toUpperCase() + var pass = tcpHubPasswordEdit.text + + if (hubClientButton.checked) { + VescIf.connectTcpHub(server, port, vescId, pass) + } else { + VescIf.tcpServerConnectToHub(server, port, vescId, pass) + } + } + } + + // Row 1: VESC ID | idEdit(span 2) | Client radio(span 3) + Label { text: "VESC ID" } + TextField { + id: tcpHubVescIdEdit + Layout.fillWidth: true + Layout.columnSpan: 2 + text: VescIf.getLastTcpHubVescID() + } + RadioButton { + id: hubClientButton + text: "Client" + checked: true + Layout.columnSpan: 3 + } + + // Row 2: Password | passEdit(span 2) | Server radio(span 3) + Label { text: "Password" } + TextField { + id: tcpHubPasswordEdit + Layout.fillWidth: true + Layout.columnSpan: 2 + text: VescIf.getLastTcpHubVescPass() + } + RadioButton { + id: hubServerButton + text: "Server" + Layout.columnSpan: 3 + } + } + } + + Item { Layout.fillHeight: true } + } + } + } + + // ========================================== + // CAN Forward (always visible below tabs) + // ========================================== + GroupBox { + title: "CAN Forward" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + + Button { + text: "Manual" + icon.source: "qrc" + Utility.getThemePath() + "icons/Bug-96.png" + ToolTip.visible: hovered + ToolTip.text: "Populate box without scanning, in case there are problems with scanning or the firmware does not support it." + onClicked: { + canFwdModel.clear() + for (var i = 0; i < 255; i++) { + canFwdModel.append({"text": "VESC " + i, "value": i}) + } + } + } + + Button { + id: canRefreshButton + text: "Scan" + icon.source: "qrc" + Utility.getThemePath() + "icons/Refresh-96.png" + ToolTip.visible: hovered + ToolTip.text: "Scan for CAN devices" + onClicked: { + canRefreshButton.enabled = false + mCommands.pingCan() + } + } + + ComboBox { + id: canFwdBox + Layout.fillWidth: true + model: canFwdModel + textRole: "text" + onActivated: { + if (canFwdModel.count > 0) { + mCommands.setCanSendId(canFwdModel.get(currentIndex).value) + } + } + } + + IconButton { + icon.source: "qrc" + Utility.getThemePath() + "icons/Help-96.png" + ToolTip.visible: hovered + ToolTip.text: "Show help" + onClicked: { + VescIf.emitMessageDialog("CAN Forward", + "CAN forward allows you to communicate with other VESCs connected over CAN-bus. " + + "Click scan to look for VESCs on the CAN-bus. If scanning does not work, " + + "manually add all IDs and select the one to communicate with.", + true, false) + } + } + + IconButton { + id: canFwdButton + checkable: true + icon.source: checked + ? ("qrc" + Utility.getThemePath() + "icons/can_on.png") + : ("qrc" + Utility.getThemePath() + "icons/can_off.png") + ToolTip.visible: hovered + ToolTip.text: "Forward communication over CAN-bus" + onClicked: { + if (mCommands.getCanSendId() >= 0 || !checked) { + mCommands.setSendCan(checked) + } else { + checked = false + VescIf.emitMessageDialog("CAN Forward", + "No CAN device is selected. Click on the refresh button " + + "if the selection box is empty.", + false, false) + } + } + } + } + } + + // ========================================== + // Autoconnect button + // ========================================== + Button { + Layout.fillWidth: true + text: "Autoconnect" + icon.source: "qrc" + Utility.getThemePath() + "icons/Wizard-96.png" + icon.width: 45 + icon.height: 45 + ToolTip.visible: hovered + ToolTip.text: "Try to automatically connect using the USB connection" + onClicked: VescIf.autoconnect() + } + + // ========================================== + // Status label + // ========================================== + Label { + id: statusLabel + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + text: "" + } + } + + // ---- TCP detect data storage ---- + property var tcpDetectData: [] + + // ---- Confirmation dialogs ---- + Dialog { + id: pairConfirmDialog + title: "Pair connected VESC" + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + anchors.centerIn: parent + Label { + text: "This is going to pair the connected VESC with this instance of VESC Tool. " + + "VESC Tool instances that are not paired with this VESC will not be able to " + + "connect over bluetooth any more. Continue?" + wrapMode: Text.WordWrap + width: parent.width + } + onAccepted: { + VescIf.addPairedUuid(VescIf.getConnectedUuid()) + VescIf.storeSettings() + // Set pairing_done flag on VESC + var ap = VescIf.appConfig() + mCommands.getAppConf() + if (Utility.waitSignal(ap, "updated", 1500)) { + ap.updateParamBool("pairing_done", true, null) + mCommands.setAppConf() + } + } + } + + Dialog { + id: unpairConfirmDialog + title: "Unpair connected VESC" + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + anchors.centerIn: parent + Label { + text: "This is going to unpair the connected VESC. Continue?" + wrapMode: Text.WordWrap + width: parent.width + } + onAccepted: { + var ap = VescIf.appConfig() + mCommands.getAppConf() + if (Utility.waitSignal(ap, "updated", 1500)) { + ap.updateParamBool("pairing_done", false, null) + mCommands.setAppConf() + VescIf.deletePairedUuid(VescIf.getConnectedUuid()) + VescIf.storeSettings() + } + } + } + + Dialog { + id: deleteConfirmDialog + title: "Delete paired VESC" + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + anchors.centerIn: parent + Label { + text: "This is going to delete this VESC from the paired list. If that VESC " + + "has the pairing flag set you won't be able to connect to it over BLE " + + "any more. Are you sure?" + wrapMode: Text.WordWrap + width: parent.width + } + onAccepted: { + if (pairedListView.currentIndex >= 0) { + var uuid = pairedModel.get(pairedListView.currentIndex).uuid + VescIf.deletePairedUuid(uuid) + VescIf.storeSettings() + } + } + } + + Dialog { + id: clearConfirmDialog + title: "Clear paired VESCs" + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + anchors.centerIn: parent + Label { + text: "This is going to clear the pairing list of this instance of VESC Tool. Are you sure?" + wrapMode: Text.WordWrap + width: parent.width + } + onAccepted: { + VescIf.clearPairedUuids() + VescIf.storeSettings() + } + } + + Dialog { + id: addUuidDialog + title: "Add UUID" + modal: true + standardButtons: Dialog.Ok | Dialog.Cancel + anchors.centerIn: parent + ColumnLayout { + anchors.fill: parent + Label { text: "UUID:" } + TextField { + id: addUuidEdit + Layout.fillWidth: true + } + } + onAccepted: { + if (addUuidEdit.text !== "") { + VescIf.addPairedUuid(addUuidEdit.text) + VescIf.storeSettings() + } + } + } + + // ---- Init ---- + Component.onCompleted: { + // Populate initial BLE device if last known + var lastBleAddr = VescIf.getLastBleAddr() + if (lastBleAddr !== "") { + var setName = VescIf.getBleName(lastBleAddr) + var name + if (setName !== "") { + name = setName + " [" + lastBleAddr + "]" + } else { + name = lastBleAddr + } + bleDevModel.append({"text": name, "addr": lastBleAddr}) + } + + updatePairedList() + } +} diff --git a/desktop/ControllersPage.qml b/desktop/ControllersPage.qml new file mode 100644 index 000000000..0641f46b0 --- /dev/null +++ b/desktop/ControllersPage.qml @@ -0,0 +1,109 @@ +/* + Desktop ControllersPage — faithful recreation of the original PageControllers widget. + ParamTable for "pid controllers" / "general" subgroup + + Position Offset Calculator group at the bottom. +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Vedder.vesc + +Item { + id: controllersPage + + property ConfigParams mMcConf: VescIf.mcConfig() + property Commands mCommands: VescIf.commands() + property var _dynamicItems: [] + + ParamEditors { + id: editors + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: 4 + spacing: 4 + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + contentWidth: width + contentHeight: paramCol.height + 16 + flickableDirection: Flickable.VerticalFlick + ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } + + ColumnLayout { + id: paramCol + width: parent.width + spacing: 4 + Item { Layout.preferredHeight: 1 } + } + } + + // Position Offset Calculator — matches original + GroupBox { + title: "Position Offset Calculator" + Layout.fillWidth: true + + RowLayout { + anchors.fill: parent + spacing: 8 + + SpinBox { + id: posOffsetBox + from: -360000 + to: 360000 + value: 0 + stepSize: 100 + editable: true + property real realValue: value / 1000.0 + textFromValue: function(v, locale) { + return "Pos Now: " + (v / 1000).toFixed(3) + " °" + } + valueFromText: function(text, locale) { + var s = text.replace("Pos Now: ", "").replace(" °", "") + return Math.round(parseFloat(s) * 1000) + } + Layout.fillWidth: true + } + + Button { + text: "Apply" + icon.source: "qrc" + Utility.getThemePath() + "icons/Download-96.png" + onClicked: { + mCommands.sendTerminalCmdSync( + "update_pid_pos_offset " + posOffsetBox.realValue.toFixed(3) + " 1") + mCommands.getMcconf() + } + } + } + } + } + + function reloadAll() { + for (var d = 0; d < _dynamicItems.length; d++) { + if (_dynamicItems[d]) _dynamicItems[d].destroy() + } + _dynamicItems = [] + + var params = mMcConf.getParamsFromSubgroup("pid controllers", "general") + for (var p = 0; p < params.length; p++) { + var paramName = params[p] + if (paramName.indexOf("::sep::") === 0) { + var sep = editors.createSeparator(paramCol, paramName.substring(7)) + if (sep) _dynamicItems.push(sep) + continue + } + var e = editors.createEditorMc(paramCol, paramName) + if (e) { e.Layout.fillWidth = true; _dynamicItems.push(e) } + } + } + + Component.onCompleted: reloadAll() + + Connections { + target: mMcConf + function onUpdated() { reloadAll() } + } +} diff --git a/desktop/DisplayToolPage.qml b/desktop/DisplayToolPage.qml new file mode 100644 index 000000000..d0b4d3bc3 --- /dev/null +++ b/desktop/DisplayToolPage.qml @@ -0,0 +1,862 @@ +/* + Desktop DisplayToolPage — Full pixel art editor for VESC display images. + Replicates the original PageDisplayTool with 3 top-level tabs: + 1) Display — DispEditor (left) + Font/Overlay sub-tabs (right) + 2) Overlay Editor — standalone DispEditor + 3) Font Editor — font settings + DispEditor + Each DispEditor = pixel canvas+palette (left) + preview+Controls/LoadSave (right) +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs +import Vedder.vesc + +Item { + id: root + + // ---- Shared DispEditor component ---- + component DispEditorComp: Item { + id: dispEditor + + property int imgW: 128 + property int imgH: 64 + property int formatIndex: 0 // 0=Idx2, 1=Idx4, 2=Idx16, 3=RGB332, 4=RGB565, 5=RGB888 + property string editColor: "#ffffff" + property bool drawLayer2: true + property int previewScale: 2 + property var imageData: [] + property var layer2Data: [] + property var colorPalette: [] + + Component.onCompleted: { + rebuildPalette() + clearImage() + } + + function paletteColorCount() { + switch (formatIndex) { + case 0: return 2 + case 1: return 4 + case 2: return 16 + default: return 0 + } + } + + function rebuildPalette() { + var count = paletteColorCount() + var pal = [] + for (var i = 0; i < count; i++) { + var v = count > 1 ? Math.round(255 * i / (count - 1)) : 0 + var hex = "#" + ("0" + v.toString(16)).slice(-2).repeat(3) + pal.push(hex) + } + colorPalette = pal + editColor = pal.length > 0 ? pal[pal.length - 1] : "#ffffff" + } + + function clearImage() { + var arr = new Array(imgW * imgH) + for (var i = 0; i < arr.length; i++) arr[i] = "#000000" + imageData = arr + clearLayer2() + edCanvas.requestPaint() + pvCanvas.requestPaint() + } + + function clearLayer2() { + var arr = new Array(imgW * imgH) + for (var i = 0; i < arr.length; i++) arr[i] = "" + layer2Data = arr + edCanvas.requestPaint() + pvCanvas.requestPaint() + } + + function getPixel(x, y) { + if (x < 0 || x >= imgW || y < 0 || y >= imgH) return "#000000" + var idx = y * imgW + x + var l2 = layer2Data[idx] + if (drawLayer2 && l2 && l2 !== "") return l2 + return imageData[idx] + } + + function setPixel(x, y, color) { + if (x < 0 || x >= imgW || y < 0 || y >= imgH) return + imageData[y * imgW + x] = color + } + + function updateSize(w, h) { + imgW = w; imgH = h + clearImage() + } + + // Layout: canvas+palette (left) | preview+tabs (right) + RowLayout { + anchors.fill: parent + spacing: 0 + + // ==== LEFT: Pixel editor + palette ==== + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.minimumWidth: 300 + spacing: 0 + + // Pixel editor canvas + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "black" + clip: true + + Canvas { + id: edCanvas + anchors.fill: parent + + property real sf: 8.0 + property real ox: -(imgW * 4) + property real oy: -(imgH * 4) + property int mlx: -1000000 + property int mly: -1000000 + property int cpx: -1 + property int cpy: -1 + + function pxAt(mx, my) { + return Qt.point( + Math.floor((mx - ox - width / 2) / sf), + Math.floor((my - oy - height / 2) / sf)) + } + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + ctx.fillStyle = "black" + ctx.fillRect(0, 0, width, height) + + ctx.save() + ctx.translate(width / 2 + ox, height / 2 + oy) + ctx.scale(sf, sf) + + for (var j = 0; j < imgH; j++) { + for (var i = 0; i < imgW; i++) { + var c = getPixel(i, j) + if (i === cpx && j === cpy) { + var r = parseInt(c.substring(1, 3), 16) / 255 + var g = parseInt(c.substring(3, 5), 16) / 255 + var b = parseInt(c.substring(5, 7), 16) / 255 + if (g < 0.5) g += 0.2; else { r -= 0.2; b -= 0.2 } + r = Math.max(0, Math.min(1, r)) + g = Math.max(0, Math.min(1, g)) + b = Math.max(0, Math.min(1, b)) + ctx.fillStyle = Qt.rgba(r, g, b, 1) + } else { + ctx.fillStyle = c + } + ctx.fillRect(i, j, 1, 1) + } + } + ctx.restore() + + // Grid + var gox = width / 2 + ox + var goy = height / 2 + oy + if (sf >= 4) { + ctx.strokeStyle = "#404040" + ctx.lineWidth = 0.5 + for (var gx = 0; gx <= imgW; gx++) { + var sx = gox + gx * sf + ctx.beginPath(); ctx.moveTo(sx, goy); ctx.lineTo(sx, goy + imgH * sf); ctx.stroke() + } + for (var gy = 0; gy <= imgH; gy++) { + var sy = goy + gy * sf + ctx.beginPath(); ctx.moveTo(gox, sy); ctx.lineTo(gox + imgW * sf, sy); ctx.stroke() + } + } + + // Coord overlay + ctx.fillStyle = "rgba(0,0,0,0.5)" + ctx.fillRect(width - 100, height - 40, 100, 40) + ctx.fillStyle = "white" + ctx.font = "11px monospace" + ctx.fillText("X: " + cpx, width - 90, height - 24) + ctx.fillText("Y: " + cpy, width - 90, height - 10) + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onPositionChanged: function(mouse) { + var pt = edCanvas.pxAt(mouse.x, mouse.y) + edCanvas.cpx = pt.x; edCanvas.cpy = pt.y + var shift = (mouse.modifiers & Qt.ShiftModifier) + if (mouse.buttons & Qt.LeftButton) { + if (shift) { + if (pt.x >= 0 && pt.x < imgW && pt.y >= 0 && pt.y < imgH) { + setPixel(pt.x, pt.y, editColor) + pvCanvas.requestPaint() + } + } else { + if (edCanvas.mlx > -999999) { + edCanvas.ox += mouse.x - edCanvas.mlx + edCanvas.oy += mouse.y - edCanvas.mly + } + edCanvas.mlx = mouse.x; edCanvas.mly = mouse.y + } + } + edCanvas.requestPaint() + } + onPressed: function(mouse) { + var shift = (mouse.modifiers & Qt.ShiftModifier) + var pt = edCanvas.pxAt(mouse.x, mouse.y) + if (mouse.button === Qt.LeftButton) { + if (shift) { + if (pt.x >= 0 && pt.x < imgW && pt.y >= 0 && pt.y < imgH) { + setPixel(pt.x, pt.y, editColor) + pvCanvas.requestPaint() + } + } else { + edCanvas.mlx = mouse.x; edCanvas.mly = mouse.y + } + } else if (mouse.button === Qt.RightButton && shift) { + if (pt.x >= 0 && pt.x < imgW && pt.y >= 0 && pt.y < imgH) + editColor = getPixel(pt.x, pt.y) + } + edCanvas.requestPaint() + } + onReleased: { edCanvas.mlx = -1000000; edCanvas.mly = -1000000 } + onWheel: function(wheel) { + var d = wheel.angleDelta.y / 600.0 + if (d > 0.8) d = 0.8; if (d < -0.8) d = -0.8 + edCanvas.sf += edCanvas.sf * d + edCanvas.ox += edCanvas.ox * d + edCanvas.oy += edCanvas.oy * d + if (edCanvas.sf < 0.5) edCanvas.sf = 0.5 + if (edCanvas.sf > 40) edCanvas.sf = 40 + edCanvas.requestPaint() + } + } + } + } + + // Palette row + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: colorPalette.length > 0 ? 34 : 0 + color: "transparent" + visible: colorPalette.length > 0 + + Flow { + anchors.fill: parent; anchors.margins: 1; spacing: 1 + Repeater { + model: colorPalette.length + Rectangle { + width: 26; height: 26; color: colorPalette[index] + border.color: editColor === colorPalette[index] ? "#ff6600" : "#666666" + border.width: editColor === colorPalette[index] ? 2 : 1 + MouseArea { + anchors.fill: parent + onClicked: editColor = colorPalette[index] + } + } + } + } + } + } + + // ==== RIGHT: Preview + Controls/Load-Save tabs ==== + ColumnLayout { + Layout.preferredWidth: 280 + Layout.minimumWidth: 200 + Layout.maximumWidth: 320 + Layout.fillHeight: true + spacing: 0 + + // Preview image + ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: Math.min(imgH * previewScale + 4, 256) + Layout.maximumHeight: 256 + clip: true + + Canvas { + id: pvCanvas + width: imgW * previewScale + height: imgH * previewScale + + onPaint: { + var ctx = getContext("2d") + ctx.fillStyle = "black" + ctx.fillRect(0, 0, width, height) + var s = previewScale + for (var j = 0; j < imgH; j++) { + for (var i = 0; i < imgW; i++) { + ctx.fillStyle = getPixel(i, j) + ctx.fillRect(i * s, j * s, s, s) + } + } + } + } + } + + // Controls / Load-Save tabs + TabBar { + id: ctrlTabBar + Layout.fillWidth: true + + TabButton { text: "Controls"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Load/Save"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: ctrlTabBar.currentIndex + + // Controls + Item { + ColumnLayout { + anchors.fill: parent; anchors.margins: 3; spacing: 2 + + SpinBox { + Layout.fillWidth: true + from: 1; to: 10; value: dispEditor.previewScale; editable: true + textFromValue: function(v) { return "Preview Scale: " + v } + valueFromText: function(t) { return parseInt(t.replace("Preview Scale: ", "")) || 2 } + onValueModified: { dispEditor.previewScale = value; pvCanvas.requestPaint() } + } + + Button { + text: "Help"; Layout.fillWidth: true + icon.source: "qrc" + Utility.getThemePath() + "icons/Help-96.png" + onClicked: helpDialog.open() + } + + Button { + text: "Clear Screen"; Layout.fillWidth: true + onClicked: dispEditor.clearImage() + } + + Button { + text: "Clear Layer 2"; Layout.fillWidth: true + onClicked: dispEditor.clearLayer2() + } + + // Current color + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 24 + color: editColor + border.color: Utility.getAppHexColor("disabledText") + border.width: 2 + } + + Item { Layout.fillHeight: true } + } + } + + // Load/Save + Item { + ColumnLayout { + anchors.fill: parent; anchors.margins: 3; spacing: 2 + + RowLayout { + Layout.fillWidth: true; spacing: 2 + Label { text: "Format" } + ComboBox { + Layout.fillWidth: true + textRole: "display" + model: Utility.stringListModel(["Indexed 2", "Indexed 4", "Indexed 16", "RGB332", "RGB565", "RGB888"]) + currentIndex: dispEditor.formatIndex + onCurrentIndexChanged: { + dispEditor.formatIndex = currentIndex + dispEditor.rebuildPalette() + edCanvas.requestPaint() + } + } + } + + RowLayout { + Layout.fillWidth: true; spacing: 2 + Button { text: "Export Bin"; Layout.fillWidth: true + onClicked: VescIf.emitMessageDialog("Export Bin", "Binary export requires native file I/O.", false, false) + } + Button { text: "Import Bin"; Layout.fillWidth: true + onClicked: VescIf.emitMessageDialog("Import Bin", "Binary import requires native file I/O.", false, false) + } + } + + RowLayout { + Layout.fillWidth: true; spacing: 2 + Button { text: "Save PNG"; Layout.fillWidth: true + ToolTip.visible: hovered; ToolTip.text: "Save image to PNG file." + onClicked: VescIf.emitStatusMessage("PNG save not yet implemented", false) + } + Button { text: "Load Image"; Layout.fillWidth: true + onClicked: VescIf.emitMessageDialog("Load Image", "Image loading with dithering requires native code.", false, false) + } + } + + SpinBox { + Layout.fillWidth: true + from: 0; to: 10000; stepSize: 10; value: 1000; editable: true + ToolTip.visible: hovered + ToolTip.text: "Scale colors when loading image." + textFromValue: function(v) { return "Load Color Scale: " + (v / 1000.0).toFixed(2) } + valueFromText: function(t) { return Math.round(parseFloat(t.replace("Load Color Scale: ", "")) * 1000) } + } + + CheckBox { text: "Dither when loading"; checked: true } + CheckBox { text: "Antialias when loading"; checked: true } + CheckBox { + text: "Show Layer 2"; checked: true + onToggled: { + dispEditor.drawLayer2 = checked + edCanvas.requestPaint(); pvCanvas.requestPaint() + } + } + + Item { Layout.fillHeight: true } + } + } + } + } + } + } + + // ======== TOP-LEVEL TABS (Display | Overlay Editor | Font Editor) ======== + ColumnLayout { + anchors.fill: parent + spacing: 0 + + TabBar { + id: mainTabBar + Layout.fillWidth: true + + TabButton { text: "Display"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Overlay Editor"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Font Editor"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: mainTabBar.currentIndex + + // ======== Tab 1: Display ======== + Item { + RowLayout { + anchors.fill: parent + anchors.margins: 3 + spacing: 2 + + // Left: W/H toolbar + DispEditor + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 2 + + RowLayout { + Layout.fillWidth: true; spacing: 2 + SpinBox { + id: wBoxDisp; from: 1; to: 1024; value: 128; editable: true + textFromValue: function(v) { return "W: " + v } + valueFromText: function(t) { return parseInt(t.replace("W: ", "")) || 128 } + } + SpinBox { + id: hBoxDisp; from: 1; to: 1024; value: 64; editable: true + textFromValue: function(v) { return "H: " + v } + valueFromText: function(t) { return parseInt(t.replace("H: ", "")) || 64 } + } + Button { + text: "Update" + onClicked: dispEditorMain.updateSize(wBoxDisp.value, hBoxDisp.value) + } + Item { Layout.fillWidth: true } + } + + DispEditorComp { + id: dispEditorMain + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + // Right: Font / Overlay sub-tabs + ColumnLayout { + Layout.preferredWidth: 240 + Layout.minimumWidth: 200 + Layout.maximumWidth: 280 + Layout.fillHeight: true + spacing: 0 + + TabBar { + id: rightSubTabBar + Layout.fillWidth: true + + TabButton { text: "Font"; topPadding: 9; bottomPadding: 9 } + TabButton { text: "Overlay"; topPadding: 9; bottomPadding: 9 } + } + + StackLayout { + Layout.fillWidth: true + Layout.fillHeight: true + currentIndex: rightSubTabBar.currentIndex + + // Font sub-tab + Item { + ScrollView { + anchors.fill: parent; clip: true + + ColumnLayout { + width: parent.width + spacing: 3 + + CheckBox { + id: drawFontCheck + text: "Draw Font" + checked: false + Layout.fillWidth: true + } + GroupBox { + title: "Draw Font Settings" + Layout.fillWidth: true + enabled: drawFontCheck.checked + + GridLayout { + anchors.fill: parent + columns: 2; columnSpacing: 3; rowSpacing: 3 + + ComboBox { + id: fontBox; Layout.columnSpan: 2; Layout.fillWidth: true + textRole: "display" + model: Utility.stringListModel(["Roboto", "DejaVu Sans Mono", "Liberation Sans", "Liberation Mono"]) + } + CheckBox { text: "Bold" } + Item {} + SpinBox { + from: 0; to: 512; value: 34; editable: true + textFromValue: function(v) { return "X: " + v } + valueFromText: function(t) { return parseInt(t.replace("X: ", "")) || 0 } + } + SpinBox { + from: 0; to: 512; value: 46; editable: true + textFromValue: function(v) { return "Y: " + v } + valueFromText: function(t) { return parseInt(t.replace("Y: ", "")) || 0 } + } + SpinBox { + from: 1; to: 128; value: 10; editable: true + textFromValue: function(v) { return "W: " + v } + valueFromText: function(t) { return parseInt(t.replace("W: ", "")) || 10 } + } + SpinBox { + from: 1; to: 128; value: 16; editable: true + textFromValue: function(v) { return "H: " + v } + valueFromText: function(t) { return parseInt(t.replace("H: ", "")) || 16 } + } + SpinBox { + Layout.columnSpan: 2; Layout.fillWidth: true + from: 10; to: 10000; stepSize: 10; value: 1000; editable: true + textFromValue: function(v) { return "Scale: " + (v / 1000.0).toFixed(2) } + valueFromText: function(t) { return Math.round(parseFloat(t.replace("Scale: ", "")) * 1000) } + } + TextField { + Layout.columnSpan: 2; Layout.fillWidth: true + text: "ABC 00" + } + CheckBox { text: "Antialias" } + CheckBox { text: "Border"; checked: true } + CheckBox { Layout.columnSpan: 2; text: "NumOnly" } + Button { + Layout.columnSpan: 2; Layout.fillWidth: true + text: "Export Font" + onClicked: VescIf.emitStatusMessage("Font export not yet implemented", false) + } + } + } + + Item { Layout.fillHeight: true } + } + } + } + + // Overlay sub-tab + Item { + ScrollView { + anchors.fill: parent; clip: true + + ColumnLayout { + width: parent.width + spacing: 3 + + CheckBox { + id: drawOverlayCheck + text: "Draw Overlay" + checked: false + Layout.fillWidth: true + } + GroupBox { + title: "Draw Overlay Settings" + Layout.fillWidth: true + enabled: drawOverlayCheck.checked + + GridLayout { + anchors.fill: parent + columns: 3; columnSpacing: 3; rowSpacing: 3 + + Label { text: "Position" } + SpinBox { + from: -512; to: 512; value: 0; editable: true + textFromValue: function(v) { return "X: " + v } + valueFromText: function(t) { return parseInt(t.replace("X: ", "")) || 0 } + } + SpinBox { + from: -512; to: 512; value: 0; editable: true + textFromValue: function(v) { return "Y: " + v } + valueFromText: function(t) { return parseInt(t.replace("Y: ", "")) || 0 } + } + + Label { + text: "Crop Start" + ToolTip.text: "Corner to start crop." + ToolTip.visible: cropStartHover.hovered + ToolTip.delay: 500 + HoverHandler { id: cropStartHover } + } + SpinBox { + from: 0; to: 512; value: 0; editable: true + textFromValue: function(v) { return "X: " + v } + valueFromText: function(t) { return parseInt(t.replace("X: ", "")) || 0 } + } + SpinBox { + from: 0; to: 512; value: 0; editable: true + textFromValue: function(v) { return "Y: " + v } + valueFromText: function(t) { return parseInt(t.replace("Y: ", "")) || 0 } + } + + Label { text: "Crop Size" } + SpinBox { + from: 0; to: 512; value: 512; editable: true + textFromValue: function(v) { return "W: " + v } + valueFromText: function(t) { return parseInt(t.replace("W: ", "")) || 512 } + } + SpinBox { + from: 0; to: 512; value: 512; editable: true + textFromValue: function(v) { return "H: " + v } + valueFromText: function(t) { return parseInt(t.replace("H: ", "")) || 512 } + } + + Label { + text: "Img Center" + ToolTip.text: "This pixel on the image corresponds to Position." + ToolTip.visible: imgCenterHover.hovered + ToolTip.delay: 500 + HoverHandler { id: imgCenterHover } + } + SpinBox { + from: -1024; to: 1024; value: 0; editable: true + textFromValue: function(v) { return "X: " + v } + valueFromText: function(t) { return parseInt(t.replace("X: ", "")) || 0 } + } + SpinBox { + from: -1024; to: 1024; value: 0; editable: true + textFromValue: function(v) { return "Y: " + v } + valueFromText: function(t) { return parseInt(t.replace("Y: ", "")) || 0 } + } + + Label { + text: "Rot Center" + ToolTip.text: "Rotation will be done around this pixel." + ToolTip.visible: rotCenterHover.hovered + ToolTip.delay: 500 + HoverHandler { id: rotCenterHover } + } + SpinBox { + from: -1024; to: 1024; value: 0; editable: true + textFromValue: function(v) { return "X: " + v } + valueFromText: function(t) { return parseInt(t.replace("X: ", "")) || 0 } + } + SpinBox { + from: -1024; to: 1024; value: 0; editable: true + textFromValue: function(v) { return "Y: " + v } + valueFromText: function(t) { return parseInt(t.replace("Y: ", "")) || 0 } + } + + Label { text: "Rotation" } + SpinBox { + Layout.columnSpan: 2; Layout.fillWidth: true + from: -360000; to: 360000; stepSize: 1000; value: 0; editable: true + property real realValue: value / 1000.0 + textFromValue: function(v) { return (v / 1000.0).toFixed(1) + " Deg" } + valueFromText: function(t) { return Math.round(parseFloat(t.replace(" Deg", "")) * 1000) } + } + + Label { text: "Scale" } + SpinBox { + Layout.columnSpan: 2; Layout.fillWidth: true + from: 1; to: 100000; stepSize: 10; value: 1000; editable: true + textFromValue: function(v) { return (v / 1000.0).toFixed(3) } + valueFromText: function(t) { return Math.round(parseFloat(t) * 1000) } + } + + Label { text: "Transparent" } + SpinBox { + Layout.columnSpan: 2; Layout.fillWidth: true + from: -1; to: 15; value: 0; editable: true + ToolTip.visible: hovered + ToolTip.text: "Color index on image to count as transparent." + } + + Button { + Layout.columnSpan: 3; Layout.fillWidth: true + text: "Save to Layer 2" + ToolTip.visible: hovered + ToolTip.text: "Save overlay to layer 2 on image." + onClicked: VescIf.emitStatusMessage("Overlay saved to Layer 2", true) + } + } + } + + Item { Layout.fillHeight: true } + } + } + } + } + } + } + } + + // ======== Tab 2: Overlay Editor ======== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 3 + spacing: 2 + + RowLayout { + Layout.fillWidth: true; spacing: 2 + SpinBox { + id: wBoxOv; from: 1; to: 1024; value: 128; editable: true + textFromValue: function(v) { return "W: " + v } + valueFromText: function(t) { return parseInt(t.replace("W: ", "")) || 128 } + } + SpinBox { + id: hBoxOv; from: 1; to: 1024; value: 64; editable: true + textFromValue: function(v) { return "H: " + v } + valueFromText: function(t) { return parseInt(t.replace("H: ", "")) || 64 } + } + Button { + text: "Update" + onClicked: dispEditorOverlay.updateSize(wBoxOv.value, hBoxOv.value) + } + Item { Layout.fillWidth: true } + } + + DispEditorComp { + id: dispEditorOverlay + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + + // ======== Tab 3: Font Editor ======== + Item { + ColumnLayout { + anchors.fill: parent + anchors.margins: 3 + spacing: 2 + + GridLayout { + Layout.fillWidth: true + columns: 4; columnSpacing: 6; rowSpacing: 3 + + RadioButton { text: "All"; checked: true } + RadioButton { text: "Numbers Only" } + Item {} + Button { + text: "Apply" + ToolTip.visible: hovered; ToolTip.text: "Apply font and settings" + onClicked: VescIf.emitStatusMessage("Font editor apply not yet implemented", false) + } + + ComboBox { + Layout.minimumWidth: 120 + textRole: "display" + model: Utility.stringListModel(["Roboto", "DejaVu Sans Mono", "Liberation Sans", "Liberation Mono"]) + } + CheckBox { text: "Anti Alias (2bpp)" } + CheckBox { text: "Bold" } + CheckBox { text: "Border"; checked: true } + + SpinBox { + from: 1; to: 128; value: 10; editable: true + textFromValue: function(v) { return "W: " + v } + valueFromText: function(t) { return parseInt(t.replace("W: ", "")) || 10 } + } + SpinBox { + from: 1; to: 128; value: 16; editable: true + textFromValue: function(v) { return "H: " + v } + valueFromText: function(t) { return parseInt(t.replace("H: ", "")) || 16 } + } + SpinBox { + Layout.columnSpan: 2; Layout.fillWidth: true + from: 10; to: 10000; stepSize: 10; value: 1000; editable: true + textFromValue: function(v) { return "Scale: " + (v / 1000.0).toFixed(2) } + valueFromText: function(t) { return Math.round(parseFloat(t.replace("Scale: ", "")) * 1000) } + } + + Button { + text: "Export Font"; Layout.columnSpan: 2; Layout.fillWidth: true + onClicked: VescIf.emitStatusMessage("Font export not yet implemented", false) + } + Button { + text: "Import Font"; Layout.columnSpan: 2; Layout.fillWidth: true + onClicked: VescIf.emitStatusMessage("Font import not yet implemented", false) + } + } + + DispEditorComp { + id: dispEditorFont + Layout.fillWidth: true + Layout.fillHeight: true + } + } + } + } + } + + // ---- Help Dialog ---- + Dialog { + id: helpDialog + title: "Usage Instructions" + width: 500; height: 400 + anchors.centerIn: parent + modal: true + standardButtons: Dialog.Ok + + ScrollView { + anchors.fill: parent; clip: true + Label { + width: parent.width - 20 + wrapMode: Text.WordWrap; textFormat: Text.RichText + text: "Navigate in editor
" + + "Left-click and drag to move. Scroll to zoom.

" + + "Draw pixels
" + + "Shift + Left-click (and drag)

" + + "Change color
" + + "Click on color buttons, or Shift + Right-click on pixel with desired color.

" + + "Update Palette Color
" + + "Ctrl + left-click on the palette buttons.

" + + "Overlay
" + + "This function overlays an image from the overlay tab to the display tab with a transform. " + + "The same transforms are available in the display library, meaning that all transform " + + "parameters can be animated.

" + + "Layer 2
" + + "The second layer can be used to draw overlays on, without messing with the main image. " + + "This way the background image can be kept clean for when it is saved." + } + } + } +} diff --git a/desktop/ExperimentPlotPage.qml b/desktop/ExperimentPlotPage.qml new file mode 100644 index 000000000..d5ce4c4df --- /dev/null +++ b/desktop/ExperimentPlotPage.qml @@ -0,0 +1,605 @@ +/* + Desktop ExperimentPlotPage — full feature parity with ExperimentPlot widget. + + Layout: + ┌────────────────────┬─────────────────────────────────────┐ + │ Left sidebar │ Main plot area (GraphsView) │ + │ ScrollView 190px │ │ + │ ┌────────────────┐ │ │ + │ │ Settings │ │ │ + │ │ History: 2000 │ │ │ + │ │ [1][2][3] │ │ │ + │ │ [4][5][6] │ │ │ + │ │ [Clear][Line] │ │ │ + │ │ [Scat][Auto] │ │ │ + │ │ [HZ ][VZ ] │ │ │ + │ ├────────────────┤ │ │ + │ │ Import │ │ │ + │ │ [XML] │ │ │ + │ ├────────────────┤ │ │ + │ │ Export │ │ │ + │ │ W:640 H:480 │ │ │ + │ │ Scale: 1.0 │ │ │ + │ │ [XML][PNG] │ │ │ + │ │ [CSV][PDF] │ │ │ + │ └────────────────┘ │ │ + └────────────────────┴─────────────────────────────────────┘ +*/ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Dialogs +import QtGraphs +import Vedder.vesc + +Item { + id: expPage + + // Internal data storage (mirrors EXPERIMENT_PLOT struct vector) + property var plotGraphs: [] // array of {label, color, xData[], yData[]} + property int currentGraphIndex: 0 + property bool needsReplot: false + property string xLabelText: "" + property string yLabelText: "" + property int _legendRevision: 0 + + // Auto-color assignment matching original order + readonly property var graphColors: [ + Utility.getAppHexColor("plot_graph1"), + Utility.getAppHexColor("plot_graph2"), + Utility.getAppHexColor("plot_graph3"), + Utility.getAppHexColor("plot_graph4"), + Utility.getAppHexColor("plot_graph5"), + Utility.getAppHexColor("plot_graph6") + ] + + // 20ms replot timer (matches original) + Timer { + id: replotTimer + interval: 20; running: true; repeat: true + onTriggered: { + if (needsReplot) { + doReplot() + needsReplot = false + } + } + } + + function doReplot() { + // Clear existing series + for (var i = experimentChart.count - 1; i >= 0; i--) + experimentChart.removeSeries(experimentChart.seriesAt(i)) + + var graphButtons = [graph1Btn, graph2Btn, graph3Btn, graph4Btn, graph5Btn, graph6Btn] + + for (var g = 0; g < plotGraphs.length; g++) { + // Check visibility via toggle buttons (first 6) + if (g < 6 && !graphButtons[g].checked) continue + + var pg = plotGraphs[g] + + var series = experimentChart.createSeries( + showLineBtn.checked ? GraphsView.SeriesTypeLine : GraphsView.SeriesTypeScatter, + pg.label + ) + series.color = pg.color + series.width = 1.5 + + for (var k = 0; k < pg.xData.length; k++) + series.append(pg.xData[k], pg.yData[k]) + } + + // Update axis labels + experimentAxisX.titleText = xLabelText + experimentAxisY.titleText = yLabelText + + // Auto-scale if checked + if (autoScaleBtn.checked) { + rescaleAxes() + } + + _legendRevision++ + } + + function rescaleAxes() { + var xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity + for (var i = 0; i < experimentChart.count; i++) { + var s = experimentChart.seriesAt(i) + for (var j = 0; j < s.count; j++) { + var pt = s.at(j) + if (pt.x < xMin) xMin = pt.x + if (pt.x > xMax) xMax = pt.x + if (pt.y < yMin) yMin = pt.y + if (pt.y > yMax) yMax = pt.y + } + } + if (xMin < xMax) { + experimentAxisX.min = xMin + experimentAxisX.max = xMax + } + if (yMin < yMax) { + var pad = (yMax - yMin) * 0.05 + if (pad < 0.001) pad = 0.001 + experimentAxisY.min = yMin - pad + experimentAxisY.max = yMax + pad + } + } + + // ---- Signal handlers from persistent C++ store ---- + Connections { + target: RtDataStore + + function onPlotInitialized(xLabel, yLabel) { + plotGraphs = [] + currentGraphIndex = 0 + xLabelText = xLabel + yLabelText = yLabel + needsReplot = true + } + + function onPlotPointAdded(graphIndex, x, y) { + // Ensure local mirror exists + while (plotGraphs.length <= graphIndex) { + plotGraphs.push({ + label: "Graph " + (plotGraphs.length + 1), + color: graphColors[Math.min(plotGraphs.length, graphColors.length - 1)], + xData: [], + yData: [] + }) + } + + var g = plotGraphs[graphIndex] + g.xData.push(x) + g.yData.push(y) + + // History cap (keep in sync with store) + RtDataStore.setPlotHistoryMax(historyBox.value) + var historyMax = historyBox.value + if (g.xData.length > historyMax) { + var excess = g.xData.length - historyMax + g.xData.splice(0, excess) + g.yData.splice(0, excess) + } + + plotGraphs = plotGraphs + needsReplot = true + } + + function onPlotGraphAdded(index, name, color) { + var newGraph = { + label: name, + color: color, + xData: [], + yData: [] + } + var temp = plotGraphs.slice() + temp.push(newGraph) + plotGraphs = temp + needsReplot = true + } + + function onPlotCurrentGraphChanged(graph) { + currentGraphIndex = graph + } + } + + // ---- XML Save/Load helpers using Utility ---- + function saveXml(path) { + var xml = '\n\n' + xml += ' ' + xLabelText + '\n' + xml += ' ' + yLabelText + '\n' + for (var i = 0; i < plotGraphs.length; i++) { + var g = plotGraphs[i] + xml += ' \n' + xml += ' \n' + xml += ' ' + g.color + '\n' + for (var j = 0; j < g.xData.length; j++) { + xml += ' ' + g.xData[j] + '' + g.yData[j] + '\n' + } + xml += ' \n' + } + xml += '\n' + return Utility.writeTextFile(path, xml) + } + + function loadXml(path) { + var text = Utility.readTextFile(path) + if (text.length === 0) return false + + var parser = new DOMParser() + // Not available in QML — use simple regex-based parsing instead + return parseXmlManual(text) + } + + function parseXmlManual(text) { + plotGraphs = [] + currentGraphIndex = 0 + + // Extract xlabel + var xlm = text.match(/(.*?)<\/xlabel>/) + if (xlm) xLabelText = xlm[1] + + var ylm = text.match(/(.*?)<\/ylabel>/) + if (ylm) yLabelText = ylm[1] + + // Extract graphs + var graphRegex = /([\s\S]*?)<\/graph>/g + var gMatch + while ((gMatch = graphRegex.exec(text)) !== null) { + var gText = gMatch[1] + var g = { label: "", color: "#4d7fc4", xData: [], yData: [] } + + var lm = gText.match(/