From 8c49e2f11ce39319c72d571365574ac9c4c69864 Mon Sep 17 00:00:00 2001 From: Jakob Gegeniger Date: Sat, 16 Nov 2024 11:53:28 +0100 Subject: [PATCH] Branching from Cathaswatch project https://git.nerdyssey.de/exodus/1NiM-WatingRoom-WatchController --- .clangd | 2 + .gitignore | 2 + .gitmodules | 3 + .vscode/c_cpp_properties.json | 41 +- .vscode/cmake-kits.json | 30 + .vscode/launch.json | 5 - .vscode/settings.json | 93 +- .vscode/tasks.json | 254 +- CMakeLists.txt | 25 +- README.md | 98 +- components/spit/CMakeLists.txt | 5 + components/spit/Notes.md | 11 + .../spit/SPIT_Protocol_en_20180211_1630.pdf | Bin 0 -> 253397 bytes .../spit/include/sad_DetectedServer.hpp | 155 ++ components/spit/include/spit_client.hpp | 203 ++ components/spit/include/spit_message.hpp | 720 ++++++ components/spit/include/spit_object.hpp | 122 + components/spit/include/spit_player.hpp | 98 + components/spit/include/spit_processor.hpp | 169 ++ components/spit/include/spit_repo.hpp | 59 + components/spit/include/spit_socket.hpp | 149 ++ components/spit/include/spit_type.hpp | 189 ++ components/spit/src/esp/from | 1 + components/spit/src/esp/license | 7 + components/spit/src/esp/uuid.c | 84 + components/spit/src/esp/uuid.h | 17 + components/spit/src/spit_client.cpp | 650 +++++ components/spit/src/spit_message.cpp | 1334 ++++++++++ components/spit/src/spit_object.cpp | 202 ++ components/spit/src/spit_player.cpp | 93 + components/spit/src/spit_processor.cpp | 745 ++++++ components/spit/src/spit_socket.cpp | 213 ++ components/spit/test/CMakeLists.txt | 5 + components/spit/test/test_spit_client.cpp | 123 + components/spit/tools/spit.lua | 361 +++ components/spit/uTest/CMakeLists.txt | 5 + components/spit/uTest/messageTest.cpp | 320 +++ components/spit/uTest/spitObjectTest.cpp | 92 + components/spit/uTest/spitProcessorTest.cpp | 350 +++ .../spit/uTest/spitProcessor_complexTest.cpp | 516 ++++ components/spit/uTest/spitRepoTest.cpp | 128 + components/spit/uTest/spitTestMain.cpp | 11 + components/spit/uTest/spitTypeTest.cpp | 88 + components/watchFace/CMakeLists.txt | 4 + components/watchFace/include/watchFace.hpp | 70 + components/watchFace/src/watchFace.cpp | 1 + components/watchFace/uTest/CMakeLists.txt | 5 + components/watchFace/uTest/WatchFaceUTest.cpp | 112 + core | 1 + cspell.json | 17 + dependencies.lock | 21 + doc/Gegengier_Engineering_CatasWatch.dvd | Bin 0 -> 168187 bytes doc/InitRun.md | 188 ++ doc/LSS-Icon.afdesign | Bin 0 -> 89027 bytes doc/LSS-Icon.jpg | Bin 0 -> 26214 bytes doc/LiveShowProject/CatasWatchProject.dpr | Bin 0 -> 38 bytes doc/LiveShowProject/CatasWatchProject.ldv | Bin 0 -> 19073 bytes doc/LiveShowProject/CatasWatchProject.lms | Bin 0 -> 14 bytes doc/LiveShowProject/CatasWatchProject.lmv | Bin 0 -> 2379 bytes doc/LiveShowProject/CatasWatchProject.lsb | Bin 0 -> 17935 bytes doc/LiveShowProject/CatasWatchProject.lsc | Bin 0 -> 213 bytes doc/LiveShowProject/CatasWatchProject.lsd | Bin 0 -> 170057 bytes doc/LiveShowProject/CatasWatchProject.lse | Bin 0 -> 96 bytes doc/LiveShowProject/CatasWatchProject.lsf | Bin 0 -> 78 bytes doc/LiveShowProject/CatasWatchProject.lsh | Bin 0 -> 317 bytes doc/LiveShowProject/CatasWatchProject.lsj | Bin 0 -> 38 bytes doc/LiveShowProject/CatasWatchProject.lsp | Bin 0 -> 661 bytes doc/LiveShowProject/CatasWatchProject.lss | Bin 0 -> 186 bytes doc/LiveShowProject/CatasWatchProject.lsu | Bin 0 -> 1172 bytes doc/LiveShowProject/CatasWatchProject.lsv | Bin 0 -> 1723 bytes doc/LiveShowProject/CatasWatchProject.lsx | Bin 0 -> 3591 bytes doc/LiveShowProject/CatasWatchProject.rbo | Bin 0 -> 539 bytes doc/LiveShowProject/CatasWatchProject.rtf | 5 + doc/LiveShowProject/CatasWatchProject.spi | Bin 0 -> 2335 bytes doc/LiveShowProject/CatasWatchProject.ssj | Bin 0 -> 22 bytes doc/LiveShowProject/CatasWatchProject.uip | Bin 0 -> 150 bytes esp_idf_project_configuration.json | 54 + eth2ap.png | Bin 88732 -> 0 bytes main/CMakeLists.txt | 2 +- main/Kconfig.projbuild | 8 +- main/config.hpp | 30 + main/ethernet_example_main.c | 222 -- main/idf_component.yml | 7 +- main/lighsMain.hpp | 49 + main/lightsMain.cpp | 651 +++++ makefile | 20 + .../frozen-eye__esp_telnet/.component_hash | 1 + .../frozen-eye__esp_telnet/.gitignore | 0 .../frozen-eye__esp_telnet/AUTHORS | 12 + .../frozen-eye__esp_telnet/CHECKSUMS.json | 1 + .../frozen-eye__esp_telnet/CMakeLists.txt | 16 + .../frozen-eye__esp_telnet/Kconfig | 41 + .../frozen-eye__esp_telnet/LICENSE | 21 + .../frozen-eye__esp_telnet/idf_component.yml | 6 + .../include/telnet/server.h | 55 + .../frozen-eye__esp_telnet/src/libtelnet.c | 1668 ++++++++++++ .../frozen-eye__esp_telnet/src/libtelnet.h | 691 +++++ .../frozen-eye__esp_telnet/src/server.c | 336 +++ sdkconfig | 2255 +++++++++++++++++ sdkconfig.defaults | 25 + startCode.sh | 3 + test/integration/CMakeLists.txt | 12 + test/integration/main/CMakeLists.txt | 2 + test/integration/main/movementTests.cpp | 340 +++ test/unitOnHost/CMakeLists.txt | 12 + test/unitOnTarget/CMakeLists.txt | 20 + test/unitOnTarget/ReadMe.md | 3 + test/unitOnTarget/dependencies.lock | 10 + test/unitOnTarget/main/CMakeLists.txt | 4 + test/unitOnTarget/main/makefile | 7 + test/unitOnTarget/main/unitTest.cpp | 43 + test/unitOnTarget/sdkconfig.defaults | 3 + tools/.gitignore | 1 + tools/trajectoryGen/trajectoryGen.py | 54 + tools/trajectoryGen/utils/defines.py | 33 + tools/trajectoryGen/utils/generator.py | 180 ++ 116 files changed, 14510 insertions(+), 590 deletions(-) create mode 100644 .clangd create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .vscode/cmake-kits.json create mode 100644 components/spit/CMakeLists.txt create mode 100644 components/spit/Notes.md create mode 100644 components/spit/SPIT_Protocol_en_20180211_1630.pdf create mode 100644 components/spit/include/sad_DetectedServer.hpp create mode 100644 components/spit/include/spit_client.hpp create mode 100644 components/spit/include/spit_message.hpp create mode 100644 components/spit/include/spit_object.hpp create mode 100644 components/spit/include/spit_player.hpp create mode 100644 components/spit/include/spit_processor.hpp create mode 100644 components/spit/include/spit_repo.hpp create mode 100644 components/spit/include/spit_socket.hpp create mode 100644 components/spit/include/spit_type.hpp create mode 100644 components/spit/src/esp/from create mode 100644 components/spit/src/esp/license create mode 100644 components/spit/src/esp/uuid.c create mode 100644 components/spit/src/esp/uuid.h create mode 100644 components/spit/src/spit_client.cpp create mode 100644 components/spit/src/spit_message.cpp create mode 100644 components/spit/src/spit_object.cpp create mode 100644 components/spit/src/spit_player.cpp create mode 100644 components/spit/src/spit_processor.cpp create mode 100644 components/spit/src/spit_socket.cpp create mode 100644 components/spit/test/CMakeLists.txt create mode 100644 components/spit/test/test_spit_client.cpp create mode 100644 components/spit/tools/spit.lua create mode 100644 components/spit/uTest/CMakeLists.txt create mode 100644 components/spit/uTest/messageTest.cpp create mode 100644 components/spit/uTest/spitObjectTest.cpp create mode 100644 components/spit/uTest/spitProcessorTest.cpp create mode 100644 components/spit/uTest/spitProcessor_complexTest.cpp create mode 100644 components/spit/uTest/spitRepoTest.cpp create mode 100644 components/spit/uTest/spitTestMain.cpp create mode 100644 components/spit/uTest/spitTypeTest.cpp create mode 100644 components/watchFace/CMakeLists.txt create mode 100644 components/watchFace/include/watchFace.hpp create mode 100644 components/watchFace/src/watchFace.cpp create mode 100644 components/watchFace/uTest/CMakeLists.txt create mode 100644 components/watchFace/uTest/WatchFaceUTest.cpp create mode 160000 core create mode 100644 cspell.json create mode 100644 dependencies.lock create mode 100644 doc/Gegengier_Engineering_CatasWatch.dvd create mode 100644 doc/InitRun.md create mode 100644 doc/LSS-Icon.afdesign create mode 100644 doc/LSS-Icon.jpg create mode 100644 doc/LiveShowProject/CatasWatchProject.dpr create mode 100644 doc/LiveShowProject/CatasWatchProject.ldv create mode 100644 doc/LiveShowProject/CatasWatchProject.lms create mode 100644 doc/LiveShowProject/CatasWatchProject.lmv create mode 100644 doc/LiveShowProject/CatasWatchProject.lsb create mode 100644 doc/LiveShowProject/CatasWatchProject.lsc create mode 100644 doc/LiveShowProject/CatasWatchProject.lsd create mode 100644 doc/LiveShowProject/CatasWatchProject.lse create mode 100644 doc/LiveShowProject/CatasWatchProject.lsf create mode 100644 doc/LiveShowProject/CatasWatchProject.lsh create mode 100644 doc/LiveShowProject/CatasWatchProject.lsj create mode 100644 doc/LiveShowProject/CatasWatchProject.lsp create mode 100644 doc/LiveShowProject/CatasWatchProject.lss create mode 100644 doc/LiveShowProject/CatasWatchProject.lsu create mode 100644 doc/LiveShowProject/CatasWatchProject.lsv create mode 100644 doc/LiveShowProject/CatasWatchProject.lsx create mode 100644 doc/LiveShowProject/CatasWatchProject.rbo create mode 100644 doc/LiveShowProject/CatasWatchProject.rtf create mode 100644 doc/LiveShowProject/CatasWatchProject.spi create mode 100644 doc/LiveShowProject/CatasWatchProject.ssj create mode 100644 doc/LiveShowProject/CatasWatchProject.uip create mode 100644 esp_idf_project_configuration.json delete mode 100644 eth2ap.png create mode 100644 main/config.hpp delete mode 100644 main/ethernet_example_main.c create mode 100644 main/lighsMain.hpp create mode 100644 main/lightsMain.cpp create mode 100644 makefile create mode 100644 managed_components/frozen-eye__esp_telnet/.component_hash create mode 100644 managed_components/frozen-eye__esp_telnet/.gitignore create mode 100644 managed_components/frozen-eye__esp_telnet/AUTHORS create mode 100644 managed_components/frozen-eye__esp_telnet/CHECKSUMS.json create mode 100644 managed_components/frozen-eye__esp_telnet/CMakeLists.txt create mode 100644 managed_components/frozen-eye__esp_telnet/Kconfig create mode 100644 managed_components/frozen-eye__esp_telnet/LICENSE create mode 100644 managed_components/frozen-eye__esp_telnet/idf_component.yml create mode 100644 managed_components/frozen-eye__esp_telnet/include/telnet/server.h create mode 100644 managed_components/frozen-eye__esp_telnet/src/libtelnet.c create mode 100644 managed_components/frozen-eye__esp_telnet/src/libtelnet.h create mode 100644 managed_components/frozen-eye__esp_telnet/src/server.c create mode 100644 sdkconfig create mode 100644 sdkconfig.defaults create mode 100644 startCode.sh create mode 100644 test/integration/CMakeLists.txt create mode 100644 test/integration/main/CMakeLists.txt create mode 100644 test/integration/main/movementTests.cpp create mode 100644 test/unitOnHost/CMakeLists.txt create mode 100644 test/unitOnTarget/CMakeLists.txt create mode 100644 test/unitOnTarget/ReadMe.md create mode 100644 test/unitOnTarget/dependencies.lock create mode 100644 test/unitOnTarget/main/CMakeLists.txt create mode 100644 test/unitOnTarget/main/makefile create mode 100644 test/unitOnTarget/main/unitTest.cpp create mode 100644 test/unitOnTarget/sdkconfig.defaults create mode 100644 tools/.gitignore create mode 100644 tools/trajectoryGen/trajectoryGen.py create mode 100644 tools/trajectoryGen/utils/defines.py create mode 100644 tools/trajectoryGen/utils/generator.py diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..437f255 --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Remove: [-f*, -m*] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2da1b12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build* +.build* \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c19f9a1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "core"] + path = core + url = git@github.com:Gustice/EspIdfDevicePlatform.git diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 5610178..d1be0d7 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,23 +1,22 @@ { - "configurations": [ - { - "name": "ESP-IDF", - "compilerPath": "${config:idf.toolsPathWin}\\tools\\xtensa-esp-elf\\esp-13.2.0_20240530\\xtensa-esp-elf\\bin\\xtensa-esp32-elf-gcc.exe", - "compileCommands": "${config:idf.buildPath}/compile_commands.json", - "includePath": [ - "${config:idf.espIdfPath}/components/**", - "${config:idf.espIdfPathWin}/components/**", - "${workspaceFolder}/**" - ], - "browse": { - "path": [ - "${config:idf.espIdfPath}/components", - "${config:idf.espIdfPathWin}/components", - "${workspaceFolder}" - ], - "limitSymbolsToIncludedHeaders": true - } - } - ], - "version": 4 + "configurations": [ + { + "name": "ESP-IDF", + "compilerPath": "C:\\Espressif\\tools\\xtensa-esp-elf\\esp-14.2.0_20250730\\xtensa-esp-elf\\bin\\xtensa-esp32-elf-gcc.exe", + "compileCommands": "${config:idf.buildPath}/compile_commands.json", + "intelliSenseMode": "gcc-x86", + "includePath": [ + "${workspaceFolder}/**", + "${workspaceFolder}/core/**" + ], + "browse": { + "path": [ + "${workspaceFolder}", + "${workspaceFolder}/core" + ], + "limitSymbolsToIncludedHeaders": true + } + } + ], + "version": 4 } diff --git a/.vscode/cmake-kits.json b/.vscode/cmake-kits.json new file mode 100644 index 0000000..d1aa225 --- /dev/null +++ b/.vscode/cmake-kits.json @@ -0,0 +1,30 @@ +[ + { + "name": "GCC 14.3.0 x86_64-linux-gnu", + "compilers": { + "C": "/usr/bin/gcc-14", + "CXX": "/usr/bin/g++-14" + }, + "isTrusted": true + }, + { + "name": "Host GCC", + "description": "GCC 15.2.0 x86_64-linux-gnu", + "compilers": { + "C": "/usr/bin/gcc", + "CXX": "/usr/bin/g++" + }, + "cmakeSettings": { + "TgtEnv": "Host" + }, + "isTrusted": true + }, + { + "name": "Esp32 Tests", + "environmentSetupScript": "/home/gustice/esp/v5.4.1/esp-idf/export.sh", + "cmakeSettings": { + "TgtEnv": "Target" + }, + "isTrusted": true + } +] \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 2511a38..3694ae4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,11 +5,6 @@ "type": "gdbtarget", "request": "attach", "name": "Eclipse CDT GDB Adapter" - }, - { - "type": "espidf", - "name": "Launch", - "request": "launch" } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f7119b3..01c91b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,82 @@ { - "C_Cpp.intelliSenseEngine": "default", - "idf.adapterTargetName": "esp32", - "idf.customExtraPaths": "C:\\Tools\\ESP\\tools\\xtensa-esp-elf-gdb\\14.2_20240403\\xtensa-esp-elf-gdb\\bin;C:\\Tools\\ESP\\tools\\riscv32-esp-elf-gdb\\14.2_20240403\\riscv32-esp-elf-gdb\\bin;C:\\Tools\\ESP\\tools\\xtensa-esp-elf\\esp-13.2.0_20240530\\xtensa-esp-elf\\bin;C:\\Tools\\ESP\\tools\\riscv32-esp-elf\\esp-13.2.0_20240530\\riscv32-esp-elf\\bin;C:\\Tools\\ESP\\tools\\esp32ulp-elf\\2.38_20240113\\esp32ulp-elf\\bin;C:\\Tools\\ESP\\tools\\cmake\\3.24.0\\bin;C:\\Tools\\ESP\\tools\\openocd-esp32\\v0.12.0-esp32-20240318\\openocd-esp32\\bin;C:\\Tools\\ESP\\tools\\ninja\\1.11.1;C:\\Tools\\ESP\\tools\\idf-exe\\1.0.3;C:\\Tools\\ESP\\tools\\ccache\\4.8\\ccache-4.8-windows-x86_64;C:\\Tools\\ESP\\tools\\dfu-util\\0.11\\dfu-util-0.11-win64;C:\\Tools\\ESP\\tools\\esp-rom-elfs\\20240305", - "idf.customExtraVars": { - "OPENOCD_SCRIPTS": "C:\\Tools\\ESP\\tools\\openocd-esp32\\v0.12.0-esp32-20240318/openocd-esp32/share/openocd/scripts", - "IDF_CCACHE_ENABLE": "1", - "ESP_ROM_ELF_DIR": "C:\\Tools\\ESP\\tools\\esp-rom-elfs\\20240305/" + "C_Cpp.clang_format_fallbackStyle": "{ BasedOnStyle: LLVM, UseTab: Never, IndentWidth: 4, TabWidth: 4, AllowShortIfStatementsOnASingleLine: false, IndentCaseLabels: false, ColumnLimit: 0, AccessModifierOffset: -2}", + "C_Cpp.intelliSenseEngine": "default", + "idf.customExtraVars": { + "OPENOCD_SCRIPTS": "/home/gustice/tools/.espressif/tools/openocd-esp32/v0.12.0-esp32-20241016/openocd-esp32/share/openocd/scripts/", + "IDF_CCACHE_ENABLE": "1", + "ESP_ROM_ELF_DIR": "/home/gustice/tools/.espressif/tools/esp-rom-elfs/20241011/", + "IDF_TARGET": "esp32" + }, + "idf.openOcdConfigs": [ + "interface/ftdi/esp_ftdi.cfg", + "target/esp32.cfg" + // "board/esp32-wrover-kit-3.3v.cfg" + ], + "idf.flashType": "UART", + "C_Cpp.autoAddFileAssociations": false, + "project.deviceIp": "172.16.17.56", + "sonarlint.pathToCompileCommands": "${workspaceFolder}\\build\\compile_commands.json", + "idf.portWin": "COM4", + "cmake.showSystemKits": false, + "cmake.defaultVariants": { + "buildType": { + "default": "debug", + "description": "The build type.", + "choices": { + "debug": { + "short": "Debug", + "long": "Disable optimizations - include debug information.", + "buildType": "Debug" + }, + "release": { + "short": "Release", + "long": "Optimize for speed - exclude debug information.", + "buildType": "Release" + } + } + } + }, + "clangd.path": "C:\\Espressif\\tools\\esp-clang\\esp-18.1.2_20240912\\esp-clang\\bin\\clangd.exe", + "clangd.arguments": [ + "--background-index", + "--query-driver=**", + "--compile-commands-dir=c:\\Users\\Jakob\\hello_world\\build" + ], + "cmake.configureOnOpen": false, + "cmake.configureOnEdit": false, + "cmake.automaticReconfigure": false, + "cmake.autoSelectActiveFolder": false, + "cmake.debugConfig": { + "setupCommands": [ + { + "description": "Skip standard library code", + "text": "skip file /usr/include/*" + }, + { + "description": "Skip GCC internal code", + "text": "skip file /usr/lib/gcc/*" + }, + { + "description": "Skip std namespace", + "text": "skip namespace std" + } + ] + }, + "cmake.options.advanced": { + "build": { + "statusBarVisibility": "inherit", + "inheritDefault": "hidden" }, - "idf.espIdfPathWin": "C:\\Tools\\ESP-IDF\\v5.3\\esp-idf", - "idf.openOcdConfigs": [ - "board/esp32-wrover-kit-3.3v.cfg" - ], - "idf.portWin": "COM4", - "idf.pythonBinPathWin": "C:\\Tools\\ESP\\python_env\\idf5.3_py3.11_env\\Scripts\\python.exe", - "idf.toolsPathWin": "C:\\Tools\\ESP" + "launch": { + "statusBarVisibility": "inherit", + "inheritDefault": "hidden" + }, + "debug": { + "statusBarVisibility": "inherit", + "inheritDefault": "hidden" + } + }, + + "idf.currentSetup": "C:\\esp\\v5.4.3\\esp-idf", + "idf.port": "COM4" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1dc7915..76e5245 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,259 +1,7 @@ { "version": "2.0.0", "tasks": [ - { - "label": "Build - Build project", - "type": "shell", - "command": "${config:idf.pythonBinPath} ${config:idf.espIdfPath}/tools/idf.py build", - "windows": { - "command": "${config:idf.pythonBinPathWin} ${config:idf.espIdfPathWin}\\tools\\idf.py build", - "options": { - "env": { - "PATH": "${env:PATH};${config:idf.customExtraPaths}" - } - } - }, - "options": { - "env": { - "PATH": "${env:PATH}:${config:idf.customExtraPaths}" - } - }, - "problemMatcher": [ - { - "owner": "cpp", - "fileLocation": [ - "autoDetect", - "${workspaceFolder}" - ], - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - } - ], - "group": { - "kind": "build", - "isDefault": true - } - }, - { - "label": "Set ESP-IDF Target", - "type": "shell", - "command": "${command:espIdf.setTarget}", - "problemMatcher": { - "owner": "cpp", - "fileLocation": [ - "autoDetect", - "${workspaceFolder}" - ], - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - } - }, - { - "label": "Clean - Clean the project", - "type": "shell", - "command": "${config:idf.pythonBinPath} ${config:idf.espIdfPath}/tools/idf.py fullclean", - "windows": { - "command": "${config:idf.pythonBinPathWin} ${config:idf.espIdfPathWin}\\tools\\idf.py fullclean", - "options": { - "env": { - "PATH": "${env:PATH};${config:idf.customExtraPaths}" - } - } - }, - "options": { - "env": { - "PATH": "${env:PATH}:${config:idf.customExtraPaths}" - } - }, - "problemMatcher": [ - { - "owner": "cpp", - "fileLocation": [ - "autoDetect", - "${workspaceFolder}" - ], - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - } - ] - }, - { - "label": "Flash - Flash the device", - "type": "shell", - "command": "${config:idf.pythonBinPath} ${config:idf.espIdfPath}/tools/idf.py -p ${config:idf.port} -b ${config:idf.flashBaudRate} flash", - "windows": { - "command": "${config:idf.pythonBinPathWin} ${config:idf.espIdfPathWin}\\tools\\idf.py flash -p ${config:idf.portWin} -b ${config:idf.flashBaudRate}", - "options": { - "env": { - "PATH": "${env:PATH};${config:idf.customExtraPaths}" - } - } - }, - "options": { - "env": { - "PATH": "${env:PATH}:${config:idf.customExtraPaths}" - } - }, - "problemMatcher": [ - { - "owner": "cpp", - "fileLocation": [ - "autoDetect", - "${workspaceFolder}" - ], - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - } - ] - }, - { - "label": "Monitor: Start the monitor", - "type": "shell", - "command": "${config:idf.pythonBinPath} ${config:idf.espIdfPath}/tools/idf.py -p ${config:idf.port} monitor", - "windows": { - "command": "${config:idf.pythonBinPathWin} ${config:idf.espIdfPathWin}\\tools\\idf.py -p ${config:idf.portWin} monitor", - "options": { - "env": { - "PATH": "${env:PATH};${config:idf.customExtraPaths}" - } - } - }, - "options": { - "env": { - "PATH": "${env:PATH}:${config:idf.customExtraPaths}" - } - }, - "problemMatcher": [ - { - "owner": "cpp", - "fileLocation": [ - "autoDetect", - "${workspaceFolder}" - ], - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - } - ], - "dependsOn": "Flash - Flash the device" - }, - { - "label": "OpenOCD: Start openOCD", - "type": "shell", - "presentation": { - "echo": true, - "reveal": "never", - "focus": false, - "panel": "new" - }, - "command": "openocd -s ${command:espIdf.getOpenOcdScriptValue} ${command:espIdf.getOpenOcdConfigs}", - "windows": { - "command": "openocd.exe -s ${command:espIdf.getOpenOcdScriptValue} ${command:espIdf.getOpenOcdConfigs}", - "options": { - "env": { - "PATH": "${env:PATH};${config:idf.customExtraPaths}" - } - } - }, - "options": { - "env": { - "PATH": "${env:PATH}:${config:idf.customExtraPaths}" - } - }, - "problemMatcher": { - "owner": "cpp", - "fileLocation": [ - "autoDetect", - "${workspaceFolder}" - ], - "pattern": { - "regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", - "file": 1, - "line": 2, - "column": 3, - "severity": 4, - "message": 5 - } - } - }, - { - "label": "adapter", - "type": "shell", - "command": "${config:idf.pythonBinPath}", - "isBackground": true, - "options": { - "env": { - "PATH": "${env:PATH}:${config:idf.customExtraPaths}", - "PYTHONPATH": "${command:espIdf.getExtensionPath}/esp_debug_adapter/debug_adapter" - } - }, - "problemMatcher": { - "background": { - "beginsPattern": "\bDEBUG_ADAPTER_STARTED\b", - "endsPattern": "DEBUG_ADAPTER_READY2CONNECT", - "activeOnStart": true - }, - "pattern": { - "regexp": "(\\d+)-(\\d+)-(\\d+)\\s(\\d+):(\\d+):(\\d+),(\\d+)\\s-(.+)\\s(ERROR)", - "file": 8, - "line": 2, - "column": 3, - "severity": 4, - "message": 9 - } - }, - "args": [ - "${command:espIdf.getExtensionPath}/esp_debug_adapter/debug_adapter_main.py", - "-e", - "${workspaceFolder}/build/${command:espIdf.getProjectName}.elf", - "-s", - "$OPENOCD_SCRIPTS", - "-dn", - "esp32", - "-om", - "connect_to_instance", - "-t", - "xtensa-esp32-elf-" - ], - "windows": { - "command": "${config:idf.pythonBinPathWin}", - "options": { - "env": { - "PATH": "${env:PATH};${config:idf.customExtraPaths}", - "PYTHONPATH": "${command:espIdf.getExtensionPath}/esp_debug_adapter/debug_adapter" - } - } - } - } + ] } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 0678268..2270884 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,25 @@ cmake_minimum_required(VERSION 3.16) -include($ENV{IDF_PATH}/tools/cmake/project.cmake) -project(X_WatchController-FW) +if(TgtEnv STREQUAL "Host") + message("Using Host-Branch") + enable_testing() + add_subdirectory("test/unitOnHost") +elseif(TgtEnv STREQUAL "Target") + # add_subdirectory("test/unitOnTarget") + add_subdirectory("core/test/unitOnTarget") +else() + message("Using ESP-Branch") + if(DEFINED ENV{UNIT_TESTS}) + message("Using UnitTestFrame") + add_subdirectory("test/unitOnTarget" ${CMAKE_BINARY_DIR}/main) + else() + set(EXTRA_COMPONENT_DIRS + "core/components" + "core/components_extern" + "core/managed_components" + ) + include($ENV{IDF_PATH}/tools/cmake/project.cmake) + message("Using Main App") + project(WatchController) + endif() +endif() diff --git a/README.md b/README.md index 861ddc8..2e4e2e7 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-S2 | ESP32-S3 | -| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C6 | ESP32-H2 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | -# eth2ap Example -(See the README.md file in the upper level 'examples' directory for more information about examples. To try a more complex application about Ethernet to WiFi data forwarding, please go to [iot-solution](https://github.com/espressif/esp-iot-solution/tree/release/v1.0/examples/eth2wifi).) - -**Note:** This example uses some internal APIs (e.g. `esp_wifi_internal_tx`) which might get changed between minor versions of ESP-IDF. +# Ethernet Example +(See the README.md file in the upper level 'examples' directory for more information about examples.) ## Overview -![eth2ap](eth2ap.png) -The similarities on MAC layer between Ethernet and Wi-Fi make it easy to forward packets from Ethernet to Wi-Fi and vice versa. This example illustrates how to implement a simple "router" which only supports forwarding packets between Ethernet port and Wi-Fi AP interface. In this case, the Ethernet should play the role of WAN (i.e. it can access outside network) so that a mobile device could get access to the Internet when it gets connected to ESP32 through Wi-Fi. +This example demonstrates basic usage of `Ethernet driver` together with `esp_netif`. Initialization of the `Ethernet driver` is wrapped in separate [sub-component](./components/ethernet_init/ethernet_init.c) of this project to clearly distinguish between the driver's and `esp_netif` initializations. The work flow of the example could be as follows: -**Note:** In this example, ESP32 works like a *bridge* between Ethernet and Wi-Fi, and it won't perform any actions on Layer3 and higher layer, which means there's no need to initialize the TCP/IP stack. +1. Install Ethernet driver +2. Attach the driver to `esp_netif` +3. Send DHCP requests and wait for a DHCP lease +4. If get IP address successfully, then you will be able to ping the device + +If you have a new Ethernet application to go (for example, connect to IoT cloud via Ethernet), try this as a basic template, then add your own code. ## How to use example @@ -19,23 +21,26 @@ The similarities on MAC layer between Ethernet and Wi-Fi make it easy to forward To run this example, it's recommended that you have an official ESP32 Ethernet development board - [ESP32-Ethernet-Kit](https://docs.espressif.com/projects/esp-idf/en/latest/hw-reference/get-started-ethernet-kit.html). This example should also work for 3rd party ESP32 board as long as it's integrated with a supported Ethernet PHY chip. Up until now, ESP-IDF supports up to four Ethernet PHY: `LAN8720`, `IP101`, `DP83848` and `RTL8201`, additional PHY drivers should be implemented by users themselves. -Besides that, `esp_eth` component can drive third-party Ethernet module which integrates MAC and PHY and provides common communication interface (e.g. SPI, USB, etc). This example will take the **DM9051** as an example, illustrating how to install the Ethernet driver in the same manner. +Besides that, `esp_eth` component can drive third-party Ethernet module which integrates MAC and PHY and provides common communication interface (e.g. SPI, USB, etc). This example will take the `DM9051`, `W5500` or `KSZ8851SNL` SPI modules as an example, illustrating how to install the Ethernet driver in the same manner. + +The ESP-IDF supports the usage of multiple Ethernet interfaces at a time when external modules are utilized which is also demonstrated by this example. There are several options you can combine: + * Internal EMAC and one SPI Ethernet module. + * Two SPI Ethernet modules of the same type connected to single SPI interface and accessed by switching appropriate CS. + * Internal EMAC and two SPI Ethernet modules of the same type. #### Pin Assignment See common pin assignments for Ethernet examples from [upper level](../README.md#common-pin-assignments). +When using two Ethernet SPI modules at a time, they are to be connected to single SPI interface. Both modules then share data (MOSI/MISO) and CLK signals. However, the CS, interrupt and reset pins need to be connected to separate GPIO for each Ethernet SPI module. + ### Configure the project ``` idf.py menuconfig ``` -In addition to the common configurations for Ethernet examples from [upper level](../README.md#common-configurations), you might also need to update the default value of following configurations: - -In the `Example Configuration` menu: -* Set the SSID and password for Wi-Fi ap interface under `Wi-Fi SSID` and `Wi-Fi Password`. -* Set the maximum connection number under `Maximum STA connections`. +See common configurations for Ethernet examples from [upper level](../README.md#common-configurations). ### Build, Flash, and Run @@ -53,62 +58,23 @@ See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/l ## Example Output -### Step 1: Initialize Ethernet and Wi-Fi (AP mode) - ```bash -I (508) example: Power On Ethernet PHY -I (518) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE -I (518) emac: emac reset done -I (518) example: Ethernet Started -...... -I (538) wifi: wifi driver task: 3ffc7fbc, prio:23, stack:3584, core=0 -I (538) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE -I (538) system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE -I (568) wifi: wifi firmware version: ec61a20 -I (568) wifi: config NVS flash: enabled -I (568) wifi: config nano formating: disabled -I (568) wifi: Init dynamic tx buffer num: 32 -I (568) wifi: Init data frame dynamic rx buffer num: 32 -I (578) wifi: Init management frame dynamic rx buffer num: 32 -I (588) wifi: Init management short buffer num: 32 -I (588) wifi: Init static rx buffer size: 1600 -I (588) wifi: Init static rx buffer num: 10 -I (598) wifi: Init dynamic rx buffer num: 32 +I (394) eth_example: Ethernet Started +I (3934) eth_example: Ethernet Link Up +I (3934) eth_example: Ethernet HW Addr 30:ae:a4:c6:87:5b +I (5864) esp_netif_handlers: eth ip: 192.168.2.151, mask: 255.255.255.0, gw: 192.168.2.2 +I (5864) eth_example: Ethernet Got IP Address +I (5864) eth_example: ~~~~~~~~~~~ +I (5864) eth_example: ETHIP:192.168.2.151 +I (5874) eth_example: ETHMASK:255.255.255.0 +I (5874) eth_example: ETHGW:192.168.2.2 +I (5884) eth_example: ~~~~~~~~~~~ ``` -### Step 2: Ethernet Connects to Router/Switch/PC (with DHCP server enabled) - -```bash -I (4518) example: Ethernet Link Up -``` - -### Step 3: Start Wi-Fi AP - -```bash -I (4618) phy: phy_version: 4100, 2a5dd04, Jan 23 2019, 21:00:07, 0, 0 -I (4618) wifi: mode : softAP (30:ae:a4:c6:87:5b) -I (4628) wifi: Total power save buffer number: 16 -I (4628) wifi: Init max length of beacon: 752/752 -I (4628) wifi: Init max length of beacon: 752/752 -``` - -### Step 4: Wi-Fi station (e.g. mobile phone) connects to ESP32's Wi-Fi - -```bash -I (10168) wifi: new:<1,0>, old:<1,0>, ap:<1,1>, sta:<255,255>, prof:1 -I (10168) wifi: station: c4:0b:cb:ec:9a:84 join, AID=1, bgn, 20 -I (10258) example: AP got a station connected -``` - -Now your mobile phone should get access to the Internet. +Now you can ping your ESP32 in the terminal by entering `ping 192.168.2.151` (it depends on the actual IP address you get). ## Troubleshooting See common troubleshooting for Ethernet examples from [upper level](../README.md#common-troubleshooting). -* If you got error message like `WiFi send packet failed` when running the example, you may need to enlarge the value of `FLOW_CONTROL_WIFI_SEND_DELAY_MS` in "ethernet_example_main.c", because Ethernet process packets faster than Wi-Fi on ESP32. -* If you got error message like `send flow control message failed or timeout` when running the example, you may need to enlarge the value of `FLOW_CONTROL_QUEUE_LENGTH` in "ethernet_example_main". -* Wi-Fi station doesn't receive any IP via DHCP? - * All Layer 3 (TCP/IP functions) on the ESP32 are disabled, including the SoftAP DHCP server. This means that devices must be able to access another DHCP server (for example on a Wi-Fi router connected via ethernet) or should use statically assigned IP addresses. - -(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.) +(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.) \ No newline at end of file diff --git a/components/spit/CMakeLists.txt b/components/spit/CMakeLists.txt new file mode 100644 index 0000000..8c4e373 --- /dev/null +++ b/components/spit/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "src" "src/esp" + INCLUDE_DIRS "include" + REQUIRES esp_system esp_netif freeRtosApi sockets +) diff --git a/components/spit/Notes.md b/components/spit/Notes.md new file mode 100644 index 0000000..231a3ff --- /dev/null +++ b/components/spit/Notes.md @@ -0,0 +1,11 @@ +# SPIT + +Original implementation from Hag seem to be not suited for Embedded devices. + +- Supports IPv4 only +- Reduced Threads to a minimum +- Then reduced usage of Mutexes to a minimum +- Reduced Sockets to a minimum +- Introduced containers and removed plain new and delete +- Removed UUID-Generation because SPIT-Types never change on embedded devices +- Callbacks instead of interfaces where suitable \ No newline at end of file diff --git a/components/spit/SPIT_Protocol_en_20180211_1630.pdf b/components/spit/SPIT_Protocol_en_20180211_1630.pdf new file mode 100644 index 0000000000000000000000000000000000000000..570220355e80a643fb5dfca0e432a5612b7496bc GIT binary patch literal 253397 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
Xy* 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 aPuxAM+&qwF2ykSFLT3xf$!Y0n3;jRgf-=(p literal 0 HcmV?d00001 diff --git a/components/spit/include/sad_DetectedServer.hpp b/components/spit/include/sad_DetectedServer.hpp new file mode 100644 index 0000000..e07f5a2 --- /dev/null +++ b/components/spit/include/sad_DetectedServer.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include +#include +#include +#include + +using namespace std; + +namespace spit { + +class SadDetectedServer; +class ISadClient { + public: + virtual void eventServerDetected(SadDetectedServer &server) = 0; + virtual void eventServerChanged(SadDetectedServer &server) = 0; + virtual void eventServerLost(SadDetectedServer &server) = 0; +}; + +class SadDetectedServer { + public: + SadDetectedServer(string name, string iD, string computerName, + string iP, string port, string clientIP, + string additions, string projName) + : mName(name), + mID(iD), + mComputerName(computerName), + mIP(iP), + mPort(port), + mClientIP(clientIP), + mAdditions(additions), + mServerProjectName(projName) { + } + + private: + string mName{}; + string mID{}; + string mComputerName{}; + string mIP{}; + string mPort{}; + string mClientIP{}; + string mAdditions{}; + string mServerProjectName{}; + bool mNew{true}; + bool mChanged{}; + bool mFoundAgain{}; + + public: + string getID() const { return mID; } + string getName() const { return mName; } + string getComputerName() const { return mComputerName; } + string getIP() const { return mIP; } + string getPort() const { return mPort; } + string getClientIP() const { return mClientIP; } + string getAdditions() const { return mAdditions; } + string getProjectName() const { return mServerProjectName; } + bool getNew() const { return mNew; } + bool getChanged() const { return mChanged; } + bool getFoundAgain() const { return mFoundAgain; } + + void setChanged() { mChanged = true; } + void setKnown() { mNew = false; } + void setFoundAgain() { mFoundAgain = true; } + void updateProjectName(string newProjectName) { + mServerProjectName = newProjectName; + if (mServerProjectName != newProjectName) { + mChanged = true; + } + } + + // SAD_Client mThreadSend will send to Multicast addresses + void resetFlags() { + mChanged = false; + mFoundAgain = false; + mNew = false; + } + + private: + static unordered_map> sKnownServers; + + public: + enum ParamIndex { + PARAM_PRECODE = 0, + PARAM_SPITTASKID = 1, + PARAM_NAME = 2, + PARAM_ID = 3, + PARAM_COMPUTERNAME = 4, + PARAM_PORT = 5, + PARAM_CLIENTIP = 6, + PARAM_ADDITIONS = 7, + PARAM_SERVERPROJECTNAME = 8, + PARAM_IP = 9, + }; + + /** + * Used by SAD_Client to add a found SPITServer - don't use this manually + * @param id the id of the server, every instance of a SPITServer has an own id + * @param p_ServerParams the server's parameter + */ + static void addDetectedServer(unique_ptr detected) { + if (auto i = sKnownServers.find(detected->getID()); i != sKnownServers.end()) { + auto &known = *(i->second.get()); + known.setFoundAgain(); + if (detected->getProjectName() != known.getProjectName()) { + known.updateProjectName(detected->getProjectName()); + } + return; + } + sKnownServers[detected->getID()] = move(detected); + } + static void removeDetectedServer(SadDetectedServer &server) { + auto i = sKnownServers.find(server.getID()); + if (i != sKnownServers.end()) { + sKnownServers.erase(i); + } + } + static void clearDetectedServers() { + sKnownServers.clear(); + } + + static void resetServerFlags() { + for (auto &srv : sKnownServers) { + srv.second->resetFlags(); + } + } + /** + * Used by SAD_Client internally -don't use this manually + */ + static void evaluateDetectedServers(ISadClient &sadClient) { + for (auto &srv : sKnownServers) { + SadDetectedServer &known = *srv.second; + if (known.getNew()) { + known.setKnown(); + sadClient.eventServerDetected(known); + } else if (known.getChanged()) { + sadClient.eventServerChanged(known); + } else if (known.getFoundAgain() == false) { + sadClient.eventServerLost(known); + removeDetectedServer(known); + } + } + } + + /** + * Returns a reference of the DetectedServer with the id p_ServerID.
+ * If no DetectedServer is found an eyception is thrown. + * @param id the id of the SPITServer + * @return the reference of the SadDetectedServer instance with getID() == p_ServerID + */ + static const SadDetectedServer &getRefDetectedServer(const string &id) { + return *sKnownServers.at(id); + } +}; + +} \ No newline at end of file diff --git a/components/spit/include/spit_client.hpp b/components/spit/include/spit_client.hpp new file mode 100644 index 0000000..5cc8e4f --- /dev/null +++ b/components/spit/include/spit_client.hpp @@ -0,0 +1,203 @@ +#pragma once + +#include "sad_DetectedServer.hpp" +#include "spit_message.hpp" +#include "spit_object.hpp" +#include "spit_player.hpp" +#include "spit_processor.hpp" +#include "spit_repo.hpp" +#include "spit_socket.hpp" +#include "spit_type.hpp" +#include +#include +#include + +using namespace std; + +namespace spit { + +class SClient : ISadClient { + public: + using Callbacks = SProcessor::Callbacks; + using ProjUpdate = SProcessor::ProjUpdate; + using ServerEvent = SProcessor::ServerEvent; + using ObjectEvent = SProcessor::ObjectEvent; + using Connection = SProcessor::Connection; + + using PlayerType = SPlayer::PTypes; + using ValueFlags = SPlayer::VFlags; + + SClient(string name, string projName, ISocket &socket, Callbacks calls); + virtual ~SClient(); + + + private: + Callbacks mCallbacks; + fos::TaskT *mThreadReceive{}; + fos::TaskT *mThreadProcess{}; + bool mStopReceiveMessages{}; + + Connection mConnectionStatus{Connection::None}; + bool mDisconnectedByServer{}; // flag true = when server disconnects + + SProcessor mProcessor; + + string mClientProjectName; + + uint8_t mMessageNumberSend{1}; + int mMessageNumberReceived{-1}; // set to -1 because the first received message will set the number (1..255) + + // Server's info filled by parseConnectAnswer + string mServerID{}; + string mServerName{}; + string mServerComputerName{}; + int mServerStepTime{-1}; + + public: + /** The ID of the client.
Every client instance should set an unique id*/ + string getID() { return mProcessor.getID(); } + /** The name of the client, should be set by the user app*/ + string getName() { return mProcessor.getName(); } + /** The name of the client's host computer*/ + string getComputerName() { return mProcessor.getComputerName(); } + void setClientProjectName(string clientProjectName) { mClientProjectName = clientProjectName; } + string getClientProjectName() { return mClientProjectName; } + + // connected Server Infos + /** The ID of the connected SPITServer*/ + string getServerID() { return mServerID; } + /** The host computername of the connected SPITServer*/ + string getServerComputerName() { return mServerComputerName; } + /** The name of the connected SPITServer*/ + string getServerName() { return mServerName; } + + /** Expected Refreah Rate of the connectd SPITServer.
+ * When the server plays objects, it will do so at discrete time steps.
+ * Play events will take place in these time steps.. + */ + int getServerStepTime() { return mServerStepTime; } + + /** Should be called when the client wants to close his project*/ + void clientProjectClose(); + /** + * Should be called whenever the client has a project loaded + */ + void clientProjectLoaded(); + + void setActionType(string spitypeID, bool actionAssigned) { + mProcessor.setActionType(spitypeID, actionAssigned); + } + void setActionObject(string objectID, bool actionAssigned) { + mProcessor.setActionObject(objectID, actionAssigned); + } + + //--------------- SPITMessage --------------- + private: + bool startReceiveSPITMessages(); + void stopReceiveSPITMessages(); + static void receiveSpitMessage(SClient &rM); + static void processSpitMessage(SClient &rM); + // Reconnect to the server when connection is lost see eventConnectionStatus and eventServerDetected + bool _doReconnect{}; // true = on eventServerDetected connect to the server identified by mServerID + public: + int sendMessage(Message::OPPart &spitMessageOPPart); + + //--------------- ServerAutoDetection SAD --------------- + public: + /** Starts the autodetection of SPITServers in the local network. If a connection is tried, the autodetection should be stopped before.*/ + void startServerAutoDetect(); + /** Stops the autodetection of SPITServers in the local network*/ + void stopServerAutoDetect(); + /** Called every time a new SPITServer is detetced in the local network */ + void eventServerDetected(SadDetectedServer &server); + /** Called every time a prior detected SPITServer is is no longer found.*/ + void eventServerLost(SadDetectedServer &server); + void eventServerChanged(SadDetectedServer &server); + + //--------------- Connection --------------- + public: + /** + * Tries a connection to the SPITServer + * @param p_ServerIP ip address of the SPITServer + * @param p_ServerPort port of the SPITServer + * @param p_ClientIP IP of the client + * @param p_ClientPort if "0" any free port will be used + * @param p_LoginName maybe some SPITServer need a login - liveSHOWsoftware will not. + * @param p_LoginPassword maybe some SPITServer need a login - liveSHOWsoftware will not + * @return true = the connection try is ok, SPITServer will answer with COnnectAnswer and an eventConnectionStatus will occur.
+ * false = something is going wrong. (client could not bind) + */ + bool connect(string serverIP, string serverPort, string clientIP, string clientPort, string loginName = "", string loginPassword = ""); + /** + * Terminates the connection to the SPITServer. + */ + void disconnect(); + + private: + void disconnectInternal(Connection cause); + void resetMessageNumberReceived(); + void parseMessage(Message& message); + void checkParseMessage(unique_ptr message); + void addOvertakingMessage(unique_ptr message); + void parseConnectAnswer(Message::ConnectAnswer &message); + void parseDisconnect(Message::Disconnect &message); + void parseWatchdog(Message::Watchdog &message); + void parseWatchdogAnswer(Message::WatchdogAnswer &message); + + public: + /** Used internally*/ + void sendWatchdog(); + /** Used internally. When the connection to the SPITServer is lost an eventConnectionStatus will occur*/ + void eventWatchdogLost(); + //--------------- SPIT Project --------------- + + /** Returns the SPITServer's project ID - may be the same as the project's name*/ + string getServerProjectID() { return mProcessor.getServerProjectID(); } + /** Returns the SPITServer's project name*/ + string getServerProjectName() { return mProcessor.getServerProjectName(); } + /** Returns frame rate per second. This is the time resolution in which the server transmits time data. The unit of time data is always frames.*/ + int getServerFramesPerSecond() { return mProcessor.getServerFramesPerSecond(); } + + public: + static void sendProject(SClient *p_PtrSClient); + + // --------------- SPIT Types --------------- + public: + /** + * Creates a fixed SPITType for application actions that are always present and project independent.
+ * Fixed SPIT types cannot be changed or deleted by the server.
+ * These SPITTypes will be automatically added to the SPITType-List and sended to the server. + * @param p_ID an unique of the SPITType, these are always the same per client approach. Different client approaches should use different ids. + * @param p_GroupName a name which groups the actions/SPITTypes + * @param p_Name the name of the Action/SPITType + * @param p_DefaultDelay maybe there is an default delay until the action starts, this is only used for graphical representations, 0 is allowd + * @param p_DefaultDuration maybe the action has a default duration, this is only used for graphical representations, 0 is allowd + * @return the pointer to the new SPITType. + */ + SType *createTypeFixed(string iD, string groupName, string name, uint64_t defaultDelay, uint64_t defaultDuration); + + public: + bool changeType(SType *spitType) { return mProcessor.changeType(spitType); } + int removeType(string spitypeID) { return mProcessor.removeType(spitypeID); } + + public: + + public: + bool changeObject(SObject *sObject) { return mProcessor.changeObject(sObject); } + void sendRemoteObject(string objectID, int valuePpm = 0, char valueFlag = 0x00) { + mProcessor.sendRemoteObject(objectID, valuePpm, valueFlag); + } + + private: + static void processSocketTask(SClient &instance); + fos::TaskT *sThread{}; + bool mAbortSearch{}; + bool sAnswered{}; + static fos::Semaphore sStartSad; + ISocket &mSocket; + void answerWatchdog() { + sAnswered = true; + } +}; + +} // namespace spit \ No newline at end of file diff --git a/components/spit/include/spit_message.hpp b/components/spit/include/spit_message.hpp new file mode 100644 index 0000000..7d28095 --- /dev/null +++ b/components/spit/include/spit_message.hpp @@ -0,0 +1,720 @@ +#pragma once + +#include +#include +#include +#include +#include +using namespace std; + +static const string SPITID = "#spit#"; + +namespace spit { + +class Message { + //----------- Header ----------- + public: + /** Maximal length of a SPITMessage*/ + static const int MAX_MESSAGE_SIZE = 512; + + class Header { + public: + static constexpr int BYTESLENGTH = 99; + Header(const Header &orig); + Header(); + + private: + string mID; + int mVersion{SPITVERSION}; + unsigned char mMessageNumber{}; + int mBytesCount{BYTESLENGTH}; + string mServerId{}; + string mClientId{}; + int mOpFlag{-1}; + int mParseOffset{}; + + public: + string getSPITID() const { return mID; } + int getSPITVersion() const { return mVersion; } + unsigned char getMessageNumber() const { return mMessageNumber; } + int getBytesCount() const { return mBytesCount; } + string getServerID() const { return mServerId; } + string getClientID() const { return mClientId; } + int getType() const { return mOpFlag; } + + // Receive + bool parse(const std::vector &message); + + // Send + Header(int p_SPITVersion, int pOpFlag); + void setMessageNumber(unsigned char messageNumber) { mMessageNumber = messageNumber; } + void setServerID(string iD) { mServerId = iD; } + void setClientID(string iD) { mClientId = iD; } + void setOPFlag(int pOpFlag) { mOpFlag = pOpFlag; } + bool fillBytes(std::vector &message); + }; + + //----------- OPPart ----------- + public: + class OPPart { + // Members + protected: + unique_ptr
mHeader{}; + int mParseOffset{}; + int mBytesCount; + + public: + virtual ~OPPart(); + + Header *getHeader() const { return mHeader.get(); } + int getBytesCount() const { return mBytesCount; } + void setServerInfoInHeader(string clientId, string serverId) { + if (!mHeader) { + return; + } + mHeader->setClientID(clientId); + mHeader->setServerID(serverId); + } + void setMessageNumber(uint8_t no) { + if (!mHeader) { + return; + } + mHeader->setMessageNumber(no); + } + + const string getServerID() const { + if (!mHeader) { + return ""; + } + return mHeader->getServerID(); + } + const string getClientID() const { + if (!mHeader) { + return ""; + } + return mHeader->getClientID(); + } + + int getType() const { + if (!mHeader) { + return -1; + } + return mHeader->getType(); + } + + // Receive + OPPart(unique_ptr
header, int pOpFlag, int size); + void setHeader(int opFlag); + virtual bool parse(const std::vector &message) = 0; + + // Send + OPPart(int size); + virtual bool fillBytes(std::vector &message) = 0; + }; + + //----------- Message ----------- + + public: + static constexpr int SPITVERSION = 1; + + // SPIT_Object value usage + static constexpr char VALUEUSE_DISCARD = 0x00; // value of SPITObejct is not used + static constexpr char VALUEUSE_INRANGE = 0x01; // value of SPITOBject in range 0 - 1000000ppm + static constexpr char VALUEUSE_OVERRIDE = 0x02; // value can be overridden + + // SPIT Player type + static constexpr char PLAYERTYPE_MAIN = 0x02; + static constexpr char PLAYERTYPE_CONTINIOUS = 0x01; + static constexpr char PLAYERTYPE_DISCRETE = 0x00; + + // PlayObject value usage + static constexpr char VALUEFLAG_DIRECT = 0x00; // use the vlue direct + static constexpr char VALUEFLAG_INCREASE = 0x01; // increase the value + static constexpr char VALUEFLAG_DECREASE = 0x02; // decrease the value + + Message() = default; + + // Members + private: + unique_ptr oPPart{}; + + // Receive + public: + Message(std::vector message); + + const Header *getHeader() const { return oPPart->getHeader(); } + int getType() const { + if (!oPPart || !oPPart->getHeader()) { + return -1; + } + return oPPart->getHeader()->getType(); + } + const OPPart *getOPPart() const { return oPPart.get(); } + template + T &getAs() const { return *reinterpret_cast(oPPart.get()); } + + private: + bool parse(std::vector message); + + // Send + public: + static std::vector createMessage(OPPart & p_OPPart); + + //------------------------------ Connection ---------------------------------- + + //----------- Connect ----------- + public: + class Connect : public OPPart { + public: + static constexpr int OPFLAG = 1; + static constexpr int BYTESLENGTH = 304; + + private: + string mClientComputerName{}; + string mClientName{}; + string mLoginName{}; + string mLoginPassword{}; + string mServerIp{}; + int mServerPort{}; + + public: + // Receive + Connect(unique_ptr
header); + bool parse(const std::vector &message); + + string getClientComputerName() const { return mClientComputerName; } + string getClientName() const { return mClientName; } + string getLoginName() const { return mLoginName; } + string getLoginPassword() const { return mLoginPassword; } + string getServerIP() const { return mServerIp; } + int getServerPort() const { return mServerPort; } + + // Send + Connect(string p_ComputerName, string p_ClientName); + + void setLoginName(string p_LoginName) { mLoginName = p_LoginName; } + void setLoginPassword(string p_LoginPassword) { mLoginPassword = p_LoginPassword; } + void setServerIP(string p_ServerIP) { mServerIp = p_ServerIP; } + void setServerPort(int p_ServerPort); + bool fillBytes(std::vector &message); + }; + + //----------- ConnectAnswer ----------- + + class ConnectAnswer : public OPPart { + public: + static constexpr int OPFLAG = 2; + static constexpr int BYTESLENGTH = 137; + + private: + string mServerComputerName{}; + string mServerName{}; + bool mConnected{}; + int mStepTime{40}; // ms step size the Server will send play sequences + + public: + ConnectAnswer(const ConnectAnswer &orig); + // Receive + ConnectAnswer(unique_ptr
header); + + bool parse(const std::vector &message); + + const string getServerComputerName() const { return mServerComputerName; } + const string getServerName() const { return mServerName; } + bool getConnected() const { return mConnected; } + int getStepTime() const { return mStepTime; } + + // Send + ConnectAnswer(); + void setServerComputerName(string p_ServerComputerName) { mServerComputerName = p_ServerComputerName; } + void setServerName(string p_ServerName) { mServerName = p_ServerName; } + void setConnected(bool p_Connected) { mConnected = p_Connected; } + void setStepTime(int p_StepTime_ms); + bool fillBytes(std::vector &message); + }; + + //----------- Disconnect ----------- + + class Disconnect : public OPPart { + public: + static constexpr int OPFLAG = 3; + static constexpr int BYTESLENGTH = 4; + + Disconnect(const Disconnect &orig); + // Receive + Disconnect(unique_ptr
header); + bool parse(const std::vector &message); + + // Send + Disconnect(); + bool fillBytes(std::vector &message); + }; + + //----------- Watchdog ----------- + + class Watchdog : public OPPart { + public: + static constexpr int OPFLAG = 8; + static constexpr int BYTESLENGTH = 4; + + Watchdog(const Watchdog &orig); + // Receive + Watchdog(unique_ptr
header); + bool parse(const std::vector &message); + + // Send + Watchdog(); + bool fillBytes(std::vector &message); + }; + + //----------- WatchdogAnswer ----------- + + class WatchdogAnswer : public OPPart { + public: + static constexpr int OPFLAG = 9; + static constexpr int BYTESLENGTH = 4; + + // Receive + WatchdogAnswer(const WatchdogAnswer &orig); + WatchdogAnswer(unique_ptr
header); + bool parse(const std::vector &message); + + // Send + WatchdogAnswer(); + bool fillBytes(std::vector &message); + }; + + //------------------------------ SPITProject ---------------------------------- + + //----------- ReportProject ----------- + + class ReportProject : public OPPart { + public: + static constexpr int OPFLAG = 11; + static constexpr int BYTESLENGTH = 113; + + private: + string mProjectID{}; + string mProjectName{}; + int mFramesPerSecond{1000}; // Frames per second + + // Receive + public: + ReportProject(const ReportProject &orig); + ReportProject(unique_ptr
header); + bool parse(const std::vector &message); + const string getProjectID() const { return mProjectID; } + const string getProjectName() const { return mProjectName; } + int getFramesPerSecond() const { return mFramesPerSecond; } + // Send + ReportProject(string p_ProjectID, string p_ProjectName); + + void setFramesPerSecond(const int p_FramesPerSecond) { mFramesPerSecond = p_FramesPerSecond; } + bool fillBytes(std::vector &message); + }; + + //----------- CloseProject ----------- + + class CloseProject : public OPPart { + public: + static constexpr int OPFLAG = 13; + static constexpr int BYTESLENGTH = 45; + + private: + string mProjectID{}; + + public: + CloseProject(const CloseProject &orig); + // Receive + CloseProject(unique_ptr
header); + bool parse(const std::vector &message); + const string getProjectID() const { + return mProjectID; + } + // Send + CloseProject(string projectID); + bool fillBytes(std::vector &message); + }; + + //----------- StartProjectTransfer ----------- + class RequestProjectTransfer : public OPPart { + public: + static constexpr int OPFLAG = 15; + static constexpr int BYTESLENGTH = 4; + + RequestProjectTransfer(const RequestProjectTransfer &orig); + // Receive + RequestProjectTransfer(unique_ptr
header); + bool parse(const std::vector &message); + // Send + RequestProjectTransfer(); + bool fillBytes(std::vector &message); + }; + + //----------- StartProjectTransfer ----------- + + class StartProjectTransfer : public OPPart { + public: + static constexpr int OPFLAG = 16; + static constexpr int BYTESLENGTH = 4; + + StartProjectTransfer(const StartProjectTransfer &orig); + // Receive + StartProjectTransfer(unique_ptr
header); + bool parse(const std::vector &message); + // Send + StartProjectTransfer(); + bool fillBytes(std::vector &message); + }; + + //----------- EndProjectTransfer ----------- + class EndProjectTransfer : public OPPart { + public: + static constexpr int OPFLAG = 17; + static constexpr int BYTESLENGTH = 4; + + EndProjectTransfer(const EndProjectTransfer &orig); + // Receive + EndProjectTransfer(unique_ptr
header); + bool parse(const std::vector &message); + // Send + EndProjectTransfer(); + bool fillBytes(std::vector &message); + }; + + //---------------------------- SPIT_Types ---------------------------------- + + //----------- ReportType ----------- + class ReportType : public OPPart { + public: + static constexpr int OPFLAG = 21; + static constexpr int BYTESLENGTH = 168; + + private: + bool mConfirmed{}; + string mTypeID{}; + bool mFixed{}; + bool mServerRemote{}; + bool mServerProjectIndepended{}; + string mTypeGroupName{}; + string mSPITTypeName{}; + uint64_t mDelay{}; + uint64_t mDuration{}; + + public: + ReportType(const ReportType &orig); + // Receive + ReportType(unique_ptr
header); + bool parse(const std::vector &message); + + bool getConfirmed() const { return mConfirmed; } + const string getID() const { return mTypeID; } + bool getFixed() const { return mFixed; } + bool getServerRemote() const { return mServerRemote; } + bool getServerProjectIndepended() const { return mServerProjectIndepended; } + const string getGroupName() const { return mTypeGroupName; } + const string getName() const { return mSPITTypeName; } + uint64_t getDelay() const { return mDelay; } + uint64_t getDuration() const { return mDuration; } + // Send + + ReportType(); + void setConfirmed(bool p_Confirmed) { mConfirmed = p_Confirmed; } + void setID(string iD) { mTypeID = iD; } + void setFixed(bool p_Fixed) { mFixed = p_Fixed; } + void setServerRemote(bool p_IsServerRemote) { mServerRemote = p_IsServerRemote; } + void setServerProjectIndepended(bool p_ProjectIndpended) { mServerProjectIndepended = p_ProjectIndpended; } + void setGroupName(string p_GroupName) { mTypeGroupName = p_GroupName; } + void setName(string p_Name) { mSPITTypeName = p_Name; } + void setDelay(uint64_t p_Delay) { mDelay = p_Delay; } + void setDuration(uint64_t p_Duration) { mDuration = p_Duration; } + + bool fillBytes(std::vector &message); + }; + + //----------- RemoveType ----------- + + class RemoveType : public OPPart { + public: + static constexpr int OPFLAG = 23; + static constexpr int BYTESLENGTH = 45; + + private: + string mTypeID{}; + + public: + RemoveType(const RemoveType &orig); + // Receive + RemoveType(unique_ptr
header); + bool parse(const std::vector &message); + + const string getID() const { return mTypeID; } + + // Send + RemoveType(); + void setID(string iD) { mTypeID = iD; } + bool fillBytes(std::vector &message); + }; + + //---------------------------- SPITObjects ---------------------------------- + + //----------- ReportObject ----------- + + class ReportObject : public OPPart { + public: + static constexpr int OPFLAG = 31; + static constexpr int BYTESLENGTH = 339; + + private: + bool mConfirmed{}; + string mSPITObjectID{}; + bool mFixed{}; + bool mServerRemote{}; + string mSPITObjectName{}; + string mTypeID{}; + string mServerParams{}; + uint64_t mFrameStart{}; + uint64_t mFrameLength{}; + uint64_t mFrameFadeInLength{}; + uint64_t mFrameFadeOutLength{}; + int mValueFactor{}; + char mValueUse{VALUEUSE_DISCARD}; + int mTrack{}; + string mRemark{}; + uint64_t mDelay{}; + uint64_t mDuration{}; + bool mActionAssigned{}; + + public: + ReportObject(const ReportObject &orig); + // Receive + ReportObject(unique_ptr
header); + bool parse(const std::vector &message); + + bool getConfirmed() const { return mConfirmed; } + const string getID() const { return mSPITObjectID; } + bool getFixed() const { return mFixed; } + bool getServerRemote() const { return mServerRemote; } + const string getName() const { return mSPITObjectName; } + const string getSPITTypeID() const { return mTypeID; } + const string getServerParams() const { return mServerParams; } + uint64_t getFrameStart() const { return mFrameStart; } + uint64_t getFrameLength() const { return mFrameLength; } + uint64_t getFrameFadeInLength() const { return mFrameFadeInLength; } + uint64_t getFrameFadeOutLength() const { return mFrameFadeOutLength; } + int getValueFactor() const { return mValueFactor; } + char getValueUse() const { return mValueUse; } + int getTrack() const { return mTrack; } + const string getRemark() const { return mRemark; } + uint64_t getDelay() const { return mDelay; } + uint64_t getDuration() const { return mDuration; } + bool getActionAssigned() const { return mActionAssigned; } + // Send + public: + ReportObject(); + + void setConfirmed(bool p_Confirmed) { mConfirmed = p_Confirmed; }; + void setID(string iD) { mSPITObjectID = iD; }; + void setFixed(bool p_Fixed) { mFixed = p_Fixed; }; + void setServerRemote(bool p_IsRemote) { mServerRemote = p_IsRemote; }; + void setName(string p_Name) { mSPITObjectName = p_Name; }; + void setSPITTypeID(string iD) { mTypeID = iD; }; + void setServerParams(string p_Parameters) { mServerParams = p_Parameters; }; + void setFrameStart(uint64_t p_StartTime) { mFrameStart = p_StartTime; }; + void setFrameLength(uint64_t p_Length) { mFrameLength = p_Length; }; + void setFrameFadeInLength(uint64_t p_FadeInTime) { mFrameFadeInLength = p_FadeInTime; }; + void setFrameFadeOutLength(uint64_t p_FadeOutTime) { mFrameFadeOutLength = p_FadeOutTime; }; + void setValueFactor(int p_Volume) { mValueFactor = p_Volume; }; + void setValueUse(char p_ValueUse) { mValueUse = p_ValueUse; }; + void setTrack(int p_Track) { mTrack = p_Track; }; + void setRemark(string p_Description) { mRemark = p_Description; }; + void setDelay(uint64_t p_Delay); + void setDuration(uint64_t p_Duration); + void setActionAssigned(bool p_ActionAssigned) { mActionAssigned = p_ActionAssigned; } + bool fillBytes(std::vector &message); + }; + + //----------- RemoveObject ----------- + + class RemoveObject : public OPPart { + public: + static constexpr int OPFLAG = 33; + static constexpr int BYTESLENGTH = 45; + + private: + string mSPITObjectID{}; + + public: + RemoveObject(const RemoveObject &orig); + // Receive + RemoveObject(unique_ptr
header); + bool parse(const std::vector &message); + + const string getID() const { return mSPITObjectID; } + // Send + public: + RemoveObject(); + void setID(string iD) { mSPITObjectID = iD; } + bool fillBytes(std::vector &message); + }; + + //---------------------------- SPITPlayer ---------------------------------- + + //----------- PlayStart ----------- + + class PlayStart : public OPPart { + public: + static constexpr int OPFLAG = 37; + static constexpr int BYTESLENGTH = 119; + + private: + string mPlayerID{}; + string mPlayerName{}; + char mPlayerType{}; + bool mPlayerIsRunning{}; + uint64_t mTime; // Frames + + public: + PlayStart(const PlayStart &orig); + // Receive + PlayStart(unique_ptr
header); + bool parse(const std::vector &message); + + const string getPlayerID() const { return mPlayerID; } + const string getPlayerName() const { return mPlayerName; } + char getPlayerType() const { return mPlayerType; } + bool getPlayerIsRunning() const { return mPlayerIsRunning; } + uint64_t getTime() const { return mTime; } + + // Send + PlayStart(string p_PlayerID, string p_PlayerName, char p_PlayerType); + void setPlayerIsRunning(bool p_IsPlaying) { mPlayerIsRunning = p_IsPlaying; } + void setTime(uint64_t p_Time) { mTime = p_Time; } + bool fillBytes(std::vector &message); + }; + + //----------- PlayObject ----------- + + class PlayObject : public OPPart { + public: + static constexpr int OPFLAG = 38; + static constexpr int BYTESLENGTH = 100; + + private: + string mPlayerID{}; + string mSPITObjectID{}; + uint64_t mFrameInsideObject{}; // Frames + bool mPlayPrePosition{}; + int mValue{}; + char mValueFlag{VALUEFLAG_DIRECT}; + + public: + PlayObject(const PlayObject &orig); + // Receive + PlayObject(unique_ptr
header); + bool parse(const std::vector &message); + + const string getPlayerID() const { return mPlayerID; } + const string getSPITObjectID() const { return mSPITObjectID; } + uint64_t getFrameInsideObject() const { return mFrameInsideObject; } + bool getPlayPrePosition() const { return mPlayPrePosition; } + int getValue() const { return mValue; } + char getValueFlag() const { return mValueFlag; } + // Send + PlayObject(); + + void setPlayerID(string p_PlayerID) { mPlayerID = p_PlayerID; } + void setID(string iD) { mSPITObjectID = iD; } + void setFrameInsideObject(uint64_t p_TimeInside) { mFrameInsideObject = p_TimeInside; } + void setPlayPrePosition(bool p_PlayOldPosition) { mPlayPrePosition = p_PlayOldPosition; } + void setValue(int p_Value) { mValue = p_Value; } + void setValueFlag(char p_ValueFlag) { mValueFlag = p_ValueFlag; } + bool fillBytes(std::vector &message); + }; + + //----------- PlayEnd ----------- + class PlayEnd : public OPPart { + public: + static constexpr int OPFLAG = 39; + static constexpr int BYTESLENGTH = 45; + + private: + string mPlayerID{}; + + public: + PlayEnd(const PlayEnd &orig); + // Receive + PlayEnd(unique_ptr
header); + bool parse(const std::vector &message); + const string getPlayerID() const { return mPlayerID; } + + // Send + PlayEnd(string p_PlayerID); + bool fillBytes(std::vector &message); + }; + + //----------- ReportPlayStatus ----------- + class ReportPlayStatus : public OPPart { + public: + static constexpr int OPFLAG = 40; + static constexpr int BYTESLENGTH = 57; + + private: + string mSPITObjectID{}; + uint64_t mFrameInsideObject{}; + int mValue{}; + + public: + ReportPlayStatus(const ReportPlayStatus &orig); + // Receive + ReportPlayStatus(unique_ptr
header); + bool parse(const std::vector &message); + + const string getID() const { return mSPITObjectID; } + uint64_t getFrameInsideObject() const { return mFrameInsideObject; } + int getValue() const { return mValue; } + // Send + ReportPlayStatus(); + + void setID(string iD); + void setFrameInsideObject(uint64_t p_TimeInside); + void setValue(int p_Value); + bool fillBytes(std::vector &message); + }; + + //----------------------- static helper functions ---------------------------- + public: + static bool isSystemLittleEndian(); + static optional getInt(const std::vector &message, int &offset); + static optional getLong(const std::vector &message, int &offset); + static optional getBoolean(const std::vector &message, int &offset); + static optional getByte(const std::vector &message, int &offset); + static int setInt(int value, std::vector &message, int offset); + static int setLong(uint64_t value, std::vector &message, int offset); + static int setBoolean(bool value, std::vector &message, int offset); + static int setByte(int value, std::vector &message, int offset); + + /** + * Returns the string char* of the part of the byte sequence (0x00 null terminated). + * @param p_Bytes the byte array + * @param offset the startposition of strings bytes + * @param p_Length the length of the string included the null terminate byte (0x00). + * The length of the string will be p_Length -1. + */ + static optional getString(const std::vector &message, int &offset, int maxLength); + + /** + * Fill the p_String in the byte array. + * If p_String length is greater than p_Length -1, the p_String will be truncated. + * CAUTION the return value should be freed after use. + * @param p_String the string + * @param p_Length the maximal length of the string included the null character. + * @throws Exception if p_String == nullptr, p_Bytes == nullptr + * p_Offset less than 0 + * p_ByteCount less than p_Offset + p_Length + * @return p_Length + */ + static int setString(string p_String, std::vector &message, int p_Offset, int p_Length); +}; + +} \ No newline at end of file diff --git a/components/spit/include/spit_object.hpp b/components/spit/include/spit_object.hpp new file mode 100644 index 0000000..3d7e062 --- /dev/null +++ b/components/spit/include/spit_object.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include "spit_type.hpp" +#include "spit_message.hpp" +#include "spit_repo.hpp" + +using namespace std; +namespace spit { + +class SObjectListener; + +class SObject { + public: + template + struct Property { + friend SObject; + Property(bool &confirmed) + : rConfirmed(confirmed) { + } + bool set(const T& val) { + if (mValue == val) + return false; + mValue = val; + rConfirmed = false; + return true; + } + T get () const { + return mValue; + } + private: + bool & rConfirmed; + T mValue{}; + }; + + Property Name{mConfirmed}; + Property Remark{mConfirmed}; + Property ServerParams{mConfirmed}; + Property SPITTypeID{mConfirmed}; + // Returns if the SPITType triggers an action on the Server or on the client. + // @return false = the action is triggered on the client The client waits for the server to play objects of this SPITType + // true = the action is triggered on the server. The server waits for the client to play objects of this SPITType. + Property ServerRemote{mConfirmed}; + Property Fixed{mConfirmed}; + Property FrameStart{mConfirmed}; + Property FrameLength{mConfirmed}; + Property FrameFadeInLength{mConfirmed}; + Property FrameFadeOutLength{mConfirmed}; + Property ValueUse{mConfirmed}; + Property ValueFactor{mConfirmed}; + Property Track{mConfirmed}; + Property Delay{mConfirmed}; + Property Duration{mConfirmed}; + + static void clearUnconfirmedObjects(SpitRepo &repo); + static void setConfirmedFalse(SpitRepo &repo); + static void eventProjectClosed(SpitRepo &repo, SpitRepo &remoteRepo); + + SObject(string iD) { + mID = iD; + } + + private: + string mID; + bool mConfirmed{}; + bool mCreatedByClient{}; + bool mActionAssigned{}; + bool mActionAssignedType{}; // Virtual construct + char mValueUse{0x01}; + int mValueFactor{1000000}; // Values are given in ppm -> 1000000 = 1/1; + bool mPlayPrePosition; + uint64_t mPlayTimeInsideFrames; + int mPlayValue; + char mPlayValueFlag; + + public: + string getID() const { return mID; } + + /** + * This is used by the server and client to tell if the other side has registered the changes. + * @return true = other side has registered the changes. + */ + bool getConfirmed() const { return mConfirmed; } + bool getCreatedByClient() const { return mCreatedByClient; } + /** + * This is a virtual construct - ActionAssignedType is not implemented in the SPITProtocol. + * This is used when an action is associated with a SPITType. + * Then this action is done for every SObject with the SPITType. + * @return true = an action is assigned for the SPITType. + */ + bool getActionAssignedType() const { return mActionAssignedType; } + /** Returns if this SObject has an associated action or not. + * @return true = the user has asscociated an action with this SObject or + * the user has asscociated an action with the SPITType of this SObject + */ + bool getActionAssigned() const { return mActionAssigned || mActionAssignedType; } + + bool getProjectIndepended(SpitRepo &repo, SpitRepo &remoteRepo) const { + SType *ptrSPITType; + if (ServerRemote.get() == false) { + ptrSPITType = repo.getEntry(SPITTypeID.get()); + if (ptrSPITType == nullptr) + return true; + return ptrSPITType->ProjectIndepended.get(); + } else { + ptrSPITType = remoteRepo.getEntry(SPITTypeID.get()); + if (ptrSPITType == nullptr) + return true; + return ptrSPITType->ProjectIndepended.get(); + } + }; + + bool setConfirmed(bool confirmed); + void setCreatedByClient(bool createdByClient) { mCreatedByClient = createdByClient; } + bool setActionAssignedType(bool actionAssigned); + bool setActionAssigned(bool actionAssigned); + + bool setMessageOP(Message::ReportObject *message); + void fillMessageOP(Message::ReportObject &message); + void fillMessageOP(Message::RemoveObject &message); +}; + +} \ No newline at end of file diff --git a/components/spit/include/spit_player.hpp b/components/spit/include/spit_player.hpp new file mode 100644 index 0000000..1fb59de --- /dev/null +++ b/components/spit/include/spit_player.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "spit_object.hpp" +#include + +namespace spit { + +class SPlayer { + + //----- PlayObject used in PlaySequences: PlaySPITStart, PlaySPITObject,..., PlaySPITEnd ------ + public: + enum PTypes { + // diescrete player: This player is used to transmit single discrete values. + PTYPE_DISCRETE = 0x00, + // continous player: This player plays SPIT objects sometime during the project. + PTYPE_CONTINOUS = 0x01, + // continous main player: time value can be used like a timecode.
The time value is the position in server's project.
A server should use only one main player. + PTYPE_MAIN = 0x02, + }; + + enum VFlags { + // Set the given value directly + VFLAG_DIRECT = 0x00, + // Increase the existing value by the given value.
This is usefull e.g. when instead of a fader a endless wheel is used*/ + VFLAG_INCREASE = 0x01, + // Decrease the existing value by the given value.
This is usefull e.g. when instead of a fader a endless wheel is used*/ + VFLAG_DECREASE = 0x02, + }; + + class PlayType { + public: + PlayType(SPlayer *spitPlayer, string spitObjectID); + PlayType(const PlayType &orig); + + string getSPITTypeID() const { return mSpitTypeID; } + + void startPlaySequence(); + bool getIsNew() const { return mIsNew; } + void setIsNew(bool isNew) { mIsNew = isNew; } + bool getIsPlayed() const { return mIsPlayed; } + void setIsPlayed(bool p_IsFound) { mIsPlayed = p_IsFound; } + string getPlayerID() const { return mPlayerID; } + string getPlayerName() const { return mPlayerName; } + bool getPlayerIsRunning() const { return mPlayerRuns; } + void setPlayerIsRunning(bool playerRuns) { mPlayerRuns = playerRuns; } + void setValue(double value) { mValue = value; } + void addValue(double value, double valueFactor) { mValue += (value * valueFactor); } + double getValue() const { return mValue; } + + private: + string mPlayerID; + string mPlayerName; + bool mPlayerRuns; + string mSpitTypeID; + bool mIsPlayed; + bool mIsNew; + double mValue; + }; + + //----- SPlayer ------ + public: + // list the SPITPlayers + static unordered_map> sSpitPlayers; // all actual played objects + static SPlayer &getSPITPlayer(string pPlayerID); // if a player does not exist creates a new one + static void clearPlayers() { sSpitPlayers.clear(); } + + SPlayer(string pPlayerID); + virtual ~SPlayer(); + + void startPlaySequence(); // called by SPIT_Client::parsePlaySPITStart + PlayType *addTypePlaying(string spitTypeID); // called by SPIT_Client::parsePlaySPITStart + void endPlaySequence(); // called by SPIT_Client::parsePlaySPITEnd + + string getID() { + return mId; + } + + unordered_map> &getPlaySPITTypes() { return mPlaySPITTypes; } + void setIsRunning(bool isRunning) { mIsRunning = isRunning; } + void setName(string name) { mName = name; } + void setType(char type) { mType = type; } + void setTime(uint64_t frames) { mTimeFrames = frames; } + bool getIsRunning() const { return mIsRunning; } + string getName() const { return mName; } + char getType() const { return mType; } + uint64_t getTime() const { return mTimeFrames; } + + private: + string mId; + string mName; + char mType; + unordered_map> mPlaySPITTypes; + + bool mIsRunning; + uint64_t mTimeFrames; +}; + +} // namespace spit \ No newline at end of file diff --git a/components/spit/include/spit_processor.hpp b/components/spit/include/spit_processor.hpp new file mode 100644 index 0000000..2d0dfc7 --- /dev/null +++ b/components/spit/include/spit_processor.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include "sad_DetectedServer.hpp" +#include "spit_message.hpp" +#include "spit_object.hpp" +#include "spit_player.hpp" +#include "spit_repo.hpp" +#include "spit_type.hpp" +#include +#include +#include + +using namespace std; + +namespace spit { + +class SProcessor { + public: + enum class ProjUpdate { + ServerLoaded, + StartTransfer, + EndTransfer, + ServerClosed, + }; + enum class ServerEvent { + Detect, + Lost, + Changed, + }; + enum class ObjectEvent { + Created, + Changed, + Deleted + }; + enum class Connection { + None = 0, // No Connection established.
Either no connect tried or disconnected + NoneServer = 1, // Server has disconnected + Lost = 2, // The connection is lost
Either because of a network crash or because the server no longer responds. + Ok = 3, // Connection to the serv er is established. + TryConnection = 4, // A connection to the server is tried. + }; + + struct Callbacks { + function onServerEvent; + function onConnectionStatus; + function onProjectUpdate; + function onSpitType; + function onSpitObject; + function onPlay; + function onPlaySpitType; + function onPlayRemoteObjectStatus; + function onCheckSpitTypeVeto; + function onCheckSpitObjectVeto; + }; + + struct InternalCallbacks { + function delay; + function sendMessage; + function parseMessage; + }; + + static constexpr int MAX_OVERTAKING_SPIT_MESSAGES = 10; + + //----- SProcessor ----- + public: + SProcessor(string name, string projName, string hostName, string guid, + Callbacks calls, InternalCallbacks intern); + virtual ~SProcessor(); + + private: + Callbacks mCallbacks; + InternalCallbacks mInternCallbacks; + + string mName; + string mGuid; + string mProjName; + string mComputerName; + + uint8_t mMessageNumberSend{1}; + int mMessageNumberReceived{-1}; // set to -1 because the first received message will set the number (1..255) + vector> mOvertakingMessages; + + string mServerID{}; + string mServerProjectID; + string mServerProjectName; + int mServerFramesPerSecond; + + public: + string getID() { return mGuid; } + string getName() { return mName; } + string getProjName() { return mProjName; } + string getComputerName() { return mComputerName; } + void setActionType(string spitypeID, bool actionAssigned); + void setActionObject(string objectID, bool actionAssigned); + void checkParseMessageData(vector data); + + unique_ptr createConnectMessage(string serverIP, string serverPort, string loginName, string loginPassword); + + std::vector createMessage(Message::OPPart &spitMessageOPPart); + void parseMessage(Message &message); + void addOvertakingMessage(unique_ptr message); + int sendOvertakingMessage(int msgNumRec); + void resetMessageNumberReceived(); + + string getServerProjectID() { return mServerProjectID; } + string getServerProjectName() { return mServerProjectName; } + int getServerFramesPerSecond() { return mServerFramesPerSecond; } + + // /** + // * Creates a SPITType for application actions.
+ // * These SPITTypes will be automatically added to the SPITType-List and sended to the server. + // * @param p_ID an unique of the SPITType, these are always the same per client approach. Different client approaches should use different ids. + // * @param p_GroupName a name which groups the actions/SPITTypes + // * @param p_Name the name of the Action/SPITType + // * @param p_DefaultDelay maybe there is an default delay until the action starts, this is only used for graphical representations, 0 is allowd + // * @param p_DefaultDuration maybe the action has a default duration, this is only used for graphical representations, 0 is allowd + // * @param p_Fixed true = the server is not allowed to change this SPITType, false = the server may change or delete this SPITType + // * @param p_ProjectIndepended true = the SPITType belongs to a specific project, false = the SpitType always exists for every project + // * @return the pointer to the new SPITType. + // */ + SType *createType(string iD, string groupName, string name, uint64_t defaultDelay, uint64_t defaultDuration, bool fixed, bool projectIndepended); + int sendTypeInternal(SType &sType); + + /** + * If a client wants to change a SPITType, he should send it to the server.
+ * If the SPITTye is a fixed one and not created by the client himself, then an eventSPITType can be sent from the server, which cancels the change.
+ * @param spitypeID the id of the SPITType to remove. + * @return 0 = is sended, -1 = is not sended + */ + bool changeType(SType *spitType); + /** + * If a client wants to remove a SPITType, he should send it to the server.
+ * If the SPITTye is a fixed one and not created by the client himself, then an eventSPITType can be sent from the server, which cancels the deletion.
+ * @param spitypeID the id of the SPITType to remove. + * @return 0 = is sended, -1 = is not sended + */ + int removeType(string spitypeID); + + int removeObject(string objectID); + bool changeObject(SObject *sObject); + void sendRemoteObject(string objectID, int valuePpm = 0, char valueFlag = 0x00); + void resetConfirmedFlags() { + SType::setConfirmedFalse(mSpitTypes); + SObject::setConfirmedFalse(mSpitObjects); + } + + private: + void parseReportProject(Message::ReportProject &message); + void parseCloseProject(Message::CloseProject &message); + void parseRequestProjectTransfer(Message::RequestProjectTransfer &message); + void parseStartProjectTransfer(Message::StartProjectTransfer &message); + void parseEndProjectTransfer(Message::EndProjectTransfer &message); + void parseReportType(Message::ReportType &message); + void parseRemoveType(Message::RemoveType &message); + void parseReportObject(Message::ReportObject &message); + void parseRemoveObject(Message::RemoveObject &message); + void parsePlayStart(Message::PlayStart &message); + void parsePlayObject(Message::PlayObject &message); + void parsePlayEnd(Message::PlayEnd &message); + void parseReportPlayStatus(Message::ReportPlayStatus &message); + int sendObjectInternal(SObject *sObject); + + SpitRepo mSpitObjects; + SpitRepo mSpitTypes; + SpitRepo mRemoteSpitObjects; + SpitRepo mRemoteSpitTypes; +}; + +} // namespace spit \ No newline at end of file diff --git a/components/spit/include/spit_repo.hpp b/components/spit/include/spit_repo.hpp new file mode 100644 index 0000000..891cc37 --- /dev/null +++ b/components/spit/include/spit_repo.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +using namespace std; + +namespace spit { + +template +class SpitRepo { + public: + bool hasEntry(const string &id) { + return mEntries.contains(id); + } + + T *getEntry(const string &id) { + auto i = mEntries.find(id); + if (i == mEntries.end()) { + return nullptr; + } + return i->second.get(); + } + const T &getEntryRef(const string &id) { + return *mEntries.at(id); + } + + void addEntry(unique_ptr entry) { + mEntries[entry->getID()] = move(entry); + } + bool removeEntry(const T &entry) { + return removeEntry(entry.getID()); + } + bool removeEntry(const string &id) { + auto i = mEntries.find(id); + if (i == mEntries.end()) { + return false; + } + mEntries.erase(i); + return true; + } + int getSize() { + return mEntries.size(); + } + + unordered_map> &getRepo() { + return mEntries; + } + + void clearEntries() { + mEntries.clear(); + } + + private: + unordered_map> mEntries; +}; + +} \ No newline at end of file diff --git a/components/spit/include/spit_socket.hpp b/components/spit/include/spit_socket.hpp new file mode 100644 index 0000000..a31350f --- /dev/null +++ b/components/spit/include/spit_socket.hpp @@ -0,0 +1,149 @@ +#pragma once + +#include "Task.hpp" +#include "esp_log.h" +#include "esp_netif.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace spit { + +/** Returns an guid */ +string createGUID(); +/** Returns the name of the client's host computer*/ +string getClientHostName(); + +class ISocket { + public: + /** + * IpAddress class is used to fetch all available IP addresses of the computer + * This is for internal usage + */ + struct IpAddress { + public: + IpAddress(const string &iPAddress, const string &subnetMask) + : mIpAddress(iPAddress), mSubnetMask(subnetMask) {} + const string mIpAddress; + const string mSubnetMask; + + /** + * Calculates the broadcast address from a ip4 address + * @param iPAddress the ip4 address + * @return returns the broadcast address + */ + string getBroadcastIP4() const { + struct in_addr broadcast; + unsigned long ipAddress; + unsigned long subnetMask; + + ipAddress = inet_addr(mIpAddress.data()); + subnetMask = inet_addr(mSubnetMask.data()); + broadcast.s_addr = ipAddress | ~subnetMask; + return inet_ntoa(broadcast); + } + }; + const IpAddress mOwnIp; + + ISocket(IpAddress ip) + : mOwnIp(ip) {} + + virtual int sendMessage(const char *message, int length) = 0; + virtual int receiveMessage(char *bu1ffer, int bufferLength, string *serverIp) const =0; + virtual void setConnectionParams(const string &serverIp, const string &serverPort, const string &clientIp, const string &clientPort) =0; + virtual int bindSocket(string clientIp, string clientPort) =0; + virtual int setBroadcast(bool broadcast) const =0; + virtual int sendMessageTo(const string &ip, const string &port, const char *message, int msgLength) =0; + + bool getIsBound() const { return mIsBound; } + protected: + bool mIsBound{}; +}; + +class Socket : public ISocket { + public: + static const int FAMILY_IP4 = AF_INET; + + + static IpAddress getAdaptersInfo(); + bool createSocketUdp(); + static const int INVALID_SOCKET = -1; + + private: + int mUdpSocketIp4; + + public: + //-------- class -------------- + Socket(IpAddress ip) : ISocket(ip) { + mUdpSocketIp4 = INVALID_SOCKET; + createSocketUdp(); + } + virtual ~Socket() { + ESP_LOGD("UDP", "Shutting down ..."); + + if (mUdpSocketIp4 != INVALID_SOCKET) { + shutdown(mUdpSocketIp4, 2); + close(mUdpSocketIp4); + mUdpSocketIp4 = INVALID_SOCKET; + } + fos::Task::delayMs(100); + mIsBound = false; + } + + private: + string mServerIP{}; + string mServerPort{}; + string mClientIp{}; + string mClientPort{}; // port given by the user maybe "0" + string mBoundClientPort{}; // port set after bind + + public: + // Sets the ip address and port for server and client. + void setConnectionParams(const string &serverIp, const string &serverPort, const string &clientIp, const string &clientPort) override { + mServerIP = serverIp; + mServerPort = serverPort; + mClientIp = clientIp; + mClientPort = clientPort; + } + + /** + * Send a message to the server as set by setConnectionParams + * @param message byte buffer containing the message + * @param length length of the message + * @return + */ + int sendMessage(const char *message, int length) override { + if (mServerPort.length() <= 0) { + return -1; + } + if (mIsBound == false) { + return -1; + } + int intResult = sendMessageTo(mServerIP, mServerPort, message, length); + return intResult; + } + + int receiveMessage(char *bu1ffer, int bufferLength, string *serverIp) const override; + int setBroadcast(bool broadcast) const override; + int bindSocket(string clientIp, string clientPort) override; + int sendMessageTo(const string &ip, const string &port, const char *message, int msgLength) override; + + string getServerIP() const { return mServerIP; } + string getServerPort() const { return mServerPort; } + string getClientIP() const { return mClientIp; } + string getClientPort() const { return mClientPort; } + string getBoundClientPort() const { return mBoundClientPort; } + void setBoundClientPort(const string &port) { mBoundClientPort = port; } + void setIsBound(bool isBound) { mIsBound = isBound; } + + private: +}; + +} \ No newline at end of file diff --git a/components/spit/include/spit_type.hpp b/components/spit/include/spit_type.hpp new file mode 100644 index 0000000..5b3c0ef --- /dev/null +++ b/components/spit/include/spit_type.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include "spit_message.hpp" +#include "spit_repo.hpp" + +using namespace std; +namespace spit { + +class STypeListener; + +class SType { + public: + template + struct Property { + friend SType; + Property(bool &confirmed) + : rConfirmed(confirmed) { + } + bool set(const T& val) { + if (mValue == val) + return false; + mValue = val; + rConfirmed = false; + return true; + } + T get () const { + return mValue; + } + private: + bool & rConfirmed; + T mValue{}; + }; + + Property GroupName{mConfirmed}; + Property Name{mConfirmed}; + Property Fixed{mConfirmed}; + Property ProjectIndepended{mConfirmed}; + // Returns if the SType triggers an action on the Server or on the client. + // @return false = the action is triggered on the client The client waits for the server to play objects of this SType + // true = the action is triggered on the server. The server waits for the client to play objects of this SType. + Property ServerRemote{mConfirmed}; + Property DefaultDelay{mConfirmed}; + Property DefaultDuration{mConfirmed}; + // Virtual construct - means ActionAssigned for STypes is is not implemented in SPITProtocol. + // This can used to setActionAssigned to all SPITObjects with this SType. + // This may be used in the case when the same action should be done for every SPITObject with this SType associated. + // @return true = the user has associated an action with this SType. + Property ActionAssigned{mConfirmed}; + + // Sets confirmed = false for all STypes. + // This should be called when the client has loaded a project. + static void setConfirmedFalse(SpitRepo &repo) { + auto &entries = repo.getRepo(); + for (auto i = entries.begin(); i != entries.end(); i++) { + i->second->setConfirmed(false); + } + } + static void eventProjectClosed(SpitRepo &repo, SpitRepo &remoteRepo) { + auto &entries = repo.getRepo(); + for (auto i = entries.begin(); i != entries.end();) { + SType *ptrSType = i->second.get(); + if (ptrSType->getCreatedByClient() == true) { + i++; + continue; + } + i = entries.erase(i); + } + // Remote + remoteRepo.clearEntries(); + } + + SType(string iD) { + mID = iD; + } + SType(string iD, string grpName, string name, bool fixed, bool projectIndependent, uint64_t delay, uint64_t duration) { + mConfirmed = false; + mID = iD; + ActionAssigned.set(false); + GroupName.set(grpName); + Name.set(name); + Fixed.set(fixed); + ProjectIndepended.set(projectIndependent); + DefaultDelay.set(delay); + DefaultDuration.set(duration); + } + + private: + bool mConfirmed{}; + string mID; + bool mCreatedByClient{}; // flag true = the SPITTyype is created by the client + // false = the SType is received from the server + + public: + string getID() const { return mID; } + + // This is used by the server and client to tell if the other side has registered the changes. + // @return true = other side has registered the changes. + bool getConfirmed() const { return mConfirmed; } + bool getCreatedByClient() const { return mCreatedByClient; } + void setCreatedByClient(bool createdByClient) { mCreatedByClient = createdByClient; } + + // Set the content of the SType as received from the server + // @param p_ReportSType + // @return true = the SType is changed + // false = the SType is NOT changed, because it is not allowed to change. + bool setMessageOP(Message::ReportType &message) { + bool changeAllowed; + + if ((mCreatedByClient == true) && (Fixed.get() == true)) { + changeAllowed = false; + } else { + changeAllowed = true; + } + + // Confirmation should be changeable + if (mCreatedByClient) { + // Server reports a client created SType -> the type is confirmed + mConfirmed = true; + } else { + // type is NOT client created -> the client has to confirm the SPITtype + mConfirmed = message.getConfirmed(); + } + + if (changeAllowed == false) { + return false; + } + bool changed = false; + + if (DefaultDelay.mValue != message.getDelay()) { + DefaultDelay.mValue = message.getDelay(); + changed = true; + } + if (DefaultDuration.mValue != message.getDuration()) { + DefaultDuration.mValue = message.getDuration(); + changed = true; + } + if (Fixed.mValue != message.getFixed()) { + Fixed.mValue = message.getFixed(); + changed = true; + } + if (GroupName.mValue != message.getGroupName()) { + GroupName.mValue = message.getGroupName(); + changed = true; + } + if (mID != message.getID()) { + mID = message.getID(); + changed = true; + } + if (Name.mValue != message.getName()) { + Name.mValue = message.getName(); + changed = true; + } + if (ProjectIndepended.mValue != message.getServerProjectIndepended()) { + ProjectIndepended.mValue = message.getServerProjectIndepended(); + changed = true; + } + if (ServerRemote.mValue != message.getServerRemote()) { + ServerRemote.mValue = message.getServerRemote(); + changed = true; + } + return changed; + } + void fillMessageOP(Message::ReportType &message) const { + message.setConfirmed(mConfirmed); + message.setDelay(DefaultDelay.mValue); + message.setDuration(DefaultDuration.mValue); + message.setFixed(Fixed.mValue); + message.setGroupName(GroupName.mValue); + message.setID(mID); + message.setName(Name.mValue); + message.setServerProjectIndepended(ProjectIndepended.mValue); + message.setServerRemote(ServerRemote.mValue); + } + void fillMessageOP(Message::RemoveType &message) const { + message.setID(mID); + } + + // This is used by the server and client to tell if the other side has registered the changes. + // @param confirmed + // @return if mConfirmed has changed + bool setConfirmed(bool confirmed) { + if (mConfirmed == confirmed) + return false; + mConfirmed = confirmed; + return true; + } +}; + +} \ No newline at end of file diff --git a/components/spit/src/esp/from b/components/spit/src/esp/from new file mode 100644 index 0000000..e548fe0 --- /dev/null +++ b/components/spit/src/esp/from @@ -0,0 +1 @@ +https://github.com/typester/esp32-uuid/tree/master \ No newline at end of file diff --git a/components/spit/src/esp/license b/components/spit/src/esp/license new file mode 100644 index 0000000..dfc485b --- /dev/null +++ b/components/spit/src/esp/license @@ -0,0 +1,7 @@ +Copyright 2019 Daisuke Murase (typester) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/components/spit/src/esp/uuid.c b/components/spit/src/esp/uuid.c new file mode 100644 index 0000000..8cb03fa --- /dev/null +++ b/components/spit/src/esp/uuid.c @@ -0,0 +1,84 @@ +#include "uuid.h" + +#include + +#include "esp_system.h" +#include "esp_random.h" + +void uuid_generate(uuid_t out) +{ + esp_fill_random(out, sizeof(uuid_t)); + + /* uuid version */ + out[6] = 0x40 | (out[6] & 0xF); + + /* uuid variant */ + out[8] = (0x80 | out[8]) & ~0x40; +} + +static uint8_t unhex_char(unsigned char s) +{ + if (0x30 <= s && s <= 0x39) { /* 0-9 */ + return s - 0x30; + } else if (0x41 <= s && s <= 0x46) { /* A-F */ + return s - 0x41 + 0xa; + } else if (0x61 <= s && s <= 0x69) { /* a-f */ + return s - 0x61 + 0xa; + } else { + /* invalid string */ + return 0xff; + } +} + +static int unhex(unsigned char *s, size_t s_len, unsigned char *r) +{ + int i; + + for (i = 0; i < s_len; i += 2) { + uint8_t h = unhex_char(s[i]); + uint8_t l = unhex_char(s[i + 1]); + if (0xff == h || 0xff == l) { + return -1; + } + r[i / 2] = (h << 4) | (l & 0xf); + } + + return 0; +} + +int uuid_parse(const char *in, uuid_t uu) +{ + const char *p = in; + uint8_t *op = (uint8_t *)uu; + + if (0 != unhex((unsigned char *)p, 8, op)) { + return -1; + } + p += 8; + op += 4; + + for (int i = 0; i < 3; i++) { + if ('-' != *p++ || 0 != unhex((unsigned char *)p, 4, op)) { + return -1; + } + p += 4; + op += 2; + } + + if ('-' != *p++ || 0 != unhex((unsigned char *)p, 12, op)) { + return -1; + } + p += 12; + op += 6; + + return 0; +} + +void uuid_unparse(const uuid_t uu, char *out) +{ + snprintf(out, UUID_STR_LEN, + "%02x%02x%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x-%02x%02x%02x%02x%02x%02x", + uu[0], uu[1], uu[2], uu[3], uu[4], uu[5], uu[6], uu[7], uu[8], uu[9], uu[10], uu[11], + uu[12], uu[13], uu[14], uu[15]); +} \ No newline at end of file diff --git a/components/spit/src/esp/uuid.h b/components/spit/src/esp/uuid.h new file mode 100644 index 0000000..eeafb1a --- /dev/null +++ b/components/spit/src/esp/uuid.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +typedef uint8_t uuid_t[16]; + +#define UUID_STR_LEN 37 + +#ifdef __cplusplus +extern "C" { +#endif +void uuid_generate(uuid_t out); +int uuid_parse(const char *in, uuid_t uu); +void uuid_unparse(const uuid_t uu, char *out); +#ifdef __cplusplus +} +#endif diff --git a/components/spit/src/spit_client.cpp b/components/spit/src/spit_client.cpp new file mode 100644 index 0000000..6a015a1 --- /dev/null +++ b/components/spit/src/spit_client.cpp @@ -0,0 +1,650 @@ + +#include "spit_client.hpp" +#include "esp_log.h" + +#include "Queue.hpp" + +namespace spit { +static const char *TAG = "SPIT"; +static fos::Queue> incommingSpit(128); + + +/** + * Create a new SPIT_client instance, should be done once by the user app. + * @param p_SPITClientInterface the user app should implement the SPIT_ClientInterface. + */ +SClient::SClient(string name, string projName, ISocket &socket, Callbacks calls) + : ISadClient(), + mCallbacks(calls), + mProcessor(name, projName, getClientHostName(), createGUID(), calls, + { + .delay = [&](int ms){ return fos::Task::delayMs(ms); }, + .sendMessage = [&](Message::OPPart &msg){ return sendMessage(msg); }, + .parseMessage = [&](Message& msg) { parseMessage(msg); } + } + ), + mSocket(socket) { +} + +SClient::~SClient() { + stopServerAutoDetect(); + disconnectInternal(Connection::None); +} + +void SClient::clientProjectClose() { + if (mConnectionStatus == Connection::Ok) { + ESP_LOGI(TAG, " ... tidy up ressources"); + Message::CloseProject closeSPITProject(mProcessor.getProjName()); + if (sendMessage(closeSPITProject) < 0) { + ESP_LOGD(TAG, "Client could not send Client's ReportProject"); + } + } +} + +void SClient::clientProjectLoaded() { + // set confirmed = false for all SPITTypes and SPITObjects + mProcessor.resetConfirmedFlags(); + if (mConnectionStatus == Connection::Ok) { + // send ReportProject to the server + Message::ReportProject reportSPITProject(mProcessor.getProjName(), mProcessor.getProjName()); + reportSPITProject.setFramesPerSecond(0); + if (sendMessage(reportSPITProject) < 0) { + ESP_LOGD(TAG, "SPIT_CLient::clientProjectLoaded could not send Client's ReportProject"); + } + + // send RequestProjectTransfer + Message::RequestProjectTransfer sMRequestProjectTransfer; + if (sendMessage(sMRequestProjectTransfer) < 0) { + ESP_LOGD(TAG, "SPIT_CLient::clientProjectLoaded could not send RequestProjectTransfer"); + } + } +} + +//---------- ServerAutoDetection SAD ----------- + +/** Starts the autodetection of SPITServers in the local network. If a connection is tried, the autodetection should be stopped before. + * If a SPITServer is detected in the locale network an eventServerDetected occurs. + */ +void SClient::startServerAutoDetect() { + if (mDisconnectedByServer == true) { + // Server has previously disconnected -> do not start ServerAutodetect directly + fos::Task::delayMs(1000); // sleep a little to give the SPITServer time to clean up its stuff + } + SadDetectedServer::clearDetectedServers(); + if (!sThread) { + sThread = new fos::TaskT(SClient::processSocketTask, *this, "spitSock", fos::Task::DefaultStackSize * 2); + } + mAbortSearch = false; + sStartSad.give(); +} + +/** Stops the auto detection of SPIT servers. */ +void SClient::stopServerAutoDetect() { + mAbortSearch = true; +} +/** + * If a new server was found by the ServerAutoDetect mechanism, this function is called. + * To do so the SAD_Client ServerAutoDetect has to be startet - see SAD_Client::startServerAutoDetect. + * If a server can be reached via several IP addresses, the IP address is selected according to the following preference: + */ +void SClient::eventServerDetected(SadDetectedServer &server) { + mCallbacks.onServerEvent(ServerEvent::Detect, server); + // If a connection has been established and the connection is lost -> reconnect + if (_doReconnect) { + if (mServerID == server.getID()) { + stopServerAutoDetect(); + ESP_LOGI(TAG, "reconnect to %s / %s", server.getIP().data(), server.getPort().data()); + if (connect(server.getIP(), server.getPort(), server.getClientIP(), "0")) { + return; + } + } + } + // Stop auto detection because a suitable server was found + stopServerAutoDetect(); + ESP_LOGI(TAG, "connect to %s / %s", server.getIP().data(), server.getPort().data()); + if (!connect(server.getIP(), server.getPort(), server.getClientIP(), "0" /*= any port*/)) { + // connection message could not be sent -> better to restart AutoDetection + startServerAutoDetect(); + } +} + +/** + * If a SPITServer is not more found, this function is called. + * To do so the SAD_Client ServerAutoDetect has to be startet - see SAD_Client::startServerAutoDetect. + */ +void SClient::eventServerLost(SadDetectedServer &server) { + mCallbacks.onServerEvent(ServerEvent::Lost, server); +} + +void SClient::eventServerChanged(SadDetectedServer &server) { + mCallbacks.onServerEvent(ServerEvent::Changed, server); + // tryConnection(detectedServer); +} + +// void MyApp::tryConnection(const SadDetectedServer &detected) { +// // fetch the infoemations from the detected +// bool boolSuccess = false; +// bool boolPossibleConnect = checkPossibleConnection(detected); + +// if (boolPossibleConnect == false) +// return; + +// // -------------------------------------- +// // You have three Options to use the detected server +// // OPTION 1 Use the first server found +// // OPTION 2 Use the first server found that fulfills specific conditions +// // OPTION 3 take a copy of the detected and do connection later +// // SadDetectedServer o_DetectedServer = detected; //this will call the copy constructor +// // --------------------------------------- + +// // Excample use the first detected LOOPBACK server, means the server runs on the same computer as this client +// // if (detected.getIpIsLoopback() == true) { //omit the if condition if you want to use the first server found + +// boolSuccess = false; +// // Stop auto detection because a suitable server was found +// stopServerAutoDetect(); +// if (mSPIT_Client != nullptr) { +// mServerProjectName = detected.getProjectName(); +// mServerIP = detected.getIP(); + +// #if defined(DEBUGOUTPUT) +// ESP_LOGD(TAG, "MySPITApp: connect to %s ", detected.getIP().data()); +// #endif +// // Use the mBoardNumber as login name +// string stringLoginName = ""; +// string stringLoginPassword = ""; +// boolSuccess = mSPIT_Client->connect(detected.getIP(), detected.getPort(), detected.getClientIP(), stringLoginName, stringLoginPassword); // ClientPort = "0" meeans use any port +// } +// if (boolSuccess == false) { +// // connection message could not be sent +// // perhaps restart AutoDetection +// startServerAutoDetect(); +// } else { +// // WAITING FOR CONNECTION ANSWER +// // If the Server is closed in the meantime no answer will return +// //-> connect(..) has started the watchdog +// } + +// // } //Excample +// } + +int SClient::sendMessage(Message::OPPart &opPart) { + if (mMessageNumberSend == 255) + mMessageNumberSend = 1; + else + mMessageNumberSend++; + opPart.setMessageNumber(mMessageNumberSend); + opPart.setServerInfoInHeader(mProcessor.getID(), mServerID); + + int opFlag = opPart.getHeader()->getType(); + try { + auto spitMessage = Message::createMessage(opPart); + if (spitMessage.empty()) { + return -1; + } + int intResult = mSocket.sendMessage(spitMessage.data(), spitMessage.size()); + if (intResult < 0) { + ESP_LOGD(TAG, "client could not send message OPFlage: %d", opFlag); + } + return intResult; + } catch (...) { + ESP_LOGD(TAG, "Exception client OPFlag: %d", opFlag); + } + return -1; +} + +bool SClient::startReceiveSPITMessages() { + if (!mSocket.getIsBound()) + return false; + mStopReceiveMessages = false; + mThreadReceive = new fos::TaskT(receiveSpitMessage, *this, "receiveTask", fos::Task::DefaultStackSize, fos::Task::MAX_PRIORITY-3); + mThreadProcess = new fos::TaskT(processSpitMessage, *this, "processTask", fos::Task::DefaultStackSize); + return true; +} + +void SClient::stopReceiveSPITMessages() { + mStopReceiveMessages = true; +} + +void SClient::receiveSpitMessage(SClient &rM) { + char buffer[Message::MAX_MESSAGE_SIZE]; + int rdLen; + while (rM.mStopReceiveMessages == false) { + rdLen = rM.mSocket.receiveMessage(buffer, Message::MAX_MESSAGE_SIZE, nullptr); + if (rdLen < 0) { + if (rM.mStopReceiveMessages) { + return; + } + continue; + } + auto vec = make_unique>(buffer, buffer + rdLen); + + incommingSpit.enqueue(std::move(vec)); + } +} + +void SClient::processSpitMessage(SClient &rM) { + while (true) { + auto data = *incommingSpit.dequeue(); + auto spitMsg = make_unique(data); + rM.checkParseMessage(move(spitMsg)); + } +} + + +void SClient::resetMessageNumberReceived() { + mMessageNumberReceived = -1; + mProcessor.resetMessageNumberReceived(); +} + +void SClient::checkParseMessage(unique_ptr p_SPITMessage) { + answerWatchdog(); + if (!p_SPITMessage || !p_SPITMessage->getHeader() || !p_SPITMessage->getOPPart()) { + ESP_LOGD(TAG, "Client::parseMessage incomplete message"); + return; + } + + int msgNum = (int)(p_SPITMessage->getHeader()->getMessageNumber() & 0xFF); + + if (msgNum == 0) { + parseMessage(*p_SPITMessage); + return; + } + if (mMessageNumberReceived < 0) { + mMessageNumberReceived = msgNum; + parseMessage(*p_SPITMessage); + return; + } + + int intNextMessageNumber = mMessageNumberReceived + 1; + if (intNextMessageNumber > 255 || intNextMessageNumber < 1) + intNextMessageNumber = 1; + + if (msgNum == intNextMessageNumber) { + mMessageNumberReceived = msgNum; + parseMessage(*p_SPITMessage); + + mMessageNumberReceived = mProcessor.sendOvertakingMessage(mMessageNumberReceived); + } else { + // p_SPITMessage has overtaken another message + mProcessor.addOvertakingMessage(move(p_SPITMessage)); + } +} + +void SClient::parseMessage(Message& message) { + answerWatchdog(); + if (!message.getHeader() || !message.getOPPart()) { + ESP_LOGD(TAG, "Client::parseMessage incomplete message"); + return; + } + + switch (message.getType()) { + //----- Connection ----- + case Message::ConnectAnswer::OPFLAG: + parseConnectAnswer(message.getAs()); + break; + case Message::Disconnect::OPFLAG: + parseDisconnect(message.getAs()); + break; + case Message::Watchdog::OPFLAG: + parseWatchdog(message.getAs()); + break; + case Message::WatchdogAnswer::OPFLAG: + parseWatchdogAnswer(message.getAs()); + break; + default: + // pass object to SubProcessor + mProcessor.parseMessage(message); + } +} + +//----- Connection ------ +/** + * Tries to connect to a SPITServer. + * @param serverIP the ip address of the SPIT server + * @param serverPort the port number of the SPIT server + * @param clientIP the local IP address of the client + * @param clientPort the port number of the Client, if "0" the OS uses any possible port number + * @param loginName login user name (liveSHOW does not use this -> mayby "" + * @param loginPassword login user password (liveSHOW does not use this -> mayby "" + * @return false = an error has occured, true = the connect was sent to the server + */ +bool SClient::connect(string serverIP, string serverPort, string clientIP, string clientPort, string loginName, string loginPassword) { + disconnect(); + mSocket.setConnectionParams(serverIP, serverPort, clientIP, clientPort); + if (mSocket.bindSocket(clientIP, clientPort) < 0) { + mStopReceiveMessages = true; + return false; + } + + auto connMsg = mProcessor.createConnectMessage(serverIP, serverPort, loginName, loginPassword); + + if (startReceiveSPITMessages() == false) { + mStopReceiveMessages = true; + return false; + } + resetMessageNumberReceived(); + Connection intOldConnectionStatus = mConnectionStatus; + + mConnectionStatus = Connection::TryConnection; + mCallbacks.onConnectionStatus(mServerID, mServerComputerName, mServerName, mServerStepTime, + mConnectionStatus, intOldConnectionStatus); + + // sleep a little to make sure that the reception of the messages is started + fos::Task::delayMs(500); + if (sendMessage(*connMsg) < 0) { + mConnectionStatus = Connection::None; + mStopReceiveMessages = true; + return false; + } + return true; +} + +/** + * Terminates the connection to the SPITServer. + */ +void SClient::disconnect() { + disconnectInternal(Connection::None); +} + +void SClient::disconnectInternal(Connection cause) { + Connection intOldConnectionStatus = mConnectionStatus; + + mConnectionStatus = cause; + switch (mConnectionStatus) { + case Connection::Lost: + break; + case Connection::None: { + Message::Disconnect sMDisconnect; + sendMessage(sMDisconnect); + } break; + case Connection::NoneServer: + break; + default: + break; + } + + stopReceiveSPITMessages(); + mCallbacks.onConnectionStatus(mServerID, mServerComputerName, mServerName, mServerStepTime, + mConnectionStatus, intOldConnectionStatus); + + if (intOldConnectionStatus != Connection::None && intOldConnectionStatus != Connection::NoneServer) { + ESP_LOGI(TAG, "eventConnectionStatus %d ", (int)mConnectionStatus); + switch (mConnectionStatus) { + case SClient::Connection::None: + ESP_LOGD(TAG, "not connected"); + break; + case SClient::Connection::NoneServer: + ESP_LOGD(TAG, "connection terminated by server"); + // call eventProjectClosed because this may not be sended by the server, + // when the connection is closed + mCallbacks.onProjectUpdate(ProjUpdate::ServerClosed, mProcessor.getServerProjectID()); + + // Server has terminated the connection -> start a new game + // _doReconnect = false; + startServerAutoDetect(); + break; + case SClient::Connection::Lost: + ESP_LOGD(TAG, "connection lost"); + // Maybe you want to reconnect, see eventServerDetected(...) + // We have lost the connection -> set the flag to true and start the autodetection to reconnect at eventServerDetected(...) + // _doReconnect = true; + startServerAutoDetect(); + break; + default: + break; + } + } +} + +void SClient::parseConnectAnswer(Message::ConnectAnswer &message) { + if (message.getClientID() != mProcessor.getID()) + return; + + mServerID = message.getServerID(); + mServerComputerName = message.getServerComputerName(); + mServerName = message.getServerName(); + mServerStepTime = message.getStepTime(); + + if (message.getConnected() == false) { + disconnectInternal(Connection::NoneServer); + } else { + mConnectionStatus = Connection::Ok; + mDisconnectedByServer = false; + ESP_LOGD(TAG, "connection established"); + stopServerAutoDetect(); // normally you should stop the autodetection before you try a connection + // This is redundand + mStopReceiveMessages = false; + + // Maybe you want to reconnect, see eventServerDetected(...) + // Here the connection is established -> don't reconnect at eventServerDetected(...) + // _doReconnect = false; + } +} + +/** + * The SPIT server was closed + * @param message + */ +void SClient::parseDisconnect(Message::Disconnect &message) { + Connection intCauseDisconnect = Connection::NoneServer; + + mDisconnectedByServer = true; + SClient::disconnectInternal(intCauseDisconnect); +} + +//----- Watchdog ------ +/** + * The SPIT server sends a watchdog message at regular intervals. The client responds immediately with WatchdogAnswer + * Conversely, the client also sends a watchdog message at regular intervals and waits for a WatchdogAnswer message. + * This is used to check if the other side is still present. + * @param message + */ +void SClient::parseWatchdog(Message::Watchdog &message) { + Message::WatchdogAnswer o_WatchdogAnswer; + sendMessage(o_WatchdogAnswer); +} +/** + * The SPIT server sends a watchdog message at regular intervals. The client responds immediately with WatchdogAnswer + * Conversely, the client also sends a watchdog message at regular intervals and waits for a WatchdogAnswer message. + * This is used to check if the other side is still present. + * @param message + */ +void SClient::parseWatchdogAnswer(Message::WatchdogAnswer &message) { + answerWatchdog(); // is also done by parseMessage +} +void SClient::sendWatchdog() { + int intResult; + Message::Watchdog message; + intResult = sendMessage(message); + if (intResult < 0) { + ESP_LOGW("WDT", "Failed to send Watchdog msg"); + } +} +void SClient::eventWatchdogLost() { + if (mConnectionStatus == Connection::TryConnection) { + disconnectInternal(Connection::NoneServer); + } else { + disconnectInternal(Connection::Lost); + } +} + + +void SClient::sendProject(SClient *p_PtrSPIT_Client) { + // int intResult; + // Message::StartProjectTransfer* ptrSMStartProjectTransfer; + // Message::EndProjectTransfer* ptrSMEndProjectTransfer; + // int intCount; + // SPITType* ptrSPITType; + // SObject* ptrSPITObject; + + // //StartProjectTransfer + // NotifyStartProjectTransfer_Client* o_RunnableStart = new NotifyStartProjectTransfer_Client(p_PtrSPIT_Client->getSPITClientInterface()); + // SPIT_Handler::addRunnable(o_RunnableStart); + + // ptrSMStartProjectTransfer = new Message::StartProjectTransfer(); + // intResult = p_PtrSPIT_Client->sendMessage(ptrSMStartProjectTransfer); + // if (intResult < 0) { + // return; + // } + + // // mWatchdog->answerWatchdog(); + // // { + // //send all listes SPITTypes + // // unique_lock o_LockType(*SPITType::getMutex()); + // intCount = SPITType::getSPITTypeCount(); + // for (int i = 0; i < intCount; i++) { + // ptrSPITType = mSpitTypes.getEntry(i); + // if (ptrSPITType == nullptr) continue; + // if (ptrSPITType->getCreatedByClient() == false) continue; + // intResult = p_PtrSPIT_Client->sendTypeInternal(ptrSPITType); + // if (intResult < 0) return; + // // mWatchdog->answerWatchdog(); + // this_thread::sleep_for(chrono::milliseconds(7)); + // } + // // } + + // // { + // // unique_lock o_LockObject(*SObject::getMutex()); + // intCount = mSpitObjects.getEntryCount(); + // for (int i = 0; i < intCount; i++) { + // ptrSPITObject = mSpitObjects.getEntry(i); + // if (ptrSPITObject == nullptr) continue; + // if (ptrSPITObject->getCreatedByClient() == false) continue; + // intResult = p_PtrSPIT_Client->sendObjectInternal(ptrSPITObject); + // if (intResult < 0) return; + // // mWatchdog->answerWatchdog(); + // this_thread::sleep_for(chrono::milliseconds(7)); + // } + // // } + + // //EndProjectTransfer + // ptrSMEndProjectTransfer = new Message::EndProjectTransfer(); + // intResult = p_PtrSPIT_Client->sendMessage(ptrSMEndProjectTransfer); + // if (intResult < 0) { + // return; + // } + // // mWatchdog->answerWatchdog(); + // NotifyEndProjectTransfer_Client* o_RunnableEnd = new NotifyEndProjectTransfer_Client(p_PtrSPIT_Client->getSPITClientInterface()); + // SPIT_Handler::addRunnable(o_RunnableEnd); + + // //Send act +} + +SType *SClient::createTypeFixed(string iD, string group, string name, uint64_t delay, uint64_t duration) { + return mProcessor.createType(iD, group, name, delay, duration, true, true); +} + +fos::Semaphore SClient::sStartSad; +unordered_map> SadDetectedServer::sKnownServers; + +void SClient::processSocketTask(SClient &instance) { + ESP_LOGI(TAG, "Starting Socket managing"); + static const string SERVERAUTODETECT_PRECODE = "#SAD#"; + static const string SERVERAUTODETECT_SEPERATOR = "#,#"; + static const string SERVERAUTODETECT_TASKID = "#spit#"; // -> "#SAD##,##spit#" + static const string SERVERAUTODETECT_PORT = "15110"; + static constexpr int WATCHDOGTIME = 3000; + + while (true) { + sStartSad.take(); + bool searching = true; + instance.mSocket.setBroadcast(true); + string stringDatagram = SERVERAUTODETECT_PRECODE + SERVERAUTODETECT_SEPERATOR + SERVERAUTODETECT_TASKID; + int intDatagramLength = stringDatagram.length(); + while (searching) { + if (instance.mAbortSearch) + break; + + SadDetectedServer::resetServerFlags(); + string stringBroadcastIP4 = instance.mSocket.mOwnIp.getBroadcastIP4(); + instance.mSocket.sendMessageTo(stringBroadcastIP4, SERVERAUTODETECT_PORT, stringDatagram.data(), intDatagramLength); + { + // wait for 3000 milliseconds or + fos::Task::delayMs(3000); + } + + bool receiving = true; + while (receiving) { + if (instance.mAbortSearch) { + break; + } + constexpr int SIZE = 512; + char buffer[SIZE + 1]; + string stringServerIP; + int intReceivedByteCount = instance.mSocket.receiveMessage(buffer, SIZE, &stringServerIP); + if (intReceivedByteCount <= 0) { + break; + } + buffer[intReceivedByteCount] = '\0'; + string stringMessage = buffer; + // Check if precode is ok + if (stringMessage.find(SERVERAUTODETECT_PRECODE.data(), 0) != 0) { + break; + } + int intIndex = 0; + int intOldPos = 0; + string stringPart; + string strParts[10]; + + while (intIndex < SadDetectedServer::PARAM_IP) { + int intPos = stringMessage.find(SERVERAUTODETECT_SEPERATOR.data(), intOldPos); + if (intPos == string::npos) { + stringPart = ""; + } else { + stringPart = stringMessage.substr(intOldPos, intPos - intOldPos); + intOldPos = intPos + SERVERAUTODETECT_SEPERATOR.length(); + } + strParts[intIndex] = stringPart; + intIndex++; + } + + auto server = make_unique( + strParts[SadDetectedServer::PARAM_NAME], + strParts[SadDetectedServer::PARAM_ID], + strParts[SadDetectedServer::PARAM_COMPUTERNAME], + stringServerIP, + strParts[SadDetectedServer::PARAM_PORT], + strParts[SadDetectedServer::PARAM_CLIENTIP], + strParts[SadDetectedServer::PARAM_ADDITIONS], + strParts[SadDetectedServer::PARAM_SERVERPROJECTNAME]); + + SadDetectedServer::addDetectedServer(move(server)); + } + fos::Task::delayMs(100); + SadDetectedServer::evaluateDetectedServers(instance); + + instance.mAbortSearch = false; + // if(instance.mConnectionStatus == Connection::Ok) break; + fos::Task::delayMs(1000); + } + instance.mSocket.setBroadcast(false); + + ESP_LOGW(TAG, "Finished searching"); + ESP_LOGW(TAG, "Start working"); + auto nextWdTrigger = fos::Timing::getMilliSec() + WATCHDOGTIME; + // auto nextWdCheck = nextWdTrigger + WATCHDOGTIME; + while (true) { + // auto time = fos::Timing::getMilliSec(); + // if (time > nextWdTrigger) { + // instance.sAnswered = false; + // // instance.sendWatchdog(); + // nextWdTrigger = nextWdCheck + WATCHDOGTIME; + // } + + // if (time > nextWdCheck) { + // if (!instance.sAnswered) { + // ESP_LOGW(TAG, "TimeOut"); + // instance.eventWatchdogLost(); + // break; + // } + // nextWdCheck = nextWdTrigger + WATCHDOGTIME; + // } + fos::Task::delayMs(25); + // if (instance.mStopReceiveMessages) + // break; + } + } +} + +} \ No newline at end of file diff --git a/components/spit/src/spit_message.cpp b/components/spit/src/spit_message.cpp new file mode 100644 index 0000000..0777670 --- /dev/null +++ b/components/spit/src/spit_message.cpp @@ -0,0 +1,1334 @@ + +#include "spit_message.hpp" + +#include "esp_log.h" +#include + +const char *TAG = "SpitMsg"; + +namespace spit { + +//---------- Receive --------- +Message::Message(std::vector message) { + if (message.empty()) { + ESP_LOGE(TAG, "message is emtpy, nothing to parse"); + return; + } + if (!parse(message)) { + ESP_LOGE(TAG, "Failed ot parse message"); + return; + } +} + +// Receive + +bool Message::parse(std::vector message) { + // parse Header + auto header = make_unique
(); + if (!header->parse(message)) { + return false; + } + + // parse OP + switch (header->getType()) { + case Message::Connect::OPFLAG: { // Connection + oPPart = make_unique(move(header)); + } break; + case Message::ConnectAnswer::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case Disconnect::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case Watchdog::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case WatchdogAnswer::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case ReportProject::OPFLAG: { // SpitProject + oPPart = make_unique(move(header)); + } break; + case CloseProject::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case RequestProjectTransfer::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case StartProjectTransfer::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case EndProjectTransfer::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case ReportType::OPFLAG: { // SpitType + oPPart = make_unique(move(header)); + } break; + case RemoveType::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case ReportObject::OPFLAG: { // SpitObject + oPPart = make_unique(move(header)); + } break; + case RemoveObject::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case PlayStart::OPFLAG: { // PlaySpitObjects + oPPart = make_unique(move(header)); + } break; + case PlayObject::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case PlayEnd::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + case ReportPlayStatus::OPFLAG: { + oPPart = make_unique(move(header)); + } break; + default: + return false; + } + + return oPPart->parse(message); +} + +//---------- Send ---------- +std::vector Message::createMessage(OPPart &oPPart) { + if (!oPPart.getHeader()) { + return {}; + } + + try { + int totalSize = Header::BYTESLENGTH + oPPart.getBytesCount(); + std::vector msgByts(totalSize); + oPPart.fillBytes(msgByts); + return msgByts; + } catch (...) { + ESP_LOGE(TAG, "Failed to allocate memory"); + } + return {}; +} + +//----------- Header ----------- +Message::Header::Header(const Header &orig) { + mID = &SPITID[0]; + mMessageNumber = orig.getMessageNumber(); + mServerId = orig.getServerID(); + mClientId = orig.getClientID(); +} + +Message::Header::Header() { + mID = &SPITID[0]; +} + +bool Message::Header::parse(const std::vector &message) { + try { + mParseOffset = 0; + auto strID = getString(message, mParseOffset, 7).value(); + if (strID != SPITID) { + return false; + } + auto iVer = getInt(message, mParseOffset).value(); + if (iVer < 0) { + return false; + } + mMessageNumber = getByte(message, mParseOffset).value(); + mBytesCount = getInt(message, mParseOffset).value(); + if (mBytesCount < 0) { + return false; + } + mServerId = getString(message, mParseOffset, 41).value(); + mClientId = getString(message, mParseOffset, 41).value(); + mOpFlag = message[mParseOffset++] & 0xFF; + } catch (...) { + ESP_LOGE(TAG, "failed to parse Header"); + return false; + } + return true; +} + +// Send +Message::Header::Header(int p_SPITVersion, int p_OPFlag) { + mID = &SPITID[0]; + mVersion = p_SPITVersion; + mOpFlag = p_OPFlag; +} + +bool Message::Header::fillBytes(std::vector &message) { + try { + mParseOffset = 0; + mParseOffset += setString(mID, message, mParseOffset, 7); + mParseOffset += setInt(SPITVERSION, message, mParseOffset); + mParseOffset += setByte(mMessageNumber, message, mParseOffset); + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mServerId, message, mParseOffset, 41); + mParseOffset += setString(mClientId, message, mParseOffset, 41); + mParseOffset += setByte(mOpFlag, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create Header"); + return false; + } + return true; +} + +//----------- OPPart ----------- + +Message::OPPart::~OPPart() { +} + +// Receive +Message::OPPart::OPPart(unique_ptr
p_Header, int p_OPFlag, int size) + : mHeader(move(p_Header)), + mBytesCount(size) { + if (!mHeader) { + mParseOffset = Header::BYTESLENGTH; + mHeader->setOPFlag(p_OPFlag); + } +} +void Message::OPPart::setHeader(int opFlag) { + mHeader = make_unique
(SPITVERSION, opFlag); + mParseOffset = Header::BYTESLENGTH; +} + +// Send +Message::OPPart::OPPart(int size) + : mBytesCount(size) {} + +//--------------------------- Connection -------------------------------- +//----------- Connect ----------- +// Receive +Message::Connect::Connect(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} + +bool Message::Connect::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mClientComputerName = getString(message, mParseOffset, 64).value(); + mClientName = getString(message, mParseOffset, 64).value(); + mLoginName = getString(message, mParseOffset, 64).value(); + mLoginPassword = getString(message, mParseOffset, 64).value(); + mServerIp = getString(message, mParseOffset, 40).value(); + mServerPort = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse Connect message"); + return false; + } + return true; +} + +// Send +Message::Connect::Connect(string pComputerName, string pClientName) + : OPPart(BYTESLENGTH), + mClientComputerName(pComputerName), + mClientName(pClientName) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} +void Message::Connect::setServerPort(int p_ServerPort) { + if (p_ServerPort < 0 || p_ServerPort > 65535) + p_ServerPort = 0; + mServerPort = p_ServerPort; +} +bool Message::Connect::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mClientComputerName, message, mParseOffset, 64); + mParseOffset += setString(mClientName, message, mParseOffset, 64); + mParseOffset += setString(mLoginName, message, mParseOffset, 64); + mParseOffset += setString(mLoginPassword, message, mParseOffset, 64); + mParseOffset += setString(mServerIp, message, mParseOffset, 40); + mParseOffset += setInt(mServerPort, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create Connect message"); + return false; + } + return true; +} + +//----------- ConnectAnswer ----------- + +// Receive +Message::ConnectAnswer::ConnectAnswer(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) {} +bool Message::ConnectAnswer::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mServerComputerName = getString(message, mParseOffset, 64).value(); + mServerName = getString(message, mParseOffset, 64).value(); + mStepTime = getInt(message, mParseOffset).value(); + mConnected = getBoolean(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse ConnectAnswer message"); + return false; + } + return true; +} + +// Send +Message::ConnectAnswer::ConnectAnswer() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} +void Message::ConnectAnswer::setStepTime(int p_StepTime_ms) { + if (p_StepTime_ms < 0) + p_StepTime_ms = 1; + mStepTime = p_StepTime_ms; +} +bool Message::ConnectAnswer::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mServerComputerName, message, mParseOffset, 64); + mParseOffset += setString(mServerName, message, mParseOffset, 64); + mParseOffset += setInt(mStepTime, message, mParseOffset); + mParseOffset += setBoolean(mConnected, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create ConnectAnswer message"); + return false; + } + return true; +} + +//----------- Disconnect ----------- + +// Receive +Message::Disconnect::Disconnect(unique_ptr
p_Header) : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::Disconnect::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse Disconnect message"); + return false; + } + return true; +} + +// Send +Message::Disconnect::Disconnect() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); +} + +bool Message::Disconnect::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create Disconnect message"); + return false; + } + return true; +} + +//----------- Watchdog ----------- +// Receive +Message::Watchdog::Watchdog(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::Watchdog::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse Watchdog message"); + return false; + } + return true; +} + +// Send +Message::Watchdog::Watchdog() : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} +bool Message::Watchdog::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create Watchdog message"); + return false; + } + return true; +} + +//----------- WatchdogAnswer ----------- + +// Receive +Message::WatchdogAnswer::WatchdogAnswer(unique_ptr
p_Header) : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::WatchdogAnswer::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse WatchdogAnswer message"); + return false; + } + return true; +} + +// Send +Message::WatchdogAnswer::WatchdogAnswer() : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} +bool Message::WatchdogAnswer::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create WatchdogAnswer message"); + return false; + } + return true; +} + +//--------------------------- SPITProject -------------------------------- + +//----------- ReportProject ----------- +// Receive +Message::ReportProject::ReportProject(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::ReportProject::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mProjectID = getString(message, mParseOffset, 41).value(); + mProjectName = getString(message, mParseOffset, 64).value(); + mFramesPerSecond = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse ReportProject message"); + return false; + } + return true; +} +// Send +Message::ReportProject::ReportProject(string p_ProjectID, string p_ProjectName) + : OPPart(BYTESLENGTH), + mProjectID(p_ProjectID), + mProjectName(p_ProjectName) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::ReportProject::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mProjectID, message, mParseOffset, 41); + mParseOffset += setString(mProjectName, message, mParseOffset, 64); + mParseOffset += setInt(mFramesPerSecond, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create ReportProject message"); + return false; + } + return true; +} + +//----------- StartProjectTransfer ----------- + +// Receive +Message::CloseProject::CloseProject(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::CloseProject::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mProjectID = getString(message, mParseOffset, 41).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse CloseProject message"); + return false; + } + return true; +} +// Send +Message::CloseProject::CloseProject(string projectID) + : OPPart(BYTESLENGTH), + mProjectID(projectID) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::CloseProject::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mProjectID, message, mParseOffset, 41); + } catch (...) { + ESP_LOGE(TAG, "failed to create CloseProject message"); + return false; + } + return true; +} + +//----------- RequestProjectTransfer ----------- + +// Receive +Message::RequestProjectTransfer::RequestProjectTransfer(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::RequestProjectTransfer::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse RequestProjectTransfer message"); + return false; + } + return true; +} +// Send +Message::RequestProjectTransfer::RequestProjectTransfer() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} +bool Message::RequestProjectTransfer::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create RequestProjectTransfer message"); + return false; + } + return true; +} + +//----------- StartProjectTransfer ----------- + +// Receive +Message::StartProjectTransfer::StartProjectTransfer(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::StartProjectTransfer::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse StartProjectTransfer message"); + return false; + } + return true; +} +// Send +Message::StartProjectTransfer::StartProjectTransfer() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::StartProjectTransfer::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create StartProjectTransfer message"); + return false; + } + return true; +} + +//----------- ENdProjectTransfer ----------- + +// Receive +Message::EndProjectTransfer::EndProjectTransfer(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::EndProjectTransfer::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse EndProjectTransfer message"); + return false; + } + return true; +} +// Send +Message::EndProjectTransfer::EndProjectTransfer() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} +bool Message::EndProjectTransfer::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create EndProjectTransfer message"); + return false; + } + return true; +} + +//--------------------------- SPITType --------------------------------- + +//----------- ReportType ----------- + +// Receive +Message::ReportType::ReportType(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::ReportType::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mConfirmed = getBoolean(message, mParseOffset).value(); + mTypeID = getString(message, mParseOffset, 41).value(); + mFixed = getBoolean(message, mParseOffset).value(); + mServerRemote = getBoolean(message, mParseOffset).value(); + mServerProjectIndepended = getBoolean(message, mParseOffset).value(); + mTypeGroupName = getString(message, mParseOffset, 39).value(); + mSPITTypeName = getString(message, mParseOffset, 64).value(); + mDelay = getLong(message, mParseOffset).value(); + mDuration = getLong(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse ReportType message"); + return false; + } + return true; +} +// Send +Message::ReportType::ReportType() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::ReportType::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = mHeader->BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setBoolean(mConfirmed, message, mParseOffset); + mParseOffset += setString(mTypeID, message, mParseOffset, 41); + mParseOffset += setBoolean(mFixed, message, mParseOffset); + mParseOffset += setBoolean(mServerRemote, message, mParseOffset); + mParseOffset += setBoolean(mServerProjectIndepended, message, mParseOffset); + mParseOffset += setString(mTypeGroupName, message, mParseOffset, 39); + mParseOffset += setString(mSPITTypeName, message, mParseOffset, 64); + mParseOffset += setLong(mDelay, message, mParseOffset); + mParseOffset += setLong(mDuration, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create ReportType message"); + return false; + } + return true; +} + +//----------- ReportType ----------- + +// Receive +Message::RemoveType::RemoveType(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::RemoveType::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mTypeID = getString(message, mParseOffset, 41).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse RemoveType message"); + return false; + } + return true; +} +// Send +Message::RemoveType::RemoveType() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::RemoveType::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mTypeID, message, mParseOffset, 41); + } catch (...) { + ESP_LOGE(TAG, "failed to create RemoveType message"); + return false; + } + return true; +} + +//---------------------------- SPITObjects ---------------------------------- + +//----------- ReportObject ----------- +// Receive +Message::ReportObject::ReportObject(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::ReportObject::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mConfirmed = getBoolean(message, mParseOffset).value(); + mSPITObjectID = getString(message, mParseOffset, 41).value(); + mFixed = getBoolean(message, mParseOffset).value(); + mServerRemote = getBoolean(message, mParseOffset).value(); + mSPITObjectName = getString(message, mParseOffset, 64).value(); + mTypeID = getString(message, mParseOffset, 41).value(); + mServerParams = getString(message, mParseOffset, 64).value(); + mFrameStart = getLong(message, mParseOffset).value(); + mFrameLength = getLong(message, mParseOffset).value(); + mFrameFadeInLength = getLong(message, mParseOffset).value(); + mFrameFadeOutLength = getLong(message, mParseOffset).value(); + mValueFactor = getInt(message, mParseOffset).value(); + mValueUse = getByte(message, mParseOffset).value(); + mTrack = getInt(message, mParseOffset).value(); + mRemark = getString(message, mParseOffset, 64).value(); + mDelay = getLong(message, mParseOffset).value(); + mDuration = getLong(message, mParseOffset).value(); + mActionAssigned = getBoolean(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse ReportObject message"); + return false; + } + return true; +} +// Send +Message::ReportObject::ReportObject() : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +void Message::ReportObject::setDelay(uint64_t p_Delay) { + if (p_Delay < 0) + p_Delay = 0; + mDelay = p_Delay; +} +void Message::ReportObject::setDuration(uint64_t p_Duration) { + if (p_Duration < 0) + p_Duration = 0; + mDuration = p_Duration; +} + +bool Message::ReportObject::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setBoolean(mConfirmed, message, mParseOffset); + mParseOffset += setString(mSPITObjectID, message, mParseOffset, 41); + mParseOffset += setBoolean(mFixed, message, mParseOffset); + mParseOffset += setBoolean(mServerRemote, message, mParseOffset); + mParseOffset += setString(mSPITObjectName, message, mParseOffset, 64); + mParseOffset += setString(mTypeID, message, mParseOffset, 41); + mParseOffset += setString(mServerParams, message, mParseOffset, 64); + mParseOffset += setLong(mFrameStart, message, mParseOffset); + mParseOffset += setLong(mFrameLength, message, mParseOffset); + mParseOffset += setLong(mFrameFadeInLength, message, mParseOffset); + mParseOffset += setLong(mFrameFadeOutLength, message, mParseOffset); + mParseOffset += setInt(mValueFactor, message, mParseOffset); + mParseOffset += setByte(mValueUse, message, mParseOffset); + mParseOffset += setInt(mTrack, message, mParseOffset); + mParseOffset += setString(mRemark, message, mParseOffset, 64); + mParseOffset += setLong(mDelay, message, mParseOffset); + mParseOffset += setLong(mDuration, message, mParseOffset); + mParseOffset += setBoolean(mActionAssigned, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create ReportObject message"); + return false; + } + return true; +} + +//----------- RemoveObject ----------- +// Receive +Message::RemoveObject::RemoveObject(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::RemoveObject::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mSPITObjectID = getString(message, mParseOffset, 41).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse RemoveObject message"); + return false; + } + return true; +} + +// Send +Message::RemoveObject::RemoveObject() : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::RemoveObject::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mSPITObjectID, message, mParseOffset, 41); + } catch (...) { + ESP_LOGE(TAG, "failed to create RemoveObject message"); + return false; + } + return true; +} + +//---------------------------- SPITPlayer ---------------------------------- + +//----------- PlayStart ----------- +// Receive +Message::PlayStart::PlayStart(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::PlayStart::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mPlayerID = getString(message, mParseOffset, 41).value(); + mPlayerName = getString(message, mParseOffset, 64).value(); + mPlayerType = getByte(message, mParseOffset).value(); + mPlayerIsRunning = getBoolean(message, mParseOffset).value(); + mTime = getLong(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse PlayStart message"); + return false; + } + return true; +} + +// Send +Message::PlayStart::PlayStart(string p_PlayerID, string p_PlayerName, char p_PlayerType) + : OPPart(BYTESLENGTH), + mPlayerID(p_PlayerID), + mPlayerName(p_PlayerName), + mPlayerType(p_PlayerType) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::PlayStart::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mPlayerID, message, mParseOffset, 41); + mParseOffset += setString(mPlayerName, message, mParseOffset, 64); + mParseOffset += setByte(mPlayerType, message, mParseOffset); + mParseOffset += setBoolean(mPlayerIsRunning, message, mParseOffset); + mParseOffset += setLong(mTime, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create PlayStart message"); + return false; + } + return true; +} + +//----------- PlayObject ----------- + +// Receive +Message::PlayObject::PlayObject(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::PlayObject::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mPlayerID = getString(message, mParseOffset, 41).value(); + mSPITObjectID = getString(message, mParseOffset, 41).value(); + mFrameInsideObject = getLong(message, mParseOffset).value(); + mPlayPrePosition = getBoolean(message, mParseOffset).value(); + mValue = getInt(message, mParseOffset).value(); + mValueFlag = getByte(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse PlayObject message"); + return false; + } + return true; +} +// Send +Message::PlayObject::PlayObject() + : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} +bool Message::PlayObject::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mPlayerID, message, mParseOffset, 41); + mParseOffset += setString(mSPITObjectID, message, mParseOffset, 41); + mParseOffset += setLong(mFrameInsideObject, message, mParseOffset); + mParseOffset += setBoolean(mPlayPrePosition, message, mParseOffset); + mParseOffset += setInt(mValue, message, mParseOffset); + mParseOffset += setByte(mValueFlag, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create PlayObject message"); + return false; + } + return true; +} + +//----------- PlayEnd ----------- + +// Receive +Message::PlayEnd::PlayEnd(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::PlayEnd::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mPlayerID = getString(message, mParseOffset, 41).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse PlayEnd message"); + return false; + } + return true; +} +// Send +Message::PlayEnd::PlayEnd(string p_PlayerID) + : OPPart(BYTESLENGTH), + mPlayerID(p_PlayerID) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +bool Message::PlayEnd::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mPlayerID, message, mParseOffset, 41); + } catch (...) { + ESP_LOGE(TAG, "failed to create PlayEnd message"); + return false; + } + return true; +} + +//----------- ReportPlayStatus ----------- + +// Receive +Message::ReportPlayStatus::ReportPlayStatus(unique_ptr
p_Header) + : OPPart(move(p_Header), OPFLAG, BYTESLENGTH) { +} +bool Message::ReportPlayStatus::parse(const std::vector &message) { + if (!mHeader) { + return false; + } + int intOPFlag = mHeader->getType(); + if (intOPFlag != OPFLAG) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mBytesCount = getInt(message, mParseOffset).value(); + mSPITObjectID = getString(message, mParseOffset, 41).value(); + mFrameInsideObject = getLong(message, mParseOffset).value(); + mValue = getInt(message, mParseOffset).value(); + } catch (...) { + ESP_LOGE(TAG, "failed to parse ReportPlayStatus message"); + return false; + } + return true; +} +// Send +Message::ReportPlayStatus::ReportPlayStatus() : OPPart(BYTESLENGTH) { + setHeader(OPFLAG); + mParseOffset = Header::BYTESLENGTH; +} + +void Message::ReportPlayStatus::setID(string p_ID) { + mSPITObjectID = p_ID; +} +void Message::ReportPlayStatus::setFrameInsideObject(uint64_t p_TimeInside) { + mFrameInsideObject = p_TimeInside; +} +void Message::ReportPlayStatus::setValue(int p_Value) { + mValue = p_Value; +} +bool Message::ReportPlayStatus::fillBytes(std::vector &message) { + if (!mHeader) { + return false; + } + if (mHeader->fillBytes(message) == false) + return false; + try { + mParseOffset = Header::BYTESLENGTH; + mParseOffset += setInt(mBytesCount, message, mParseOffset); + mParseOffset += setString(mSPITObjectID, message, mParseOffset, 41); + mParseOffset += setLong(mFrameInsideObject, message, mParseOffset); + mParseOffset += setInt(mValue, message, mParseOffset); + } catch (...) { + ESP_LOGE(TAG, "failed to create ReportPlayStatus message"); + return false; + } + return true; +} + +//---------------------------- static helper functions ----------------------- +bool Message::isSystemLittleEndian() { + // todo replace with #include then if constexpr (std::endian::native == std::endian::big) + int intOne = 1; + char *firstByte; + firstByte = (char *)&intOne; + if (*firstByte > 0) { + return false; + } + return true; +} + +optional Message::getInt(const std::vector &message, int &offset) { + if (offset < 0 || offset + 4 > message.size()) + return std::nullopt; + // low byte first + int intValue = 0; + if (isSystemLittleEndian() == false) { + intValue += ((message[offset + 0] & 0x000000ff)); + intValue += ((message[offset + 1] & 0x000000ff) << 8); + intValue += ((message[offset + 2] & 0x000000ff) << 16); + intValue += ((message[offset + 3] & 0x000000ff) << 24); + } else { + intValue += ((message[offset + 3] & 0x000000ff)); + intValue += ((message[offset + 2] & 0x000000ff) << 8); + intValue += ((message[offset + 1] & 0x000000ff) << 16); + intValue += ((message[offset + 0] & 0x000000ff) << 24); + } + offset += 4; + return intValue; +} + +int Message::setInt(int p_Value, std::vector &message, int p_Offset) { + if ((p_Offset < 0) || (message.size() < p_Offset + 4)) + throw "ParseError_send"; + + // low byte first + if (isSystemLittleEndian() == false) { + message[p_Offset + 0] = (char)(p_Value & 0x000000ff); + message[p_Offset + 1] = (char)((p_Value & 0x0000ff00) >> 8); + message[p_Offset + 2] = (char)((p_Value & 0x00ff0000) >> 16); + message[p_Offset + 3] = (char)((p_Value & 0xff000000) >> 24); + } else { + message[p_Offset + 3] = (char)(p_Value & 0x000000ff); + message[p_Offset + 2] = (char)((p_Value & 0x0000ff00) >> 8); + message[p_Offset + 1] = (char)((p_Value & 0x00ff0000) >> 16); + message[p_Offset + 0] = (char)((p_Value & 0xff000000) >> 24); + } + return 4; +} + +optional Message::getLong(const std::vector &message, int &offset) { + if (offset < 0 || offset + 8 > message.size()) + return std::nullopt; + + // low byte first + uint64_t intValue = 0; + if (isSystemLittleEndian() == false) { + intValue += ((uint64_t)(message[offset] & 0x000000ff)); + intValue += ((uint64_t)(message[offset + 1] & 0x000000ff) << 8); + intValue += ((uint64_t)(message[offset + 2] & 0x000000ff) << 16); + intValue += ((uint64_t)(message[offset + 3] & 0x000000ff) << 24); + intValue += ((uint64_t)(message[offset + 4] & 0x000000ff) << 32); + intValue += ((uint64_t)(message[offset + 5] & 0x000000ff) << 40); + intValue += ((uint64_t)(message[offset + 6] & 0x000000ff) << 48); + intValue += ((uint64_t)(message[offset + 7] & 0x000000ff) << 56); + } else { + intValue += ((uint64_t)(message[offset + 7] & 0x000000ff)); + intValue += ((uint64_t)(message[offset + 6] & 0x000000ff) << 8); + intValue += ((uint64_t)(message[offset + 5] & 0x000000ff) << 16); + intValue += ((uint64_t)(message[offset + 4] & 0x000000ff) << 24); + intValue += ((uint64_t)(message[offset + 3] & 0x000000ff) << 32); + intValue += ((uint64_t)(message[offset + 2] & 0x000000ff) << 40); + intValue += ((uint64_t)(message[offset + 1] & 0x000000ff) << 48); + intValue += ((uint64_t)(message[offset + 0] & 0x000000ff) << 56); + } + offset += 8; + return intValue; +} + +int Message::setLong(uint64_t p_Value, std::vector &message, int p_Offset) { + if ((p_Offset < 0) || (message.size() < p_Offset + 8)) + throw "ParseError_send"; + // low byte first + if (isSystemLittleEndian() == false) { + message[p_Offset + 0] = (char)((p_Value & 0x00000000000000ff)); + message[p_Offset + 1] = (char)((p_Value & 0x000000000000ff00) >> 8); + message[p_Offset + 2] = (char)((p_Value & 0x0000000000ff0000) >> 16); + message[p_Offset + 3] = (char)((p_Value & 0x00000000ff000000) >> 24); + message[p_Offset + 4] = (char)((p_Value & 0x000000ff00000000) >> 32); + message[p_Offset + 5] = (char)((p_Value & 0x0000ff0000000000) >> 40); + message[p_Offset + 6] = (char)((p_Value & 0x00ff000000000000) >> 48); + message[p_Offset + 7] = (char)((p_Value & 0xff00000000000000) >> 56); + } else { + message[p_Offset + 7] = (char)((p_Value & 0x00000000000000ff)); + message[p_Offset + 6] = (char)((p_Value & 0x000000000000ff00) >> 8); + message[p_Offset + 5] = (char)((p_Value & 0x0000000000ff0000) >> 16); + message[p_Offset + 4] = (char)((p_Value & 0x00000000ff000000) >> 24); + message[p_Offset + 3] = (char)((p_Value & 0x000000ff00000000) >> 32); + message[p_Offset + 2] = (char)((p_Value & 0x0000ff0000000000) >> 40); + message[p_Offset + 1] = (char)((p_Value & 0x00ff000000000000) >> 48); + message[p_Offset + 0] = (char)((p_Value & 0xff00000000000000) >> 56); + } + return 8; +} + +optional Message::getBoolean(const std::vector &message, int &offset) { + if (offset < 0 || offset + 1 > message.size()) + return std::nullopt; + return {message[offset++] > 0x00}; +} + +int Message::setBoolean(bool p_Value, std::vector &message, int p_Offset) { + if ((p_Offset < 0) || (message.size() < p_Offset + 1)) + throw "ParseError_send"; + + if (p_Value == false) { + message[p_Offset] = 0x00; + } else { + message[p_Offset] = 0x01; + } + return 1; +} + +optional Message::getByte(const std::vector &message, int &offset) { + if (offset < 0 || offset + 1 > message.size()) + return std::nullopt; + + return {message[offset++]}; +} + +int Message::setByte(int p_Value, std::vector &message, int p_Offset) { + if ((p_Offset < 0) || (message.size() < p_Offset + 1)) + throw "ParseError_send"; + + message[p_Offset] = (char)(p_Value & 0xff); + return 1; +} + +optional Message::getString(const std::vector &message, int &offset, int maxLength) { + if (offset < 0 || offset + maxLength > message.size()) + return std::nullopt; + + size_t len = strnlen(&message[offset], maxLength); + if (len >= maxLength) + return std::nullopt; + + string str(&message[offset], len); + offset += maxLength; + return str; +} + +int Message::setString(string p_String, std::vector &message, int offset, int maxLength) { + if ((offset < 0) || (maxLength < 1) || (message.size() < offset + maxLength)) + throw "ParseError_send"; + + int intLength; + if (p_String.length() + 1 > maxLength) { + intLength = maxLength; + } else { + intLength = p_String.length() + 1; + } + int intChar = 0; + bool boolNullChar = false; + while (intChar < intLength) { + message[offset + intChar] = p_String.data()[intChar]; + if (p_String[intChar] == '\0') { + boolNullChar = true; + break; + } + intChar++; + } + if (boolNullChar == false) { + message[offset + intLength - 1] = '\0'; + } + return maxLength; +} +} \ No newline at end of file diff --git a/components/spit/src/spit_object.cpp b/components/spit/src/spit_object.cpp new file mode 100644 index 0000000..8e6dc1b --- /dev/null +++ b/components/spit/src/spit_object.cpp @@ -0,0 +1,202 @@ + +#include "spit_object.hpp" +#include "spit_socket.hpp" +#include "esp_log.h" + +namespace spit { + +/** + * clears all unconfirmed SObjects not created by this SPIT_Client. + */ +void SObject::clearUnconfirmedObjects(SpitRepo &repo) { + auto &entries = repo.getRepo(); + for (auto i = entries.begin(); i != entries.end();) { + SObject *ptrSObject = i->second.get(); + if (ptrSObject->getCreatedByClient() == false && ptrSObject->getConfirmed() == false) { + i = entries.erase(i); + } else { + i++; + } + } +} + +void SObject::eventProjectClosed(SpitRepo &repo, SpitRepo &remoteRepo) { + auto &entries = repo.getRepo(); + for (auto i = entries.begin(); i != entries.end(); ) { + SObject *ptrSObject = i->second.get(); + if (ptrSObject->getCreatedByClient() == true) { + i++; + continue; + } + i = entries.erase(i); + } + // Remote + remoteRepo.clearEntries(); +} + +/** + * sets confirmed = false for all SObjects. + * This should be called when the client has loaded a project. + */ +void SObject::setConfirmedFalse(SpitRepo &repo) { + auto &entries = repo.getRepo(); + for (auto i = entries.begin(); i != entries.end(); i++) { + i->second->setConfirmed(false); + } +} + +bool SObject::setMessageOP(Message::ReportObject *message) { + bool changeAllowed; + + if ((mCreatedByClient == true) && (Fixed.mValue == true)) { + changeAllowed = false; + } else { + changeAllowed = true; + } + + // Confirmation should be changable + if (mCreatedByClient) { + // Server reports a client created SPITType -> the type is confirmed + mConfirmed = true; + } else { + // type is NOT client created -> the client has to confirm the SPITtype + mConfirmed = message->getConfirmed(); + } + + if (changeAllowed == false) { + return false; + } + bool changed = false; + if (Delay.mValue != message->getDelay()) { + Delay.mValue = message->getDelay(); + changed = true; + } + if (Duration.mValue != message->getDuration()) { + Duration.mValue = message->getDuration(); + changed = true; + } + if (Fixed.mValue != message->getFixed()) { + Fixed.mValue = message->getFixed(); + changed = true; + } + if (FrameFadeInLength.mValue != message->getFrameFadeInLength()) { + FrameFadeInLength.mValue = message->getFrameFadeInLength(); + changed = true; + } + if (FrameFadeOutLength.mValue != message->getFrameFadeOutLength()) { + FrameFadeOutLength.mValue = message->getFrameFadeOutLength(); + changed = true; + } + if (FrameLength.mValue != message->getFrameLength()) { + FrameLength.mValue = message->getFrameLength(); + changed = true; + } + if (FrameStart.mValue != message->getFrameStart()) { + FrameStart.mValue = message->getFrameStart(); + changed = true; + } + if (Remark.mValue != message->getRemark()) { + Remark.mValue = message->getRemark(); + changed = true; + } + if (mID != message->getID()) { + mID = message->getID(); + changed = true; + } + if (Name.mValue != message->getName()) { + Name.mValue = message->getName(); + changed = true; + } + if (SPITTypeID.mValue != message->getSPITTypeID()) { + SPITTypeID.mValue = message->getSPITTypeID(); + changed = true; + } + if (ServerParams.mValue != message->getServerParams()) { + ServerParams.mValue = message->getServerParams(); + changed = true; + } + if (ServerRemote.mValue != message->getServerRemote()) { + ServerRemote.mValue = message->getServerRemote(); + changed = true; + } + if (Track.mValue != message->getTrack()) { + Track.mValue = message->getTrack(); + changed = true; + } + if (mValueFactor != message->getValueFactor()) { + mValueFactor = message->getValueFactor(); + changed = true; + } + if (mValueUse != message->getValueUse()) { + mValueUse = message->getValueUse(); + changed = true; + } + return changed; +} + +void SObject::fillMessageOP(Message::ReportObject &message) { + message.setActionAssigned(getActionAssigned()); // Action can be assigned to type or to this object + message.setConfirmed(mConfirmed); + message.setDelay(Delay.mValue); + message.setDuration(Duration.mValue); + message.setFixed(Fixed.mValue); + message.setFrameFadeInLength(FrameFadeInLength.mValue); + message.setFrameFadeOutLength(FrameFadeOutLength.mValue); + message.setFrameLength(FrameLength.mValue); + message.setFrameStart(FrameStart.mValue); + message.setID(mID); + message.setName(Name.mValue); + message.setRemark(Remark.mValue); + message.setSPITTypeID(SPITTypeID.mValue); + message.setServerParams(ServerParams.mValue); + message.setServerRemote(ServerRemote.mValue); + message.setTrack(Track.mValue); + message.setValueFactor(mValueFactor); + message.setValueUse(mValueUse); +} + +void SObject::fillMessageOP(Message::RemoveObject &message) { + message.setID(mID); +} + +/** + * This is used by the server and client to tell if the other side has registered the changes. + * @param p_Confirmed + * @return if mConfirmed has changed + */ +bool SObject::setConfirmed(bool p_Confirmed) { + if (mConfirmed == p_Confirmed) + return false; + mConfirmed = p_Confirmed; + return true; +} + +/** + * This is a virtual construct - ActionAssignedType is not implemented in the SPITProtocol. + * This is used when an action is associated with a SPITType. + * Then this action is done for every SObject with the SPITType. + * @return true = an action is assigned for the SPITType. + */ +bool SObject::setActionAssignedType(bool p_ActionAssigned) { + if (mActionAssignedType == p_ActionAssigned) + return false; + bool oldActionAssigend = getActionAssigned(); + mActionAssignedType = p_ActionAssigned; + if (getActionAssigned() == oldActionAssigend) + return false; + mConfirmed = false; + return true; +} + +bool SObject::setActionAssigned(bool p_ActionAssigned) { + if (mActionAssigned == p_ActionAssigned) + return false; + bool oldActionAssigend = getActionAssigned(); + mActionAssigned = p_ActionAssigned; + if (getActionAssigned() == oldActionAssigend) + return false; + mConfirmed = false; + return true; +} + +} diff --git a/components/spit/src/spit_player.cpp b/components/spit/src/spit_player.cpp new file mode 100644 index 0000000..833d438 --- /dev/null +++ b/components/spit/src/spit_player.cpp @@ -0,0 +1,93 @@ + +#include "spit_player.hpp" +#include "esp_log.h" +#include +#include + +using namespace std; + +namespace spit { + +unordered_map> SPlayer::sSpitPlayers; + +SPlayer &SPlayer::getSPITPlayer(string playerID) { + auto i = sSpitPlayers.find(playerID); + if (i == sSpitPlayers.end()) { + sSpitPlayers[playerID] = make_unique(playerID); + } + return *sSpitPlayers.at(playerID); +} + +SPlayer::SPlayer(string playerID) { + mId = playerID; +} + +SPlayer::~SPlayer() { + mPlaySPITTypes.clear(); +} + +/** + * + * @param p_SPITObjectID + * @return true = new PlayObject added
+ * false = PlayObject exists + */ +SPlayer::PlayType *SPlayer::addTypePlaying(string spitTypeID) { + PlayType *ptrPlaySPITType; + auto i = mPlaySPITTypes.find(spitTypeID); + if (i == mPlaySPITTypes.end()) { + auto playSpitType = make_unique(this, spitTypeID); + ptrPlaySPITType = playSpitType.get(); + mPlaySPITTypes[spitTypeID] = move(playSpitType); + } else { + ptrPlaySPITType = i->second.get(); + } + ptrPlaySPITType->setIsPlayed(true); + return ptrPlaySPITType; +} + +void SPlayer::startPlaySequence() { + for (auto i = mPlaySPITTypes.begin(); i != mPlaySPITTypes.end(); i++) { + PlayType *ptrPlaySPITType = i->second.get(); + ptrPlaySPITType->startPlaySequence(); + } +} + +void SPlayer::endPlaySequence() { + for (auto i = mPlaySPITTypes.begin(); i != mPlaySPITTypes.end();) { + if (i->second.get()->getIsPlayed() == false) { + i = mPlaySPITTypes.erase(i); + } else { + ++i; + } + } +} + +SPlayer::PlayType::PlayType(SPlayer *p_SPITPlayer, string spitTypeID) { + mIsNew = true; + + mSpitTypeID = spitTypeID; + mPlayerID = p_SPITPlayer->getID(); + mPlayerName = p_SPITPlayer->getName(); + startPlaySequence(); + mIsNew = true; +} +SPlayer::PlayType::PlayType(const PlayType &orig) { + mSpitTypeID = orig.getSPITTypeID(); + mIsPlayed = orig.getIsPlayed(); + mIsNew = orig.getIsNew(); + + mPlayerID = orig.getPlayerID(); + mPlayerName = orig.getPlayerName(); + mPlayerRuns = orig.getPlayerIsRunning(); + + mValue = orig.getValue(); +} +void SPlayer::PlayType::startPlaySequence() { + mIsPlayed = false; + mIsNew = false; + + mValue = 0; +} + +} \ No newline at end of file diff --git a/components/spit/src/spit_processor.cpp b/components/spit/src/spit_processor.cpp new file mode 100644 index 0000000..c2ea248 --- /dev/null +++ b/components/spit/src/spit_processor.cpp @@ -0,0 +1,745 @@ + +#include "spit_processor.hpp" +#include "esp_log.h" + +namespace spit { +static const char *TAG = "SPIT"; + +/** + * Create a new SPIT_client instance, should be done once by the user app. + * @param p_SPITClientInterface the user app should implement the SPIT_ClientInterface. + */ +SProcessor::SProcessor(string name, string projName, string hostName, string guid, Callbacks calls, InternalCallbacks intern) + : mCallbacks(calls), + mInternCallbacks(intern), + mName(name), + mGuid(guid), + mProjName(projName), + mComputerName(hostName) { +} + +SProcessor::~SProcessor() { +} + +unique_ptr SProcessor::createConnectMessage(string serverIP, string serverPort, string loginName, string loginPassword) { + auto msg = make_unique(getComputerName(), mName); + msg->setLoginName(loginName); + msg->setLoginPassword(loginPassword); + msg->setServerIP(serverIP); + if (serverPort.length() <= 0) { + serverPort = "0"; + } + int intServerPort = stoi(serverPort); + msg->setServerPort(intServerPort); + return msg; +} + +/** + * Virtual construct - means actionAssigned for SPITTypes is is not implemented in SPITProtocol. + * This can used to setActionAssigned to all SPITObjects with this SPITType. + * This may be used in the case when the same action should be done for every SPITObject with this SPITType associated. + * This tells the server not what action is done, but it tells the server that there is an action. + * @param typeId the id of the SPITType + * @param actionAssigned + */ +void SProcessor::setActionType(string typeId, bool actionAssigned) { + SType *spitType = mSpitTypes.getEntry(typeId); + if (spitType == nullptr) + return; + if (spitType->ActionAssigned.set(actionAssigned)) { + sendTypeInternal(*spitType); + for (auto &[key, value] : mSpitObjects.getRepo()) { + if (value->SPITTypeID.get() == spitType->getID()) { + if (value->setActionAssignedType(actionAssigned) == true) { + sendObjectInternal(value.get()); + } + } + } + } +} + +/** This should be called if the user wants to associate/remove an action with a SPITObject. */ +void SProcessor::setActionObject(string spitObjId, bool actionAssigned) { + SObject *spitObject = mSpitObjects.getEntry(spitObjId); + if (spitObject == nullptr) + return; + if (spitObject->setActionAssigned(actionAssigned) == true) { + sendObjectInternal(spitObject); + } +} + +std::vector SProcessor::createMessage(Message::OPPart &opPart) { + if (mMessageNumberSend == 255) + mMessageNumberSend = 1; + else + mMessageNumberSend++; + opPart.setMessageNumber(mMessageNumberSend); + opPart.setServerInfoInHeader(mGuid, mServerID); + + int opFlag = opPart.getHeader()->getType(); + try { + return Message::createMessage(opPart); + } catch (...) { + ESP_LOGD(TAG, "Exception client OPFlag: %d", opFlag); + } + return {}; +} + +void SProcessor::resetMessageNumberReceived() { + mMessageNumberReceived = -1; + mOvertakingMessages.clear(); +} + +void SProcessor::checkParseMessageData(vector data) { + auto message = make_unique(data); + // answerWatchdog(); + if (!message->getHeader() || !message->getOPPart()) { + ESP_LOGD(TAG, "Client::parseMessage incomplete message"); + return; + } + + int msgNum = (int)(message->getHeader()->getMessageNumber() & 0xFF); + + if (msgNum == 0) { + parseMessage(*message); + return; + } + if (mMessageNumberReceived < 0) { + mMessageNumberReceived = msgNum; + parseMessage(*message); + return; + } + + int msgNumNext = mMessageNumberReceived + 1; + if (msgNumNext > 255 || msgNumNext < 1) + msgNumNext = 1; + + if (msgNum == msgNumNext) { + mMessageNumberReceived = msgNum; + parseMessage(*message); + + mMessageNumberReceived = sendOvertakingMessage(mMessageNumberReceived); + } else { + // message has overtaken another message + addOvertakingMessage(move(message)); + } +} + +void SProcessor::addOvertakingMessage(unique_ptr message) { + if (!message) { + return; + } + if (mOvertakingMessages.size() > MAX_OVERTAKING_SPIT_MESSAGES) { + ESP_LOGD(TAG, "Client receiveing: perhaps message lost !!!!!"); + } + + int intMessageNumber = (int)(message->getHeader()->getMessageNumber() & 0xFF); + for (auto it = mOvertakingMessages.begin(); it != mOvertakingMessages.end(); ++it) { + const auto &overtaking = *it; + int intOvertakingMessageNumber = (int)(overtaking->getHeader()->getMessageNumber() & 0xFF); + if (intMessageNumber < intOvertakingMessageNumber) { + mOvertakingMessages.insert(it, move(message)); + return; + } + } + mOvertakingMessages.push_back(move(message)); +} + +int SProcessor::sendOvertakingMessage(int msgNumRec) { + // check overtaking messages + auto it = mOvertakingMessages.begin(); + while (mOvertakingMessages.size() > 0) { + + int msgNumNext = msgNumRec + 1; + if (msgNumNext > 255 || msgNumNext < 1) + msgNumNext = 1; + + auto &overtakingMessage = *it; + int msgNum = (int)(overtakingMessage->getHeader()->getMessageNumber() & 0xFF); + + if (msgNum == msgNumNext) { + msgNumRec = msgNum; + parseMessage(*overtakingMessage); + mOvertakingMessages.erase(it); + it = mOvertakingMessages.begin(); + } else { + break; + } + } + return msgNumRec; +} + +void SProcessor::parseMessage(Message &message) { + // answerWatchdog(); + if (!message.getHeader() || !message.getOPPart()) { + ESP_LOGD(TAG, "Client::parseMessage incomplete message"); + return; + } + + switch (message.getType()) { + //----- Connection ----- + case Message::ReportProject::OPFLAG: + parseReportProject(message.getAs()); + break; + //----- SPIT Project ----- + case Message::StartProjectTransfer::OPFLAG: + parseStartProjectTransfer(message.getAs()); + break; + case Message::CloseProject::OPFLAG: + parseCloseProject(message.getAs()); + break; + case Message::RequestProjectTransfer::OPFLAG: + parseRequestProjectTransfer(message.getAs()); + break; + case Message::EndProjectTransfer::OPFLAG: + parseEndProjectTransfer(message.getAs()); + break; + //----- SPIT Types ------ + case Message::ReportType::OPFLAG: + parseReportType(message.getAs()); + break; + case Message::RemoveType::OPFLAG: + parseRemoveType(message.getAs()); + //----- SPIT Objects ------ + break; + case Message::ReportObject::OPFLAG: + parseReportObject(message.getAs()); + break; + case Message::RemoveObject::OPFLAG: + parseRemoveObject(message.getAs()); + break; + //----- Play SPIT Objects ------ + case Message::PlayStart::OPFLAG: + parsePlayStart(message.getAs()); + break; + case Message::PlayObject::OPFLAG: + parsePlayObject(message.getAs()); + break; + case Message::PlayEnd::OPFLAG: + parsePlayEnd(message.getAs()); + break; + case Message::ReportPlayStatus::OPFLAG: + parseReportPlayStatus(message.getAs()); + break; + default: + ESP_LOGD(TAG, "Client::parseMessage: Unknown Message"); + } +} + +//----- SPIT Project ------ +void SProcessor::parseReportProject(Message::ReportProject &message) { + ESP_LOGD(TAG, "parse ReportProject"); + mServerProjectID = message.getProjectID(); + mServerProjectName = message.getProjectName(); + mServerFramesPerSecond = message.getFramesPerSecond(); + mCallbacks.onProjectUpdate(ProjUpdate::ServerLoaded, mServerProjectID); + + // Set all SPITTypes and SPITObjects confirmed = false + SType::setConfirmedFalse(mSpitTypes); + SObject::setConfirmedFalse(mSpitObjects); + + // Send RequestProjectTransfer + Message::RequestProjectTransfer sMRequestProjectTransfer; + if (mInternCallbacks.sendMessage(sMRequestProjectTransfer) < 0) { + ESP_LOGD(TAG, "Client::parseReportProject could not send RequestProjectTransfer"); + } + + // todo Don't send the ReportProject here it should be sended after parseEndProjectTransfer + // Send client's ReportProject + Message::ReportProject reportProject(mProjName, mProjName); + reportProject.setFramesPerSecond(0); + if (mInternCallbacks.sendMessage(reportProject) < 0) { + ESP_LOGD(TAG, "Client::parseReportProject could not send Client's ReportProject"); + } +} + +void SProcessor::parseCloseProject(Message::CloseProject &p_CloseSPITProject) { + ESP_LOGD(TAG, "parse CloseProject"); + string stringProjectID; + mCallbacks.onProjectUpdate(ProjUpdate::ServerClosed, p_CloseSPITProject.getProjectID()); + SPlayer::clearPlayers(); +} + +void SProcessor::parseRequestProjectTransfer(Message::RequestProjectTransfer &message) { + ESP_LOGD(TAG, "parse RequestProjectTransfer"); + // StartProjectTransfer + Message::StartProjectTransfer sMStartProjectTransfer; + if (mInternCallbacks.sendMessage(sMStartProjectTransfer) < 0) { + return; + } + + // send all listes SPITTypes + for (const auto &[key, value] : mSpitTypes.getRepo()) { + sendTypeInternal(*value); + mInternCallbacks.delay(10); + } + for (auto &[key, value] : mSpitObjects.getRepo()) { + sendObjectInternal(value.get()); + mInternCallbacks.delay(10); + } + + // EndProjectTransfer + Message::EndProjectTransfer sMEndProjectTransfer; + mInternCallbacks.sendMessage(sMEndProjectTransfer); +} + +void SProcessor::parseStartProjectTransfer(Message::StartProjectTransfer &message) { + mCallbacks.onProjectUpdate(ProjUpdate::StartTransfer, mServerProjectID); +} + +void SProcessor::parseEndProjectTransfer(Message::EndProjectTransfer &message) { + SObject::clearUnconfirmedObjects(mSpitObjects); + mCallbacks.onProjectUpdate(ProjUpdate::EndTransfer, mServerProjectID); + + // Send Message::ReportProject after the mSPITClientInterface is notfied of EndProjectTransfer + Message::ReportProject reportSPITProject(mProjName, mProjName); + reportSPITProject.setFramesPerSecond(0); + if (mInternCallbacks.sendMessage(reportSPITProject) < 0) { + ESP_LOGD(TAG, "Client::parseReportProject could not send Client's ReportProject"); + } +} + +void SProcessor::parseReportType(Message::ReportType &message) { + ESP_LOGD(TAG, "parse ReportType ID %s Name %s", message.getID().c_str(), message.getName().c_str()); + SType *sType; + bool isNew = false; + bool isChanged = false; + if (message.getServerRemote() == false) { + sType = mSpitTypes.getEntry(message.getID()); + if (sType == nullptr) { + // The user's app may veto the SPITType + bool createdByClient = false; + if (sType) { + createdByClient = sType->getCreatedByClient(); + } + + if (mCallbacks.onCheckSpitTypeVeto(false, createdByClient) == false) { + ESP_LOGW(TAG, "TypeVeto %s / %s", message.getID().c_str(), message.getName().c_str()); + return; + } + auto spitType = make_unique(message.getID()); + sType = spitType.get(); + mSpitTypes.addEntry(move(spitType)); + isNew = true; + } + } else { + sType = mRemoteSpitTypes.getEntry(message.getID()); + if (sType == nullptr) { + auto spitType = make_unique(message.getID()); + sType = spitType.get(); + mRemoteSpitTypes.addEntry(move(spitType)); + isNew = true; + } + } + if (!sType->setMessageOP(message)) { + // SPITType is not changable -> send back to the server + sendTypeInternal(*sType); + return; + } + + auto eventType = isNew ? ObjectEvent::Created : ObjectEvent::Changed; + // add or change SPITType + mCallbacks.onSpitType(eventType, sType->ServerRemote.get(), *sType); + + if (sType != nullptr) { + if (sType->setConfirmed(true) == true || isChanged == true) { + sendTypeInternal(*sType); + } + } +} +void SProcessor::parseRemoveType(Message::RemoveType &message) { + ESP_LOGD(TAG, "parse RemoveType %s", message.getID().c_str()); + bool isRemote = false; + string sTypeID = message.getID(); + + SType *sType = mRemoteSpitTypes.getEntry(sTypeID); + if (!sType) { + isRemote = true; + } else { + sType = mSpitTypes.getEntry(sTypeID); + if (!sType) { + isRemote = false; + } else { + // No SType with sTypeID is listet + return; + } + } + + if (sType->getCreatedByClient() && sType->Fixed.get()) { + // the SPITyte is created by client himself and it is fixed -> don't remove the SType!! + // Send the SPITTyp back to the server + sendTypeInternal(*sType); + return; + } + + mCallbacks.onSpitType(ObjectEvent::Deleted, isRemote, *sType); + if (isRemote == false) { + mSpitTypes.removeEntry(*sType); + } else { + mRemoteSpitTypes.removeEntry(*sType); + } +} + +/** + * Change an existing SPIT_Type. + * @param p_SPITType the SPITType vont + * @return true = changed, false = not changed maybe p_SPITType is NULL or the SPITType doesn't exists + */ +bool SProcessor::changeType(SType *sType) { + if (sType == nullptr) + return false; + SType *mod = mSpitTypes.getEntry(sType->getID()); + if (mod == nullptr) + return false; + + mod->DefaultDelay.set(sType->DefaultDelay.get()); + mod->DefaultDuration.set(sType->DefaultDuration.get()); + mod->Fixed.set(sType->Fixed.get()); + mod->GroupName.set(sType->GroupName.get()); + mod->Name.set(sType->Name.get()); + mod->ProjectIndepended.set(sType->ProjectIndepended.get()); + + mCallbacks.onSpitType(ObjectEvent::Changed, false, *mod); + sendTypeInternal(*mod); + return true; +} + +/** + * If a client has removed a SPITType where id == p_SPITTypeID, it should send it to the server. + * The corresponding SPITType if listet in SPITType will be deleted. + * An eventRemoveSPITType will occur. + * If a connection to a SPITServer is established, the SPITServer may dicard the deletion an an eventSPITType will occur + * -> the SPITType will be listet again. + * @param p_SPITType the id of the SPITType ro remove + * @return 0 = is sended, -1 = is not sended + */ +int SProcessor::removeType(string sType) { + if (sType.length() <= 0) + return -1; + SType *mod = mSpitTypes.getEntry(sType); + if (mod == nullptr) + return -1; + Message::RemoveType sMRemoveSPITType; + mod->fillMessageOP(sMRemoveSPITType); + mCallbacks.onSpitType(ObjectEvent::Deleted, false, *mod); + mSpitTypes.removeEntry(*mod); + return mInternCallbacks.sendMessage(sMRemoveSPITType); +} + +SType *SProcessor::createType(string id, string groupName, string name, uint64_t delay, uint64_t duration, bool fixed, bool projIndipendent) { + bool boolNew; + SType *sType = mSpitTypes.getEntry(id); + if (sType == nullptr) { + auto spitType = make_unique(id, groupName, name, fixed, projIndipendent, delay, duration); + sType = spitType.get(); + mSpitTypes.addEntry(move(spitType)); + sType->setCreatedByClient(true); + boolNew = true; + } else { + sType->GroupName.set(groupName); + sType->Name.set(name); + sType->Fixed.set(fixed); + sType->ProjectIndepended.set(projIndipendent); + sType->DefaultDelay.set(delay); + sType->DefaultDuration.set(duration); + boolNew = false; + } + + auto eventType = boolNew ? ObjectEvent::Created : ObjectEvent::Changed; + mCallbacks.onSpitType(eventType, false, *sType); + sendTypeInternal(*sType); + return sType; +} + +/** + * If a client has changed a SPITType, it should send it to the server to transmit the changes. + * If the SPITType is a fixed one and not created by the client himself, then an eventSPITType can be sent from the server, which cancels the changes. + * @param sType the pointer to the changed SPITType + * @return 0 = is sended, -1 = is not sended + */ +int SProcessor::sendTypeInternal(SType &sType) { + Message::ReportType reportType; + sType.fillMessageOP(reportType); + return mInternCallbacks.sendMessage(reportType); +} + +//----- SPIT Objects ------ +void SProcessor::parseReportObject(Message::ReportObject &message) { + ESP_LOGD(TAG, "parse ReportObject %s", message.getID().c_str()); + SObject *sObject; + SType *sType; + bool isNew = false; + bool isRemote = false; + bool isChanged = false; + { + ESP_LOGD(TAG, "parse ReportObject %s / %s", message.getID().c_str(), message.getName().c_str()); + if (message.getServerRemote() == false) { + isRemote = false; + sObject = mSpitObjects.getEntry(message.getID()); + sType = mSpitTypes.getEntry(message.getSPITTypeID()); + + if (sType == nullptr) { + // No associated SPITType found + if (sObject != nullptr) { + // SpitObject exists but has no corresponding SPITType -> remove the SPITObject without sending a message + // Maybe the SPITObject has changed the SPITTypeID to a SPITTypeID of another board + mSpitObjects.removeEntry(*sObject); + } + return; + } + bool isKnownType = sType != nullptr; + if (sObject == nullptr) { + // The user's app my veto the SPITType + if (mCallbacks.onCheckSpitObjectVeto(isKnownType) == false) { + ESP_LOGW(TAG, "ObjectVeto %s / %s", message.getID().c_str(), message.getName().c_str()); + return; + } + auto spitObj = make_unique(message.getID()); + sObject = spitObj.get(); + mSpitObjects.addEntry(move(spitObj)); + isNew = true; + } else { + // The user's app my veto the SPITType + if (mCallbacks.onCheckSpitObjectVeto(isKnownType) == false) { + // SpitObject exists and User APp denied the SPITObject -> remove the SPITObject without sending a message + mSpitObjects.removeEntry(*sObject); + ESP_LOGW(TAG, "TypeVeto %s / %s", message.getID().c_str(), message.getName().c_str()); + return; + } + } + } else { + isRemote = true; + sObject = mRemoteSpitObjects.getEntry(message.getID()); + sType = mRemoteSpitTypes.getEntry(message.getSPITTypeID()); + if (sType == nullptr) { + // No associated SPITType found + return; + } + if (sObject == nullptr) { + auto spitObj = make_unique(message.getID()); + sObject = spitObj.get(); + mRemoteSpitObjects.addEntry(move(spitObj)); + isNew = true; + } + } + + if (!sObject->setMessageOP(&message)) { + ESP_LOGE(TAG, "Change not allowed %s", sObject->getID().c_str()); + // setMessageOP error - the SPITObject can't be changed, it may be fixed and crated by this client + sendObjectInternal(sObject); + return; + } + + if (isRemote == false) { + // set the virtual action assignment + sObject->setActionAssignedType(sType->ActionAssigned.get()); + } + + auto eventType = isNew ? ObjectEvent::Created : ObjectEvent::Changed; + mCallbacks.onSpitObject(eventType, isRemote, *sType, *sObject); + + if (sObject->setConfirmed(true) == true || isChanged == true) { + // Confirmation has changed + sendObjectInternal(sObject); + } + } +} + +void SProcessor::parseRemoveObject(Message::RemoveObject &message) { + ESP_LOGD(TAG, "parse RemoveObject %s", message.getID().c_str()); + SObject *sObject = mRemoteSpitObjects.getEntry(message.getID()); + if (sObject != nullptr) { + SType *sType = mRemoteSpitTypes.getEntry(sObject->SPITTypeID.get()); + if (sType != nullptr) { + mCallbacks.onSpitObject(ObjectEvent::Deleted, true, *sType, *sObject); + } + mRemoteSpitObjects.removeEntry(*sObject); + } else { + // call removeObject, this will notify the server that the object is removed + removeObject(message.getID()); + } +} + +int SProcessor::sendObjectInternal(SObject *sObject) { + if (sObject == nullptr) + return -1; + Message::ReportObject reportObject; + sObject->fillMessageOP(reportObject); + return mInternCallbacks.sendMessage(reportObject); +} + +/** + * When the client changes an existing SPITObject (normally the client should not do this), then he should send the changes to the SPITServer. + * An eventSPITObject occurs. + * The SPITServer may discard the changes and a second eventSPITObject occurs to correct the changes. + * @param obj the pointer to the changed SPITObject. + * @return 0 = sended, -1 = not sended + */ +bool SProcessor::changeObject(SObject *sObject) { + if (sObject == nullptr) + return false; + SObject *mod = mSpitObjects.getEntry(sObject->getID()); + if (mod == nullptr) + return false; + SType *sType = mSpitTypes.getEntry(sObject->getID()); + if (mod == nullptr) + return false; + + mod->setActionAssigned(sObject->getActionAssigned()); + mod->Delay.set(sObject->Delay.get()); + mod->Duration.set(sObject->Delay.get()); + mod->Fixed.set(sObject->Fixed.get()); + mod->FrameFadeInLength.set(sObject->FrameFadeInLength.get()); + mod->FrameFadeOutLength.set(sObject->FrameFadeOutLength.get()); + mod->FrameLength.set(sObject->FrameLength.get()); + mod->FrameStart.set(sObject->FrameStart.get()); + mod->Name.set(sObject->Name.get()); + mod->Remark.set(sObject->Remark.get()); + mod->SPITTypeID.set(sObject->SPITTypeID.get()); + mod->ServerParams.set(sObject->ServerParams.get()); + mod->Track.set(sObject->Track.get()); + mod->ValueFactor.set(sObject->ValueFactor.get()); + mod->ValueUse.set(sObject->ValueUse.get()); + + mCallbacks.onSpitObject(ObjectEvent::Changed, false, *sType, *mod); + sendObjectInternal(mod); + return true; +} + +/** + * When the client wants to remove a SPITObject with id == p_SPITObjectID (normally the client should not do this), then he should send it to the SPITServer. + * The corresponding SPITObject if listet in SPITObject will be deleted. + * An eventRemoveSPITType will occur. + * If an connection to a SPITServer is established, the SPITServer may discard the deletion and an eventSPITObject occurs + * -> the SPITObject is listet again. + * @param objId the id of the SPITObject to remove. + * @return 0 = sended to the SPITServer, -1 = not sended to the SPITServer or nos SPITObject with id == objId exists. + */ +int SProcessor::removeObject(string objId) { + if (objId.length() <= 0) + return -1; + Message::RemoveObject remObj; + + SObject *sObject = mSpitObjects.getEntry(objId); + if (sObject == nullptr) + return -1; + SType *sType = mSpitTypes.getEntry(sObject->SPITTypeID.get()); + if (sType == nullptr) + return -1; + + sObject->fillMessageOP(remObj); + mCallbacks.onSpitObject(ObjectEvent::Deleted, false, *sType, *sObject); + mSpitObjects.removeEntry(*sObject); + return mInternCallbacks.sendMessage(remObj); +} + +// //----- Play SPIT Objects ------ + +void SProcessor::parsePlayStart(Message::PlayStart &message) { + ESP_LOGD(TAG, "parse PlayStart ID: %s Name: %s Running: %d", message.getPlayerID().c_str(), message.getPlayerName().c_str(), (int)message.getPlayerIsRunning()); + SPlayer &player = SPlayer::getSPITPlayer(message.getPlayerID()); + player.setIsRunning(message.getPlayerIsRunning()); + player.setName(message.getPlayerName()); + player.setType(message.getPlayerType()); + player.setTime(message.getTime()); + player.startPlaySequence(); + + mCallbacks.onPlay(player.getID(), player.getName(), player.getType(), player.getIsRunning(), player.getTime()); +} + +void SProcessor::parsePlayObject(Message::PlayObject &message) { + + SPlayer &player = SPlayer::getSPITPlayer(message.getPlayerID()); + SObject *sObject = mSpitObjects.getEntry(message.getSPITObjectID()); + + if (sObject == nullptr) { + return; + } + SPlayer::PlayType *playType = player.addTypePlaying(sObject->SPITTypeID.get()); + if (playType == nullptr) { + return; + } + playType->setPlayerIsRunning(player.getIsRunning()); + playType->addValue(((double)message.getValue() / 1000000), ((double)sObject->ValueFactor.get() / 1000000)); +} + +void SProcessor::parsePlayEnd(Message::PlayEnd &message) { + SPlayer &sPlayer = SPlayer::getSPITPlayer(message.getPlayerID()); + + auto &playTypes = sPlayer.getPlaySPITTypes(); + for (auto i = playTypes.begin(); i != playTypes.end(); i++) { + SPlayer::PlayType *playType = i->second.get(); + bool isStarted = playType->getIsNew(); + bool iStopped = (playType->getIsPlayed() == false); + SType *ptrSPITType = mSpitTypes.getEntry(playType->getSPITTypeID()); + if (ptrSPITType == nullptr) { + // set isplayed to false, the ptrPlayObject will be removed by sPlayer.endPlaySequence() + playType->setIsPlayed(false); + continue; + } + mCallbacks.onPlaySpitType(playType->getPlayerID(), playType->getSPITTypeID(), playType->getPlayerIsRunning(), isStarted, iStopped, playType->getValue()); + } + sPlayer.endPlaySequence(); +} + +void SProcessor::parseReportPlayStatus(Message::ReportPlayStatus &message) { + SType *sType = NULL; + + SObject *sObject = mRemoteSpitObjects.getEntry(message.getID()); + if (sObject == nullptr) { + sObject = mSpitObjects.getEntry(message.getID()); + if (sObject == nullptr) { + return; + } + } + if (sObject->ServerRemote.get() == false) { + sType = mSpitTypes.getEntry(sObject->SPITTypeID.get()); + } else { + sType = mRemoteSpitTypes.getEntry(sObject->SPITTypeID.get()); + } + + if (sType == nullptr) { + return; + } + mCallbacks.onPlayRemoteObjectStatus(*sType, *sObject, message.getFrameInsideObject(), message.getValue()); +} + +// //--------- Remote ---------- + +/** + * Send a remote control command to the SPITServer. + * There may be different command types: + * a) Some don't use value e.g. go to next scene, olay a jingle + * b) Some use values e.g. the main sound fader + * @param sObjectId the id of the remote SPITObject (this represents the remote control command) + * @param valuePpm the value in ppm + * in case a) the value is ignored + * in case b) the value is used + * @param valueFlag indicates how the server should use valuePpm: + * VALUEFLAG_DIRECT (0x00) The server overwrites its existing value with p_Value + * VALUEFLAG_INCREASE (0x01) The server increases his existing value by valuePpm + * VALUEFLAG_DECREASE (0x01) The server decreases his existing value by valuePpm + */ +void SProcessor::sendRemoteObject(string sObjectId, int valuePpm, char valueFlag) { + SObject *remoteObject = mRemoteSpitObjects.getEntry(sObjectId); + if (remoteObject == nullptr) + return; + + Message::PlayStart playSPITStart(mGuid, mProjName, SPlayer::PTYPE_DISCRETE); + playSPITStart.setPlayerIsRunning(false); + playSPITStart.setTime(0); + mInternCallbacks.sendMessage(playSPITStart); + + Message::PlayObject playSPITObject; + playSPITObject.setID(remoteObject->getID()); + playSPITObject.setPlayPrePosition(false); + playSPITObject.setPlayerID(mGuid); + playSPITObject.setValue(valuePpm); + playSPITObject.setValueFlag(valueFlag); + mInternCallbacks.sendMessage(playSPITObject); + + Message::PlayEnd playSPITEnd(mGuid); + mInternCallbacks.sendMessage(playSPITEnd); +} + +} // namespace spit \ No newline at end of file diff --git a/components/spit/src/spit_socket.cpp b/components/spit/src/spit_socket.cpp new file mode 100644 index 0000000..6e72f8d --- /dev/null +++ b/components/spit/src/spit_socket.cpp @@ -0,0 +1,213 @@ +#include "spit_socket.hpp" +#include "Task.hpp" +#include "esp/uuid.h" +#include "esp_log.h" +#include "esp_netif.h" +#include +#include + +using namespace std; + +namespace spit { + +/** + * Creates socket. If there exist some, they will be closed. + * @return + */ +bool Socket::createSocketUdp() { + if (mUdpSocketIp4 != INVALID_SOCKET) { + close(mUdpSocketIp4); + mUdpSocketIp4 = INVALID_SOCKET; + } + + mUdpSocketIp4 = socket(AF_INET, SOCK_DGRAM, 0); + int intResult = -1; + bool success = true; + if (mUdpSocketIp4 == INVALID_SOCKET) { + success = false; + } else { + int intOptionValue4 = 1; + intResult = setsockopt(mUdpSocketIp4, SOL_SOCKET, SO_REUSEADDR, (void *)&intOptionValue4, sizeof(intOptionValue4)); + } + if (intResult < 0) { + success = false; + } + + if (success == true) { + struct timeval tv; + tv.tv_sec = 1; // seconds + tv.tv_usec = 0; // microseconds (0.0 sec) + // This sets the receive timeout + setsockopt(mUdpSocketIp4, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + // set the mUdpSocketIp4 in NON BLOCKING mode because + // if the UDPSockets are in blocking mode the receiveMessage does not return in every constellation + // int intFlags; + // intFlags = fcntl(mUdpSocketIp4, F_GETFL); + // fcntl(mUdpSocketIp4, F_SETFL, intFlags | O_NONBLOCK); + + // int intBufferSize = 2046 * 512; + // intResult = setsockopt(mUdpSocketIp4, SOL_SOCKET, SO_RCVBUF, (void*)&intBufferSize, sizeof(intBufferSize)); + // intResult = setsockopt(mUdpSocketIp4, SOL_SOCKET, SO_SNDBUF, (void*)&intBufferSize, sizeof(intBufferSize)); + } + return success; +} + +/** + * Binds the socket to the client's ip address and port. The socket option SO_REUSEADDR is set to true. + * @param clientIp ip address of the client + * @param clientPort port of the client. + * if p_Port == "0" any free port will be used + * @return -1 id bind fails, o if bind is ok + * + */ +int Socket::bindSocket(string clientIp, string clientPort) { + string stringClientPort; + if (clientPort.length() <= 0) + stringClientPort = "0"; + else + stringClientPort = clientPort; + addrinfo *clientAI; + if (getaddrinfo(clientIp.data(), clientPort.data(), nullptr, &clientAI) != 0) { + return -1; + } + if (mUdpSocketIp4 == INVALID_SOCKET) { + return -1; + } + auto *socketAdr = (sockaddr_in *)(clientAI->ai_addr); + int intSocklen = sizeof(*socketAdr); + // copy the socketAdr, may be not filled correct by getaddrinfo + sockaddr_in o_IP4SocketAddress; + memset(&o_IP4SocketAddress, 0, intSocklen); + o_IP4SocketAddress.sin_addr = socketAdr->sin_addr; + o_IP4SocketAddress.sin_family = socketAdr->sin_family; + o_IP4SocketAddress.sin_port = socketAdr->sin_port; + int intResult = bind(mUdpSocketIp4, (const sockaddr *)(socketAdr), (socklen_t)intSocklen); + if (intResult < 0) { + return intResult; + } + if ((intResult == 0) && + (getsockname(mUdpSocketIp4, (sockaddr *)socketAdr, (socklen_t *)&intSocklen) != -1)) { + setBoundClientPort(to_string((int)socketAdr->sin_port)); + } + freeaddrinfo(clientAI); + + setIsBound(intResult != -1); + return intResult; +} + +/** + * Set whether the socket are broadcast sockets or not. Set the sockopt SO_BROADCAST to 0 or 1 + * @param p_Broadcast true SO_BROADCAST is set + * false = SO_BROADCAST is not set + * @return 0 = ok, -1 = error + */ +int Socket::setBroadcast(bool broadcast) const { + int intOptionValue = broadcast ? 1 : 0; + int intResult = setsockopt(mUdpSocketIp4, SOL_SOCKET, SO_BROADCAST, &intOptionValue, sizeof(int)); + return intResult; +} +/** + * @param ip the ip address of the server + * @param port the port of the server + * @param msg buffer containing the message + * @param msgLength the length of the message + * @return -1 send fails + * 0 send is done + */ +int Socket::sendMessageTo(const string &ip, const string &port, const char *message, int msgLength) { + if (mUdpSocketIp4 == INVALID_SOCKET) { + return -1; + } + + // prepare AddressInfo + int intResult = -1; + addrinfo *ptrAddressInfo; + if (getaddrinfo(ip.data(), port.data(), nullptr, &ptrAddressInfo) != 0) { + return intResult; + } + + auto *ptrIP4SocketAddress = (sockaddr_in *)(ptrAddressInfo->ai_addr); + int intSocklen = sizeof(*ptrIP4SocketAddress); + if (mUdpSocketIp4 != INVALID_SOCKET) { + // copy the ptrIP4SocketAddress, may be not filled correct by getaddrinfo + sockaddr_in o_IP4SocketAddress; + memset(&o_IP4SocketAddress, 0, intSocklen); + o_IP4SocketAddress.sin_addr = ptrIP4SocketAddress->sin_addr; + o_IP4SocketAddress.sin_family = ptrIP4SocketAddress->sin_family; + o_IP4SocketAddress.sin_port = ptrIP4SocketAddress->sin_port; + intResult = sendto(mUdpSocketIp4, message, msgLength, 0, (const sockaddr *)&o_IP4SocketAddress, (socklen_t)intSocklen); + if (intResult < 0) { + ESP_LOGE("SPIT", "sendMessageTo IP4 sendto %s FAILED %s\n", ip.data(), strerror(errno)); + ESP_LOGE("SPIT", "sendMessageTo ret=%d %s / %s / %d\n", intResult, port.data(), message, msgLength); + } + freeaddrinfo(ptrAddressInfo); + } + if (intResult < 0) { + return -1; + } + return 0; +} +/** + * Call this for receiving a message + * This will block until a message is received or the underlying socket is closed + * @param p_Buffer the buffer containing the received message later. + * @param p_BufferLength the size of the buffer + * @param p_ServerIP a pointer to a string, when a messages is received the string will contain the ip address of the sender/server + * if p_ServerIP is NULL - no detection of the server will be done. + * @return + */ +int Socket::receiveMessage(char *p_Buffer, int p_BufferLength, string *serverIp) const { + int intResult = -1; + if (mUdpSocketIp4 == INVALID_SOCKET) { + return intResult; + } + sockaddr_in o_IP4SocketAddress; + int intSocklen = sizeof(o_IP4SocketAddress); + memset(&o_IP4SocketAddress, 0, intSocklen); + intResult = recvfrom(mUdpSocketIp4, (void *)p_Buffer, p_BufferLength, 0, (sockaddr *)&o_IP4SocketAddress, (socklen_t *)&intSocklen); + if (intResult >= 0 && serverIp != nullptr) { + char bufferIP4[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(o_IP4SocketAddress.sin_addr), bufferIP4, intSocklen); + *serverIp = bufferIP4; + } + return intResult; +} + +ISocket::IpAddress Socket::getAdaptersInfo() { + // iterate over active interfaces, and print out IPs + esp_netif_t *netif = nullptr; + if ((netif = esp_netif_next_unsafe(netif)) != nullptr) { + ESP_LOGI("SPIT", "Connected to %s", esp_netif_get_desc(netif)); + esp_netif_ip_info_t ip; + ESP_ERROR_CHECK(esp_netif_get_ip_info(netif, &ip)); + ESP_LOGI("SPIT", "- IPv4 address: " IPSTR ",", IP2STR(&ip.ip)); + ESP_LOGI("SPIT", "- IPv4 netmask: " IPSTR ",", IP2STR(&ip.netmask)); + char buffer[64]; + sprintf(buffer, IPSTR, IP2STR(&ip.ip)); + string stringIP4 = buffer; + sprintf(buffer, IPSTR, IP2STR(&ip.netmask)); + string stringSubnet = buffer; + return {stringIP4, stringSubnet}; + } + return {"0.0.0.0", "0.0.0.0"}; +} + +string createGUID() { + uuid_t uu; + uuid_generate(uu); + char uu_str[UUID_STR_LEN]; + uuid_unparse(uu, uu_str); + return {uu_str}; +} + +string getClientHostName() { + const char *hostname; + esp_netif_get_hostname(esp_netif_get_default_netif(), &hostname); + if (hostname != nullptr) { + return {hostname}; + } + return ""; +} + +} \ No newline at end of file diff --git a/components/spit/test/CMakeLists.txt b/components/spit/test/CMakeLists.txt new file mode 100644 index 0000000..1edbbf9 --- /dev/null +++ b/components/spit/test/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "." + INCLUDE_DIRS "." + REQUIRES cmock spit +) diff --git a/components/spit/test/test_spit_client.cpp b/components/spit/test/test_spit_client.cpp new file mode 100644 index 0000000..254eff8 --- /dev/null +++ b/components/spit/test/test_spit_client.cpp @@ -0,0 +1,123 @@ +/** + * @file test_mean.cpp + * @author Gustice + * @brief Hardware bound unit-Test example device + */ + +#include "spit_client.hpp" +#include "unity.h" +#include +#include +#include + +using namespace spit; +using namespace std; + +queue callbackEvents; + +class ClientMock { + public: + void onServerEvent(SClient::ServerEvent event, const SadDetectedServer &detectedServer) { + } + void onConnectionStatus(string serverID, string serverComputerName, string serverName, int serverStepTime, SClient::Connection connectionStatus, SClient::Connection oldConnectionStatus) { + } + void onProjectUpdate(SClient::ProjUpdate state, string projectId) { + } + void onSpitType(SClient::ObjectEvent event, bool isRemote, const SType &spitType) { + } + void onSpitObject(SClient::ObjectEvent event, bool isRemote, const SType &spitType, const SObject &spitObject) { + } + void onPlay(string playerID, string playerName, char playerType, bool playerIsRunning, uint64_t timeFrames) { + } + void onPlaySpitType(string playerID, string spitTypeID, bool playerIsRunning, bool started, bool ended, double value) { + } + void onPlayRemoteObjectStatus(const SType &spitType, const SObject &spitObject, uint64_t p_FrameInsideObject, int value) { + } + bool onCheckSpitTypeVeto(bool knownType, bool createdByClient) { + return true; + } + bool onCheckSpitObjectVeto(bool knownType) { + return true; + } +}; + +class SocketMock : public ISocket { + public: + SocketMock(IpAddress ip) : ISocket(ip) {} + + int sendMessage(const char *message, int length) override { + return 0; + } + int receiveMessage(char *bu1ffer, int bufferLength, string *serverIp) const override { + return 0; + } + void setConnectionParams(const string &serverIp, const string &serverPort, const string &clientIp, const string &clientPort) override { + } + int bindSocket(string clientIp, string clientPort) override { + return 0; + } + int setBroadcast(bool broadcast) const override { + return 0; + } + int sendMessageTo(const string &ip, const string &port, const char *message, int msgLength) override { + return 0; + } +}; + +// class SpitClientFix { +// protected: +// static ClientMock client; +// ISocket::IpAddress ip{"10.10.0.1", "255.255.255.0"}; +// SocketMock socket{ip}; +// SClient::Callbacks callbacks{ +// .onServerEvent = [&](SClient::ServerEvent event, const SadDetectedServer &detectedServer){client.onServerEvent(event, detectedServer);} , +// .onConnectionStatus = [&](string serverID, string serverComputerName, string serverName, int serverStepTime, SClient::Connection connectionStatus, SClient::Connection oldConnectionStatus){client.onConnectionStatus(serverID, serverComputerName, serverName, serverStepTime, connectionStatus, oldConnectionStatus);} , +// .onProjectUpdate = [&](SClient::ProjUpdate state, string projectId){client.onProjectUpdate(state, projectId);} , +// .onSpitType = [&](SClient::ObjectEvent event, bool isRemote, const SType &spitType){client.onSpitType(event, isRemote, spitType);} , +// .onSpitObject = [&](SClient::ObjectEvent event, bool isRemote, const SType &spitType, const SObject &spitObject){client.onSpitObject(event, isRemote, spitType, spitObject);} , +// .onPlay = [&](string playerID, string playerName, char playerType, bool playerIsRunning, uint64_t timeFrames){client.onPlay(playerID, playerName, playerType, playerIsRunning, timeFrames);} , +// .onPlaySpitType = [&](string playerID, string spitTypeID, bool playerIsRunning, bool started, bool ended, double value){client.onPlaySpitType(playerID, spitTypeID, playerIsRunning, started, ended, value);} , +// .onPlayRemoteObjectStatus = [&](const SType &spitType, const SObject &spitObject, uint64_t p_FrameInsideObject, int value){client.onPlayRemoteObjectStatus( spitType, spitObject, p_FrameInsideObject, value);} , +// .onCheckSpitTypeVeto = [&](bool knownType, bool createdByClient){return client.onCheckSpitTypeVeto(knownType, createdByClient);} , +// .onCheckSpitObjectVeto = [&](bool knownType){return client.onCheckSpitObjectVeto(knownType);} +// }; +// SClient sut{"sut", "sutProj", socket, callbacks}; +// }; + +TEST_CASE("Full Connect precedure", "[spit][init]") { + static ClientMock client; + ISocket::IpAddress ip{"10.10.0.1", "255.255.255.0"}; + SocketMock socket{ip}; + SClient::Callbacks callbacks{ + .onServerEvent = [&](SClient::ServerEvent event, const SadDetectedServer &detectedServer) { client.onServerEvent(event, detectedServer); }, + .onConnectionStatus = [&](string serverID, string serverComputerName, string serverName, int serverStepTime, SClient::Connection connectionStatus, SClient::Connection oldConnectionStatus) { client.onConnectionStatus(serverID, serverComputerName, serverName, serverStepTime, connectionStatus, oldConnectionStatus); }, + .onProjectUpdate = [&](SClient::ProjUpdate state, string projectId) { client.onProjectUpdate(state, projectId); }, + .onSpitType = [&](SClient::ObjectEvent event, bool isRemote, const SType &spitType) { client.onSpitType(event, isRemote, spitType); }, + .onSpitObject = [&](SClient::ObjectEvent event, bool isRemote, const SType &spitType, const SObject &spitObject) { client.onSpitObject(event, isRemote, spitType, spitObject); }, + .onPlay = [&](string playerID, string playerName, char playerType, bool playerIsRunning, uint64_t timeFrames) { client.onPlay(playerID, playerName, playerType, playerIsRunning, timeFrames); }, + .onPlaySpitType = [&](string playerID, string spitTypeID, bool playerIsRunning, bool started, bool ended, double value) { client.onPlaySpitType(playerID, spitTypeID, playerIsRunning, started, ended, value); }, + .onPlayRemoteObjectStatus = [&](const SType &spitType, const SObject &spitObject, uint64_t p_FrameInsideObject, int value) { client.onPlayRemoteObjectStatus(spitType, spitObject, p_FrameInsideObject, value); }, + .onCheckSpitTypeVeto = [&](bool knownType, bool createdByClient) { return client.onCheckSpitTypeVeto(knownType, createdByClient); }, + .onCheckSpitObjectVeto = [&](bool knownType) { return client.onCheckSpitObjectVeto(knownType); }}; + SClient sut{"sut", "sutProj", socket, callbacks}; + + // tood connect - Response - Request project + + // Transfer items liveSHOW -> Device + + { + Message::ReportType t1; + t1.setConfirmed(false); + t1.setID("type1Id#"); + t1.setFixed(true); + t1.setServerRemote("serverId"); + t1.setServerProjectIndepended(true); + t1.setGroupName("sGroup"); + t1.setName("type1Name"); + t1.setDelay(100); + t1.setDuration(200); + + + socket. + } +} \ No newline at end of file diff --git a/components/spit/tools/spit.lua b/components/spit/tools/spit.lua new file mode 100644 index 0000000..15f94d1 --- /dev/null +++ b/components/spit/tools/spit.lua @@ -0,0 +1,361 @@ +-- COPY to: /usr/lib/x86_64-linux-gnu/wireshark/plugins/spit.lua + +-- LiveShowSoftware SPIT-Protocol +spit_proto = Proto("spit", "LiveShowSoftware SPIT") +spit_sad_proto = Proto("spit_sad", "LiveShowSoftware SPIT SAD") + +-- Define fields (add more as needed) +local f_hVer = ProtoField.uint32("spit.version", "Version") +local f_hNum = ProtoField.uint8("spit.msgCount", "MsgCount") +local f_hLen = ProtoField.uint32("spit.len", "Length") +local f_hSerId = ProtoField.string("spit.serverId", "ServerID") +local f_hCliId = ProtoField.string("spit.clientId", "ClientID") +local f_hType = ProtoField.uint8("spit.msgType", "MsgType") +local f_pLen = ProtoField.uint32("spit.paylen", "Payload Length") + +local f_pSession = ProtoField.string("spit.key", "Spit Session") +local f_pKey = ProtoField.string("spit.key", "Spit Key") +local f_pSadKey = ProtoField.string("spit.sadKey", "Spit SAD Key") +local f_pServerDevName = ProtoField.string("spit.serverDevName", "Server Device Name") + +local f_pClientDevName = ProtoField.string("spit.clientDevName", "Client Device Name") +local f_pClientName = ProtoField.string("spit.clientName", "Client Name") +local f_pLoginName = ProtoField.string("spit.loginName", "Login Name") +local f_pLoginPassword = ProtoField.string("spit.loginPassword", "Login Password") +local f_pServerName = ProtoField.string("spit.serverName", "Server Name") +local f_pServerIp = ProtoField.string("spit.serverIp", "Server IP") +local f_pServerPort = ProtoField.uint16("spit.serverIp", "Server Port") +local f_pSrvParam = ProtoField.string("spit.serverParam", "Server Paramter") +local f_pStepTime = ProtoField.string("spit.stepTime", "Step Time") +local f_pConnected = ProtoField.string("spit.connected", "Connected") +local f_pProjectId = ProtoField.string("spit.projectId", "Project ID") +local f_pProjectName = ProtoField.string("spit.projectName", "Project Name") + +local f_pObjectId = ProtoField.string("spit.ObjectId", "Object ID") +local f_pObjectName = ProtoField.string("spit.ObjectName", "Object Name") +local f_pTypeId = ProtoField.string("spit.TypeId", "Type ID") +local f_pTypeName = ProtoField.string("spit.TypeName", "Type Name") +local f_pTypeGroupName = ProtoField.string("spit.TypeGroupName", "Type Group Name") +local f_pObjectsFrames = ProtoField.uint64("spit.ObjectFrames", "Object Frames") +local f_pValue = ProtoField.uint64("spit.Value", "Value") +local f_pValueFlag = ProtoField.uint8("spit.ValueFlag", "ValueFlag") + +local f_pConfirm = ProtoField.uint8("spit.confirmed", "Confirmation") +local f_pFixed = ProtoField.uint8("spit.fixed", "Fixed") +local f_pAssigned = ProtoField.uint8("spit.assigned", "Action assigned") +local f_pSrvRemote = ProtoField.uint8("spit.serverRemote", "Server Remote") +local f_pProjIndep = ProtoField.uint8("spit.projectIndipendent", "Project Indipendent") +local f_pValueUse = ProtoField.uint8("spit.valueUse", "Value Use") + +local f_pFrameStart = ProtoField.uint64("spit.frameStart", "Frame Start") +local f_pFrameLength = ProtoField.uint64("spit.frameLength", "Frame Length") +local f_pFrameFadeIn = ProtoField.uint64("spit.fadeInLength", "Fade In Length") +local f_pFrameFadeOut = ProtoField.uint64("spit.fadeOutLength", "Fade Out Length") +local f_pValueFactor = ProtoField.uint32("spit.valueFactor", "Value Factor") +local f_pFramesPerSec = ProtoField.uint32("spit.framesPerSec", "Frames per second") +local f_pTrack = ProtoField.uint32("spit.track", "Track") +local f_pRemark = ProtoField.string("spit.remark", "Remack") +local f_pDelay = ProtoField.uint64("spit.delay", "Delay") +local f_pDuration = ProtoField.uint64("spit.duration", "Duration") + +local f_pPlayerId = ProtoField.string("spit.PlayerId", "Player ID") +local f_pPlayerName = ProtoField.string("spit.PlayerName", "Player Name") +local f_pPlayerType = ProtoField.uint8("spit.PlayerType", "Player Type") +local f_pPlayerRunning = ProtoField.uint8("spit.PlayerRunning", "Player Running") +local f_pPlayerFrames = ProtoField.uint64("spit.PlayerFrame", "Player Frames") +local f_pPlayerPrePos = ProtoField.uint8("spit.PlayerPrePos", "Player Preposition") + +local sessionInfos = {} + +spit_proto.fields = { + f_pSession, f_pKey, f_pSadKey, f_pServerDevName, + f_hVer, f_hNum, f_hLen, f_hSerId, f_hCliId, f_hType, f_pLen, + f_pClientDevName, f_pClientName, f_pLoginName, f_pLoginPassword, f_pServerName, f_pServerIp, f_pServerPort, f_pSrvParam, f_pStepTime, f_pConnected, f_pProjectId, f_pProjectName, + f_pObjectId, f_pObjectName, f_pTypeId, f_pTypeName, f_pTypeGroupName, f_pObjectsFrames, f_pValue, f_pValueFlag, + f_pConfirm, f_pFixed, f_pAssigned, f_pSrvRemote, f_pProjIndep, f_pValueUse, + f_pFrameStart, f_pFrameLength, f_pFrameFadeIn, f_pFrameFadeOut, f_pValueFactor, f_pFramesPerSec, f_pTrack, f_pRemark, f_pDelay, f_pDuration, + f_pPlayerId, f_pPlayerName, f_pPlayerType, f_pPlayerRunning, f_pPlayerFrames, f_pPlayerPrePos + } + +function extractSession(buffer, pinfo) + local serverHash = buffer(16,41):string() + local clientHash = buffer(57,41):string() + return string.format("%s - %s", sessionInfos[serverHash], sessionInfos[clientHash]) +end + +function extractVersionString(buffer) + local vBytes = buffer(7,4):bytes() + local vStr = "" + for i = 0, 3 do + local byte = vBytes:get_index(i) + local high_nibble = bit.rshift(byte, 4) + local low_nibble = bit.band(byte, 0x0F) + vStr = vStr .. tostring(high_nibble) .. tostring(low_nibble) + end + return vStr + +end + +function addBigEndianValue(tree, proto, buffer) + local val = -1; + -- local val = buffer(99,4):le_uint() + if proto.type == ftypes.UINT32 then + val = buffer:le_uint() + elseif proto.type == ftypes.UINT64 then + val = buffer:le_uint64() + end + tree:add(proto, buffer):set_text(proto.name .. ": " .. val) +end + +-- Heuristic dissector function +function spit_proto.dissector(buffer, pinfo, tree) + if buffer:len() < 6 then return false end + if buffer(0,6):string() ~= "#spit#" then return false end + + pinfo.cols.protocol = "SPIT" + local subtree = tree:add(spit_proto, buffer(), "SPIT Protocol Data") + + local hTree = subtree:add("Header") + hTree:add(f_hVer, buffer(7,4)):set_text("Version: " .. extractVersionString(buffer)) + hTree:add(f_hNum, buffer(11,1)) + local hLen = buffer(12,4):le_uint() + subtree:add(f_hLen, buffer(12,4)):set_text("Length: " .. hLen) + hTree:add(f_hSerId, buffer(16,41)) + hTree:add(f_hCliId, buffer(57,41)) + + local type = buffer(98,1):uint() + hTree:add(f_hType, buffer(98,1)) + + local pTree = subtree:add("Payload") + -- local pLen = buffer(99,4):le_uint() + -- pTree:add(f_pLen, buffer(99,4)):set_text("Pyload Length: " .. pLen) + addBigEndianValue(pTree, f_pLen, buffer(99,4)) + + local info_text = "Type: " .. type + if type == 1 then -- Connect + pTree:add(f_pClientDevName, buffer(103,64)) + pTree:add(f_pClientName, buffer(167,64)) + pTree:add(f_pLoginName, buffer(231,64)) + pTree:add(f_pLoginPassword, buffer(295,64)) + pTree:add(f_pServerIp, buffer(359,40)) + pTree:add(f_pServerPort, buffer(399,2)) + + local clientName = buffer(167,64):string() + local clientHash = buffer(57,41):string() + sessionInfos[clientName] = clientHash -- e.g. 70866c9b-af1c-4765-a17e-8b45c93f3aca + sessionInfos[clientHash] = clientName -- + sessionInfos[tostring(pinfo.src)] = clientName + + info_text = string.format("Connect Req | Client: %s (%s) | ADDR: %s:%s", + clientName, clientHash, buffer(359,40):string(), buffer(399,2):uint()) + + elseif type == 2 then -- Connect Response + pTree:add(f_pServerDevName, buffer(103,64)) + pTree:add(f_pServerName, buffer(167,64)) + pTree:add(f_pStepTime, buffer(231,4)) + pTree:add(f_pConnected, buffer(235,1)) + + local serverName = buffer(167,64):string() + local serverHash = buffer(16,41):string() + sessionInfos[serverName] = serverHash -- e.g. 405c7100-64e8-435d-8a7b-00c5629f9ccf + sessionInfos[serverHash] = serverName + sessionInfos[tostring(pinfo.src)] = serverName + + info_text = string.format("Connect Res | Server: %s (%s) ", + serverName, serverHash) + + elseif type == 3 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + info_text = string.format("Disconnect") + + elseif type == 8 then + info_text = string.format("Watchdog") + + elseif type == 9 then + info_text = string.format("Watchdog Ans") + + elseif type == 11 then -- Report Porject + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pProjectId, buffer(103,41)) + pTree:add(f_pProjectName, buffer(144,64)) + addBigEndianValue(pTree, f_pFramesPerSec, buffer(208,4)) + info_text = string.format("%s -> %s | Report Project | Project: %s / %s", + sessionInfos[tostring(pinfo.src)], sessionInfos[tostring(pinfo.dst)], buffer(103,41):string(), buffer(144,64):string()) + + elseif type == 13 then -- Close Porject + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pProjectId, buffer(103,41)) + info_text = string.format("Close Project | Project: %s", + buffer(103,64):string()) + + elseif type == 15 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + info_text = string.format("Request Project Transfer") + + elseif type == 16 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + info_text = string.format("Start Project Transfer") + + elseif type == 17 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + info_text = string.format("End Project Transfer") + + elseif type == 21 then -- Report Type + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pConfirm, buffer(103,1)) + pTree:add(f_pTypeId, buffer(104,41)) + pTree:add(f_pFixed, buffer(145,1)) + pTree:add(f_pSrvRemote, buffer(146,1)) + pTree:add(f_pProjIndep, buffer(147,1)) + pTree:add(f_pTypeGroupName, buffer(148,39)) + pTree:add(f_pTypeName, buffer(187,64)) + addBigEndianValue(pTree, f_pDelay, buffer(251,8)) + addBigEndianValue(pTree, f_pDuration, buffer(259,8)) + + local group = buffer(148,39):string() + local name = buffer(187,64):string() + local id = buffer(104,41):string() + info_text = string.format("%s -> %s | Report SPIT Type: %s.%s (%s)", sessionInfos[tostring(pinfo.src)], sessionInfos[tostring(pinfo.dst)], group, name, id) + + elseif type == 23 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pTypeId, buffer(103,41)) + info_text = string.format("Remove SPIT Type") + + elseif type == 31 then -- Report Object + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pConfirm, buffer(103,1)) + pTree:add(f_pObjectId, buffer(104,41)) + pTree:add(f_pFixed, buffer(145,1)) + pTree:add(f_pSrvRemote, buffer(146,1)) + pTree:add(f_pObjectName, buffer(147,64)) + pTree:add(f_pTypeId, buffer(211,41)) + pTree:add(f_pSrvParam, buffer(252,64)) + addBigEndianValue(pTree, f_pFrameStart, buffer(316,8)) + addBigEndianValue(pTree, f_pFrameLength, buffer(324,8)) + addBigEndianValue(pTree, f_pFrameFadeIn, buffer(332,8)) + addBigEndianValue(pTree, f_pFrameFadeOut, buffer(340,8)) + addBigEndianValue(pTree, f_pValueFactor, buffer(348,4)) + pTree:add(f_pValueUse, buffer(349,1)) + addBigEndianValue(pTree, f_pTrack, buffer(350,4)) + pTree:add(f_pRemark, buffer(354,64)) + addBigEndianValue(pTree, f_pDelay, buffer(418,8)) + addBigEndianValue(pTree, f_pDuration, buffer(426,8)) + pTree:add(f_pAssigned, buffer(435,1)) + + local name = buffer(147,64):string() + local tId = buffer(211,41):string() + local oId = buffer(104,41):string() + info_text = string.format("%s -> %s | Report SPIT Object: %s (%s / %s)", sessionInfos[tostring(pinfo.src)], sessionInfos[tostring(pinfo.dst)], name, oId, tId) + + elseif type == 33 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pObjectId, buffer(103,41)) + info_text = string.format("Remove SPIT Object") + + elseif type == 37 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pPlayerId, buffer(103,41)) + pTree:add(f_pPlayerName, buffer(144,64)) + pTree:add(f_pPlayerType, buffer(208,1)) + pTree:add(f_pPlayerRunning, buffer(209,1)) + addBigEndianValue(pTree, f_pPlayerFrames, buffer(210,8)) + info_text = string.format("Play SPIT Start") + + elseif type == 38 then -- Play Spit object + pTree:add(f_pSession, extractSession(buffer, pinfo)) + pTree:add(f_pPlayerId, buffer(103,41)) + pTree:add(f_pObjectId, buffer(144,41)) + addBigEndianValue(pTree, f_pObjectsFrames, buffer(185,8)) + pTree:add(f_pPlayerPrePos, buffer(193,1)) + addBigEndianValue(pTree, f_pValue, buffer(194,4)) + pTree:add(f_pValueFlag, buffer(198,1)) + + local oId = buffer(144,41):string() + info_text = string.format("%s -> %s | Play SPIT Object: %s", sessionInfos[tostring(pinfo.src)], sessionInfos[tostring(pinfo.dst)], oId) + + elseif type == 39 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + + pTree:add(f_pPlayerId, buffer(103,41)) + info_text = string.format("Play SPIT End") + + elseif type == 40 then + pTree:add(f_pSession, extractSession(buffer, pinfo)) + pTree:add(f_pObjectId, buffer(103,41)) + addBigEndianValue(pTree, f_pObjectsFrames, buffer(144,8)) + addBigEndianValue(pTree, f_pValue, buffer(152,4)) + info_text = string.format("Report Play Status") + + else + info_text = string.format("ERROR Unknown") + end + + pinfo.cols['info'] = info_text + return true -- We handled this packet +end + +function split(str, delim) + local result = {} + local delim_len = delim + local s = 1 + while true do + local e, next_e = string.find(str, delim, s) + if not e then + table.insert(result, string.sub(str, s)) + break + end + table.insert(result, string.sub(str, s, e - 1)) + s = next_e + 1 + end + return result +end + +-- Heuristic dissector function +function spit_sad_proto.dissector(buffer, pinfo, tree) + if buffer:len() < 14 then return false end + if buffer(0,14):string() ~= "#SAD##,##spit#" then return false end + + pinfo.cols.protocol = "SPIT-SAD" + + local info_text = "SAD" + if buffer:len() > 16 then + local payload = buffer(0,buffer:len()):string() + local parts = split(payload, "#,#") + + local subtree = tree:add(spit_sad_proto, buffer(), "Discovery Data") + subtree:add(f_pSadKey, parts[1]) -- 1 #SAD# + subtree:add(f_pKey, parts[2]) -- 2 #spit# + subtree:add(f_pServerName, parts[3]) -- 3 liveShowSPIT + subtree:add(f_hSerId , parts[4]) -- 4 405c7100-64e8-435d-8a7b-00c5629f9ccf + subtree:add(f_pServerDevName, parts[5]) -- 5 P17gen1 + subtree:add(f_pServerPort, parts[6]) -- 6 15120 + subtree:add(f_pServerIp, parts[7]) -- 7 172.16.17.56 + subtree:add(f_pProjectName, parts[9]) -- 9 CatasWatchProject + + info_text = string.format("SPIT-SAD Discover Data | Client: %s Porject: %s ", + parts[4], parts[9]) + else + info_text = string.format("SPIT-SAD Discover") + end + + pinfo.cols['info'] = info_text + return true -- We handled this packet +end + +-- Register as a heuristic UDP dissector +spit_proto:register_heuristic("udp", spit_proto.dissector) + +spit_sad_proto:register_heuristic("udp", spit_sad_proto.dissector) + diff --git a/components/spit/uTest/CMakeLists.txt b/components/spit/uTest/CMakeLists.txt new file mode 100644 index 0000000..3c359e9 --- /dev/null +++ b/components/spit/uTest/CMakeLists.txt @@ -0,0 +1,5 @@ +UnitTestsFrame() +target_link_libraries(${moduleName} PUBLIC + fmt + hwMock +) diff --git a/components/spit/uTest/messageTest.cpp b/components/spit/uTest/messageTest.cpp new file mode 100644 index 0000000..d371d76 --- /dev/null +++ b/components/spit/uTest/messageTest.cpp @@ -0,0 +1,320 @@ +/** + * @file TestableUTest.cpp + * @author Gustice + * @brief + */ + +#include "doctest/doctest.h" +#include +#include + +#include "spit_message.hpp" + +using namespace spit; + +class SpitMessageFix { + protected: +}; + +void checkData(std::vector a, std::vector b) { + CHECK(a.size() == b.size()); + for (size_t i = 0; i < b.size(); ++i) { + CHECK_MESSAGE(a[i] == b[i], + fmt::format("Mismatch at index, {} : a = 0x{:02x} , b = 0x{:02x}", + i, (int)a[i], (int)b[i])); + } +} + +TEST_CASE_FIXTURE(SpitMessageFix, "Connect Request") { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0020*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0030*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x62, 0x33, 0x63, 0x66, 0x32, 0x34, // .........db3cf24 + /*0040*/ 0x34, 0x2d, 0x36, 0x30, 0x35, 0x39, 0x2d, 0x34, 0x30, 0x65, 0x36, 0x2d, 0x61, 0x33, 0x35, 0x35, // 4-6059-40e6-a355 + /*0050*/ 0x2d, 0x32, 0x62, 0x66, 0x34, 0x34, 0x36, 0x37, 0x33, 0x32, 0x62, 0x30, 0x36, 0x00, 0x00, 0x00, // -2bf446732b06... + /*0060*/ 0x00, 0x00, 0x01, 0x30, 0x01, 0x00, 0x00, 0x64, 0x65, 0x76, 0x70, 0x69, 0x00, 0x00, 0x00, 0x00, // ...0...devpi.... + /*0070*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0080*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0090*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x71, 0x74, 0x74, 0x53, 0x70, 0x69, 0x74, 0x42, // .......MqttSpitB + /*00b0*/ 0x72, 0x69, 0x64, 0x67, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ridge........... + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x37, 0x32, 0x2e, 0x31, 0x36, 0x2e, 0x31, 0x36, // .......172.16.16 + /*0170*/ 0x2e, 0x31, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .10............. + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // ................ + /*0190*/ 0x3b, 0x00, 0x00 // ;.. + }; + std::vector msgC(msg.begin(), msg.end()); + + { + Message sut = Message(msgC); + CHECK_EQ(sut.getType(), Message::Connect::OPFLAG); + auto &pM = sut.getAs(); + + CHECK_EQ(pM.getClientComputerName(), "devpi"); + CHECK_EQ(pM.getClientName(), "MqttSpitBridge"); + CHECK_EQ(pM.getLoginName(), ""); + CHECK_EQ(pM.getLoginPassword(), ""); + CHECK_EQ(pM.getServerIP(), "172.16.16.10"); + CHECK_EQ(pM.getServerPort(), 15120); + } + + { + Message::Connect sut("devpi", "MqttSpitBridge"); + sut.setLoginName(""); + sut.setLoginPassword(""); + sut.setServerIP("172.16.16.10"); + sut.setServerPort(15120); + + sut.setMessageNumber(2); + sut.setServerInfoInHeader("db3cf244-6059-40e6-a355-2bf446732b06", ""); + auto data = Message::createMessage(sut); + + CHECK(data.size() == msg.size()); + for (size_t i = 0; i < msg.size(); ++i) { + CHECK_MESSAGE(data[i] == msgC[i], + fmt::format("Mismatch at index, {} : a = 0x{:02x} , b = 0x{:02x}", + i, (int)data[i], (int)msg[i])); + } + } +} + +TEST_CASE_FIXTURE(SpitMessageFix, "Connect Response") { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x62, 0x33, 0x63, 0x66, 0x32, 0x34, // 9ccf.....db3cf24 + /*0040*/ 0x34, 0x2d, 0x36, 0x30, 0x35, 0x39, 0x2d, 0x34, 0x30, 0x65, 0x36, 0x2d, 0x61, 0x33, 0x35, 0x35, // 4-6059-40e6-a355 + /*0050*/ 0x2d, 0x32, 0x62, 0x66, 0x34, 0x34, 0x36, 0x37, 0x33, 0x32, 0x62, 0x30, 0x36, 0x00, 0x00, 0x00, // -2bf446732b06... + /*0060*/ 0x00, 0x00, 0x02, 0x89, 0x00, 0x00, 0x00, 0x50, 0x31, 0x37, 0x67, 0x65, 0x6e, 0x31, 0x00, 0x00, // .......P17gen1.. + /*0070*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0080*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0090*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6c, 0x69, 0x76, 0x65, 0x53, 0x48, 0x4f, 0x57, 0x00, // .......liveSHOW. + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x01 // .......(.... + }; + std::vector msgC(msg.begin(), msg.end()); + + { + Message sut = Message(msgC); + CHECK_EQ(sut.getType(), Message::ConnectAnswer::OPFLAG); + auto &pM = sut.getAs(); + + CHECK_EQ(pM.getServerComputerName(), "P17gen1"); + CHECK_EQ(pM.getServerName(), "liveSHOW"); + CHECK_EQ(pM.getConnected(), true); + CHECK_EQ(pM.getStepTime(), 40); + } + + { + Message::ConnectAnswer sut; + sut.setServerComputerName("P17gen1"); + sut.setServerName("liveSHOW"); + sut.setConnected(true); + sut.setStepTime(40); + + sut.setMessageNumber(1); + sut.setServerInfoInHeader("db3cf244-6059-40e6-a355-2bf446732b06", "405c7100-64e8-435d-8a7b-00c5629f9ccf"); + auto data = Message::createMessage(sut); + checkData(data, msgC); + } +} + + +TEST_CASE_FIXTURE(SpitMessageFix, "Report Spit Type") { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x62, 0x33, 0x63, 0x66, 0x32, 0x34, // 9ccf.....db3cf24 + /*0040*/ 0x34, 0x2d, 0x36, 0x30, 0x35, 0x39, 0x2d, 0x34, 0x30, 0x65, 0x36, 0x2d, 0x61, 0x33, 0x35, 0x35, // 4-6059-40e6-a355 + /*0050*/ 0x2d, 0x32, 0x62, 0x66, 0x34, 0x34, 0x36, 0x37, 0x33, 0x32, 0x62, 0x30, 0x36, 0x00, 0x00, 0x00, // -2bf446732b06... + /*0060*/ 0x00, 0x00, 0x15, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, // ........ActionMo + /*0070*/ 0x76, 0x65, 0x54, 0x6f, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // veTo1........... + /*0080*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0090*/ 0x00, 0x01, 0x00, 0x01, 0x43, 0x61, 0x74, 0x61, 0x73, 0x57, 0x61, 0x74, 0x63, 0x68, 0x4d, 0x6f, // ....CatasWatchMo + /*00a0*/ 0x76, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // vement.......... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x63, 0x74, 0x69, 0x6f, // ...........Actio + /*00c0*/ 0x6e, 0x4d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // nMoveTo1........ + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ........... + }; + std::vector msgC(msg.begin(), msg.end()); + + { + Message sut = Message(msgC); + CHECK_EQ(sut.getType(), Message::ReportType::OPFLAG); + auto &pM = sut.getAs(); + + CHECK_EQ(pM.getConfirmed(), true); + CHECK_EQ(pM.getID(), "ActionMoveTo1"); + CHECK_EQ(pM.getFixed(), true); + CHECK_EQ(pM.getServerRemote(), false); + CHECK_EQ(pM.getServerProjectIndepended(), true); + CHECK_EQ(pM.getGroupName(), "CatasWatchMovement"); + CHECK_EQ(pM.getName(), "ActionMoveTo1"); + CHECK_EQ(pM.getDelay(), 0); + CHECK_EQ(pM.getDuration(), 0); + } + + { + Message::ReportType sut; + sut.setConfirmed(true); + sut.setID("ActionMoveTo1"); + sut.setFixed(true); + sut.setServerRemote(false); + sut.setServerProjectIndepended(true); + sut.setGroupName("CatasWatchMovement"); + sut.setName("ActionMoveTo1"); + sut.setDelay(0); + sut.setDuration(0); + + sut.setMessageNumber(6); + sut.setServerInfoInHeader("db3cf244-6059-40e6-a355-2bf446732b06", "405c7100-64e8-435d-8a7b-00c5629f9ccf"); + auto data = Message::createMessage(sut); + + checkData(data, msgC); + } +} + +TEST_CASE_FIXTURE(SpitMessageFix, "Report Spit Object") { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x62, 0x33, 0x63, 0x66, 0x32, 0x34, // 9ccf.....db3cf24 + /*0040*/ 0x34, 0x2d, 0x36, 0x30, 0x35, 0x39, 0x2d, 0x34, 0x30, 0x65, 0x36, 0x2d, 0x61, 0x33, 0x35, 0x35, // 4-6059-40e6-a355 + /*0050*/ 0x2d, 0x32, 0x62, 0x66, 0x34, 0x34, 0x36, 0x37, 0x33, 0x32, 0x62, 0x30, 0x36, 0x00, 0x00, 0x00, // -2bf446732b06... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x68, 0x63, // ...S....00000mhc + /*0070*/ 0x67, 0x65, 0x64, 0x30, 0x69, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // ged0i00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x7a, 0x34, 0x30, 0x74, 0x63, 0x79, 0x6b, 0x63, 0x33, 0x00, 0x00, // 00000z40tcykc3.. + /*0090*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x6f, 0x76, 0x65, 0x54, 0x6f, 0x31, // ...ActionMoveTo1 + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x70, 0x00, // ............vip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d, 0xfc, 0x12, 0x00, // ............m... + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x4b, 0xd0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ....K........... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ...... + }; + std::vector msgC(msg.begin(), msg.end()); + + { + Message sut = Message(msgC); + CHECK_EQ(sut.getType(), Message::ReportObject::OPFLAG); + auto &pM = sut.getAs(); + + CHECK_EQ(pM.getConfirmed(), true); + CHECK_EQ(pM.getID(), "00000mhcged0i0000000000000000z40tcykc3"); + CHECK_EQ(pM.getFixed(), false); + CHECK_EQ(pM.getServerRemote(), false); + CHECK_EQ(pM.getName(), ""); + CHECK_EQ(pM.getSPITTypeID(), "ActionMoveTo1"); + CHECK_EQ(pM.getServerParams(), "vip"); + CHECK_EQ(pM.getFrameStart(), 1244269); + CHECK_EQ(pM.getFrameLength(), 53323); + CHECK_EQ(pM.getFrameFadeInLength(), 0); + CHECK_EQ(pM.getFrameFadeOutLength(), 0); + CHECK_EQ(pM.getValueFactor(), 1000000); + CHECK_EQ(pM.getValueUse(), 0); + CHECK_EQ(pM.getTrack(), 1); + CHECK_EQ(pM.getRemark(), ""); + CHECK_EQ(pM.getDelay(), 0); + CHECK_EQ(pM.getDuration(), 0); + CHECK_EQ(pM.getActionAssigned(), false); + } + + { + Message::ReportObject sut; + sut.setConfirmed(true); + sut.setID("00000mhcged0i0000000000000000z40tcykc3"); + sut.setFixed(false); + sut.setServerRemote(false); + sut.setName(""); + sut.setSPITTypeID("ActionMoveTo1"); + sut.setServerParams("vip"); + sut.setFrameStart(1244269); + sut.setFrameLength(53323); + sut.setFrameFadeInLength(0); + sut.setFrameFadeOutLength(0); + sut.setValueFactor(1000000); + sut.setValueUse(0); + sut.setTrack(1); + sut.setRemark(""); + sut.setDelay(0); + sut.setDuration(0); + sut.setActionAssigned(false); + + sut.setMessageNumber(21); + sut.setServerInfoInHeader("db3cf244-6059-40e6-a355-2bf446732b06", "405c7100-64e8-435d-8a7b-00c5629f9ccf"); + auto data = Message::createMessage(sut); + + checkData(data, msgC); + } +} + +TEST_CASE_FIXTURE(SpitMessageFix, "Report Play Object") { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x83, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x62, 0x33, 0x63, 0x66, 0x32, 0x34, // 9ccf.....db3cf24 + /*0040*/ 0x34, 0x2d, 0x36, 0x30, 0x35, 0x39, 0x2d, 0x34, 0x30, 0x65, 0x36, 0x2d, 0x61, 0x33, 0x35, 0x35, // 4-6059-40e6-a355 + /*0050*/ 0x2d, 0x32, 0x62, 0x66, 0x34, 0x34, 0x36, 0x37, 0x33, 0x32, 0x62, 0x30, 0x36, 0x00, 0x00, 0x00, // -2bf446732b06... + /*0060*/ 0x00, 0x00, 0x28, 0x39, 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, // ..(9...00000m8ue + /*0070*/ 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // dyyt000000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x31, 0x66, 0x6a, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, // 0001fj0va8bom... + /*0090*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ............ + }; + std::vector msgC(msg.begin(), msg.end()); + + { + Message sut = Message(msgC); + CHECK_EQ(sut.getType(), Message::ReportPlayStatus::OPFLAG); + auto &pM = sut.getAs(); + + CHECK_EQ(pM.getID(), "00000m8uedyyt0000000000000001fj0va8bom"); + CHECK_EQ(pM.getFrameInsideObject(), 0); + CHECK_EQ(pM.getValue(), 0); + } + + { + Message::ReportPlayStatus sut; + sut.setID("00000m8uedyyt0000000000000001fj0va8bom"); + sut.setFrameInsideObject(0); + sut.setValue(0); + + sut.setMessageNumber(131); + sut.setServerInfoInHeader("db3cf244-6059-40e6-a355-2bf446732b06", "405c7100-64e8-435d-8a7b-00c5629f9ccf"); + auto data = Message::createMessage(sut); + + checkData(data, msgC); + } +} diff --git a/components/spit/uTest/spitObjectTest.cpp b/components/spit/uTest/spitObjectTest.cpp new file mode 100644 index 0000000..137cfd1 --- /dev/null +++ b/components/spit/uTest/spitObjectTest.cpp @@ -0,0 +1,92 @@ +/** + * @file spitRepoTest.cpp + * @author Gustice + * @brief + */ + +#include "doctest/doctest.h" +#include +#include + +#include "spit_object.hpp" + +using namespace spit; +using namespace std; + +class SpitObjectFix { + protected: +}; + +TEST_CASE_FIXTURE(SpitObjectFix, "Typical use case") { + { + SObject sut("#Id#123"); + CHECK(sut.getID() == "#Id#123"); + + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Name.set("G123") == true); + CHECK(sut.Name.get() == "G123"); + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Name.set("G123") == false); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Name.set("G321") == true); + CHECK(sut.Name.get() == "G321"); + CHECK(sut.getConfirmed() == false); + } + { + SObject sut("#Id#123"); + CHECK(sut.getID() == "#Id#123"); + + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Fixed.set(true) == true); + CHECK(sut.Fixed.get() == true); + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Fixed.set(true) == false); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Fixed.set(false) == true); + CHECK(sut.Fixed.get() == false); + CHECK(sut.getConfirmed() == false); + } + { + SObject sut("#Id#123"); + CHECK(sut.getID() == "#Id#123"); + + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.FrameLength.set(123) == true); + CHECK(sut.FrameLength.get() == 123); + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.FrameLength.set(123) == false); + CHECK(sut.getConfirmed() == true); + CHECK(sut.FrameLength.set(321) == true); + CHECK(sut.FrameLength.get() == 321); + CHECK(sut.getConfirmed() == false); + } + // ... + + // { + // SObject sut("#Id#123", "grp123", "name123", true, false, 1234, 4321); + // CHECK(sut.getID() == "#Id#123"); + // CHECK(sut.GroupName.get() == "grp123"); + // CHECK(sut.Name.get() == "name123"); + // CHECK(sut.Fixed.get() == true); + // CHECK(sut.ProjectIndepended.get() == false); + + // CHECK(sut.DefaultDelay.get() == 1234); + // CHECK(sut.DefaultDuration.get() == 4321); + // } +} + + + + diff --git a/components/spit/uTest/spitProcessorTest.cpp b/components/spit/uTest/spitProcessorTest.cpp new file mode 100644 index 0000000..f3be48e --- /dev/null +++ b/components/spit/uTest/spitProcessorTest.cpp @@ -0,0 +1,350 @@ +/** + * @file spitRepoTest.cpp + * @author Gustice + * @brief + */ + +#include "doctest/doctest.h" +#include +#include +#include + +#include "spit_processor.hpp" + +using namespace spit; +using namespace std; +static queue cbEvents; + +class ClientMock { + public: + void onServerEvent(SProcessor::ServerEvent event, const SadDetectedServer &detectedServer) { + } + void onConnectionStatus(string serverID, string serverComputerName, string serverName, int serverStepTime, SProcessor::Connection connectionStatus, SProcessor::Connection oldConnectionStatus) { + } + void onProjectUpdate(SProcessor::ProjUpdate state, string projectId) { + } + void onSpitType(SProcessor::ObjectEvent event, bool isRemote, const SType &sType) { + cbEvents.push(fmt::format("{}: {} {} {} {}", (int)event, isRemote, sType.getID(), sType.Name.get(), sType.GroupName.get())); + } + void onSpitObject(SProcessor::ObjectEvent event, bool isRemote, const SType &sType, const SObject &sObject) { + cbEvents.push(fmt::format("{}: {} {} {} {} {}", (int)event, isRemote, sType.getID(), sType.Name.get(), sObject.Name.get(), sObject.getID())); + } + void onPlay(string playerID, string playerName, char playerType, bool playerIsRunning, uint64_t timeFrames) { + } + void onPlaySpitType(string playerID, string spitTypeID, bool playerIsRunning, bool started, bool ended, double value) { + } + void onPlayRemoteObjectStatus(const SType &sType, const SObject &sObject, uint64_t p_FrameInsideObject, int value) { + } + bool onCheckSpitTypeVeto(bool knownType, bool createdByClient) { + cbEvents.push(fmt::format("veto check {}/{}", knownType, createdByClient)); + return true; + } + bool onCheckSpitObjectVeto(bool knownType) { + return true; + } +}; + +class SpitClientFix { + protected: + std::vector messages; + + ClientMock client; + SProcessor::Callbacks callbacks{ + .onServerEvent = [&](SProcessor::ServerEvent event, const SadDetectedServer &detectedServer) { client.onServerEvent(event, detectedServer); }, + .onConnectionStatus = [&](string serverID, string serverComputerName, string serverName, int serverStepTime, SProcessor::Connection connectionStatus, SProcessor::Connection oldConnectionStatus) { client.onConnectionStatus(serverID, serverComputerName, serverName, serverStepTime, connectionStatus, oldConnectionStatus); }, + .onProjectUpdate = [&](SProcessor::ProjUpdate state, string projectId) { client.onProjectUpdate(state, projectId); }, + .onSpitType = [&](SProcessor::ObjectEvent event, bool isRemote, const SType &t) { client.onSpitType(event, isRemote, t); }, + .onSpitObject = [&](SProcessor::ObjectEvent event, bool isRemote, const SType &t, const SObject &o) { client.onSpitObject(event, isRemote, t, o); }, + .onPlay = [&](string playerID, string playerName, char playerType, bool playerIsRunning, uint64_t timeFrames) { client.onPlay(playerID, playerName, playerType, playerIsRunning, timeFrames); }, + .onPlaySpitType = [&](string playerID, string spitTypeID, bool playerIsRunning, bool started, bool ended, double value) { client.onPlaySpitType(playerID, spitTypeID, playerIsRunning, started, ended, value); }, + .onPlayRemoteObjectStatus = [&](const SType &t, const SObject &o, uint64_t p_FrameInsideObject, int value) { client.onPlayRemoteObjectStatus(t, o, p_FrameInsideObject, value); }, + .onCheckSpitTypeVeto = [&](bool knownType, bool createdByClient) { return client.onCheckSpitTypeVeto(knownType, createdByClient); }, + .onCheckSpitObjectVeto = [&](bool knownType) { return client.onCheckSpitObjectVeto(knownType); }}; + SProcessor::InternalCallbacks intCallbacks{ + .delay = [&](int ms) { return; }, + .sendMessage = [&](Message::OPPart &msg) { return 0; }, + .parseMessage = [&](Message &msg) { + // auto o = fmt::format("msg-{}", msg.getHeader()->getMessageNumber()); + // messages.push_back(o); + }, + }; + SProcessor sut{"sut", "sutProj", "dev", "##GUID##", callbacks, intCallbacks}; +}; + +TEST_CASE_FIXTURE(SpitClientFix, "Spit types") { + { + Message::ReportType t; + t.setConfirmed(false); + t.setID("type1Id#Rem"); + t.setFixed(true); + t.setServerRemote(true); + t.setServerProjectIndepended(true); + t.setGroupName("sGroup"); + t.setName("type1Name"); + t.setDelay(100); + t.setDuration(200); + sut.checkParseMessageData(sut.createMessage(t)); + } + CHECK_MESSAGE(cbEvents.front() == "0: true type1Id#Rem type1Name sGroup", cbEvents.front()); + cbEvents.pop(); + + { + Message::ReportType t; + t.setConfirmed(false); + t.setID("type2Id#Rem"); + t.setFixed(true); + t.setServerRemote(true); + t.setServerProjectIndepended(true); + t.setGroupName("sGroup"); + t.setName("type2Name"); + t.setDelay(100); + t.setDuration(200); + sut.checkParseMessageData(sut.createMessage(t)); + } + CHECK_MESSAGE(cbEvents.front() == "0: true type2Id#Rem type2Name sGroup", cbEvents.front()); + cbEvents.pop(); + + { + Message::ReportType t; + t.setConfirmed(false); + t.setID("type1Id#Rem"); + t.setFixed(true); + t.setServerRemote(true); + t.setServerProjectIndepended(true); + t.setGroupName("sGroup"); + t.setName("type1Name"); + t.setDelay(200); + t.setDuration(300); + sut.checkParseMessageData(sut.createMessage(t)); + } + CHECK_MESSAGE(cbEvents.front() == "1: true type1Id#Rem type1Name sGroup", cbEvents.front()); + cbEvents.pop(); + + CHECK(cbEvents.empty()); + + // Local Objects + + { + Message::ReportType t; + t.setConfirmed(false); + t.setID("type1Id#Loc"); + t.setFixed(true); + t.setServerRemote(false); + t.setServerProjectIndepended(true); + t.setGroupName("sGroup"); + t.setName("type1Name"); + t.setDelay(100); + t.setDuration(200); + sut.checkParseMessageData(sut.createMessage(t)); + } + CHECK_MESSAGE(cbEvents.front() == "veto check false/false", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: false type1Id#Loc type1Name sGroup", cbEvents.front()); + cbEvents.pop(); + + { + Message::ReportType t; + t.setConfirmed(false); + t.setID("type2Id#Loc"); + t.setFixed(true); + t.setServerRemote(false); + t.setServerProjectIndepended(true); + t.setGroupName("sGroup"); + t.setName("type2Name"); + t.setDelay(100); + t.setDuration(200); + sut.checkParseMessageData(sut.createMessage(t)); + } + CHECK_MESSAGE(cbEvents.front() == "veto check false/false", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: false type2Id#Loc type2Name sGroup", cbEvents.front()); + cbEvents.pop(); + + { + Message::ReportType t; + t.setConfirmed(false); + t.setID("type1Id#Loc"); + t.setFixed(true); + t.setServerRemote(false); + t.setServerProjectIndepended(true); + t.setGroupName("sGroup"); + t.setName("type1Name"); + t.setDelay(200); + t.setDuration(300); + sut.checkParseMessageData(sut.createMessage(t)); + } + CHECK_MESSAGE(cbEvents.front() == "1: false type1Id#Loc type1Name sGroup", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); +} + +TEST_CASE_FIXTURE(SpitClientFix, "Message Counter") { + Message::ReportType t1; + t1.setID("type#1"); + t1.setServerRemote(true); + auto m1 = sut.createMessage(t1); + + Message::ReportType t2; + t2.setID("type#2"); + t2.setServerRemote(true); + auto m2 = sut.createMessage(t2); + + Message::ReportType t3; + t3.setID("type#3"); + t3.setServerRemote(true); + auto m3 = sut.createMessage(t3); + + Message::ReportType t4; + t4.setID("type#4"); + t4.setServerRemote(true); + auto m4 = sut.createMessage(t4); + + Message::ReportType t5; + t5.setID("type#5"); + t5.setServerRemote(true); + auto m5 = sut.createMessage(t5); + + Message::ReportType t6; + t6.setID("type#6"); + t6.setServerRemote(true); + auto m6 = sut.createMessage(t6); + + CHECK(cbEvents.empty()); + SUBCASE("Normal order") { + REQUIRE(cbEvents.size() == 0); + sut.checkParseMessageData(m1); + CHECK_MESSAGE(cbEvents.front() == "0: true type#1 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m2); + CHECK_MESSAGE(cbEvents.front() == "0: true type#2 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m3); + CHECK_MESSAGE(cbEvents.front() == "0: true type#3 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m4); + CHECK_MESSAGE(cbEvents.front() == "0: true type#4 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + } + + CHECK(cbEvents.empty()); + SUBCASE("overtakes one") { + REQUIRE(cbEvents.size() == 0); + sut.checkParseMessageData(m1); + CHECK_MESSAGE(cbEvents.front() == "0: true type#1 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m3); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m2); + CHECK_MESSAGE(cbEvents.front() == "0: true type#2 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#3 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m4); + CHECK_MESSAGE(cbEvents.front() == "0: true type#4 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + } + + CHECK(cbEvents.empty()); + SUBCASE("overtakes two") { + REQUIRE(cbEvents.size() == 0); + sut.checkParseMessageData(m1); + CHECK_MESSAGE(cbEvents.front() == "0: true type#1 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m4); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m2); + CHECK_MESSAGE(cbEvents.front() == "0: true type#2 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m3); + CHECK_MESSAGE(cbEvents.front() == "0: true type#3 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#4 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m5); + CHECK_MESSAGE(cbEvents.front() == "0: true type#5 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + } + + CHECK(cbEvents.empty()); + SUBCASE("many overtakes") { + REQUIRE(cbEvents.size() == 0); + sut.checkParseMessageData(m1); + CHECK_MESSAGE(cbEvents.front() == "0: true type#1 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m5); + CHECK(cbEvents.empty()); + sut.checkParseMessageData(m6); + CHECK(cbEvents.empty()); + sut.checkParseMessageData(m4); + CHECK(cbEvents.empty()); + sut.checkParseMessageData(m3); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m2); + CHECK_MESSAGE(cbEvents.front() == "0: true type#2 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#3 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#4 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#5 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#6 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + } + + CHECK(cbEvents.empty()); + SUBCASE("reverse order") { + REQUIRE(cbEvents.size() == 0); + sut.checkParseMessageData(m1); + CHECK_MESSAGE(cbEvents.front() == "0: true type#1 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m6); + CHECK(cbEvents.empty()); + sut.checkParseMessageData(m5); + CHECK(cbEvents.empty()); + sut.checkParseMessageData(m4); + CHECK(cbEvents.empty()); + sut.checkParseMessageData(m3); + CHECK(cbEvents.empty()); + + sut.checkParseMessageData(m2); + CHECK_MESSAGE(cbEvents.front() == "0: true type#2 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#3 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#4 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#5 ", cbEvents.front()); + cbEvents.pop(); + CHECK_MESSAGE(cbEvents.front() == "0: true type#6 ", cbEvents.front()); + cbEvents.pop(); + CHECK(cbEvents.empty()); + } +} \ No newline at end of file diff --git a/components/spit/uTest/spitProcessor_complexTest.cpp b/components/spit/uTest/spitProcessor_complexTest.cpp new file mode 100644 index 0000000..0a39a93 --- /dev/null +++ b/components/spit/uTest/spitProcessor_complexTest.cpp @@ -0,0 +1,516 @@ +/** + * @file spitRepoTest.cpp + * @author Gustice + * @brief + */ + +#include "doctest/doctest.h" +#include +#include +#include + +#include "spit_processor.hpp" + +using namespace spit; +using namespace std; +static queue cbEvents; + +class ClientMockCmplx { + public: + void onServerEvent(SProcessor::ServerEvent event, const SadDetectedServer &detectedServer) { + } + void onConnectionStatus(string serverID, string serverComputerName, string serverName, int serverStepTime, SProcessor::Connection connectionStatus, SProcessor::Connection oldConnectionStatus) { + } + void onProjectUpdate(SProcessor::ProjUpdate state, string projectId) { + } + void onSpitType(SProcessor::ObjectEvent event, bool isRemote, const SType &sType) { + cbEvents.push(fmt::format("{}: {} {} {} {}", (int)event, isRemote, sType.getID(), sType.Name.get(), sType.GroupName.get())); + } + void onSpitObject(SProcessor::ObjectEvent event, bool isRemote, const SType &sType, const SObject &sObject) { + cbEvents.push(fmt::format("{}: {} {} {} {} {}", (int)event, isRemote, sType.getID(), sType.Name.get(), sObject.Name.get(), sObject.getID())); + } + void onPlay(string playerID, string playerName, char playerType, bool playerIsRunning, uint64_t timeFrames) { + } + void onPlaySpitType(string playerID, string spitTypeID, bool playerIsRunning, bool started, bool ended, double value) { + } + void onPlayRemoteObjectStatus(const SType &sType, const SObject &sObject, uint64_t p_FrameInsideObject, int value) { + } + bool onCheckSpitTypeVeto(bool knownType, bool createdByClient) { + cbEvents.push(fmt::format("veto check {}/{}", knownType, createdByClient)); + return true; + } + bool onCheckSpitObjectVeto(bool knownType) { + return true; + } +}; + +class SpitClientCmplsFix { + protected: + ClientMockCmplx client; + SProcessor::Callbacks callbacks{ + .onServerEvent = [&](SProcessor::ServerEvent event, const SadDetectedServer &detectedServer) { client.onServerEvent(event, detectedServer); }, + .onConnectionStatus = [&](string serverID, string serverComputerName, string serverName, int serverStepTime, SProcessor::Connection connectionStatus, SProcessor::Connection oldConnectionStatus) { client.onConnectionStatus(serverID, serverComputerName, serverName, serverStepTime, connectionStatus, oldConnectionStatus); }, + .onProjectUpdate = [&](SProcessor::ProjUpdate state, string projectId) { client.onProjectUpdate(state, projectId); }, + .onSpitType = [&](SProcessor::ObjectEvent event, bool isRemote, const SType &t) { client.onSpitType(event, isRemote, t); }, + .onSpitObject = [&](SProcessor::ObjectEvent event, bool isRemote, const SType &t, const SObject &o) { client.onSpitObject(event, isRemote, t, o); }, + .onPlay = [&](string playerID, string playerName, char playerType, bool playerIsRunning, uint64_t timeFrames) { client.onPlay(playerID, playerName, playerType, playerIsRunning, timeFrames); }, + .onPlaySpitType = [&](string playerID, string spitTypeID, bool playerIsRunning, bool started, bool ended, double value) { client.onPlaySpitType(playerID, spitTypeID, playerIsRunning, started, ended, value); }, + .onPlayRemoteObjectStatus = [&](const SType &t, const SObject &o, uint64_t p_FrameInsideObject, int value) { client.onPlayRemoteObjectStatus(t, o, p_FrameInsideObject, value); }, + .onCheckSpitTypeVeto = [&](bool knownType, bool createdByClient) { return client.onCheckSpitTypeVeto(knownType, createdByClient); }, + .onCheckSpitObjectVeto = [&](bool knownType) { return client.onCheckSpitObjectVeto(knownType); }}; + SProcessor::InternalCallbacks intCallbacks { + .delay = [&](int ms){ return ; }, + .sendMessage = [&](Message::OPPart &msg){ return 0; }, + .parseMessage = [&](Message &msg) { }, + }; + SProcessor sut{"sut", "sutProj", "dev", "##GUID##", callbacks, intCallbacks}; +}; + +static void spoofMsgCounter(std::vector &msg, uint8_t cnt) { + msg.at(11) = cnt; +} + +TEST_CASE_FIXTURE(SpitClientCmplsFix, "Spit types Raw") { + int cnt = 1; + { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x41, 0x63, 0x00, 0x00, 0x00, // #spit#.....Ac... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x15, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ........00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x37, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000m70va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x00, 0x4d, 0x61, 0x69, 0x6e, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x6c, 0x00, // ....MainScene.l. + /*00a0*/ 0x00, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .ement.......... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x50, 0x6f, 0x73, // ...........InPos + /*00c0*/ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x63, 0x65, 0x6e, 0x65, // ition1Scene.cene + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ........... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000m70va8bom InPosition1Scene MainScene", cbEvents.front()); + cbEvents.pop(); + } + { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x42, 0x63, 0x00, 0x00, 0x00, // #spit#.....Bc... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x15, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ........00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x68, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000mh0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x00, 0x4d, 0x61, 0x69, 0x6e, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x6c, 0x00, // ....MainScene.l. + /*00a0*/ 0x00, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .ement.......... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x50, 0x6f, 0x73, // ...........InPos + /*00c0*/ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x63, 0x65, 0x6e, 0x65, // ition2Scene.cene + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ........... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000mh0va8bom InPosition2Scene MainScene", cbEvents.front()); + cbEvents.pop(); + } + { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x43, 0x63, 0x00, 0x00, 0x00, // #spit#.....Cc... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x15, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ........00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x72, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000mr0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x00, 0x4d, 0x61, 0x69, 0x6e, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x6c, 0x00, // ....MainScene.l. + /*00a0*/ 0x00, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .ement.......... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x50, 0x6f, 0x73, // ...........InPos + /*00c0*/ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x33, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x63, 0x65, 0x6e, 0x65, // ition3Scene.cene + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ........... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000mr0va8bom InPosition3Scene MainScene", cbEvents.front()); + cbEvents.pop(); + } + { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x44, 0x63, 0x00, 0x00, 0x00, // #spit#.....Dc... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x15, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ........00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6e, 0x6b, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000nk0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x00, 0x4d, 0x61, 0x69, 0x6e, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x6c, 0x00, // ....MainScene.l. + /*00a0*/ 0x00, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .ement.......... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x50, 0x6f, 0x73, // ...........InPos + /*00c0*/ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x34, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x63, 0x65, 0x6e, 0x65, // ition4Scene.cene + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ........... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000nk0va8bom InPosition4Scene MainScene", cbEvents.front()); + cbEvents.pop(); + } + { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x45, 0x63, 0x00, 0x00, 0x00, // #spit#.....Ec... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x15, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ........00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6e, 0x75, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000nu0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x00, 0x4d, 0x61, 0x69, 0x6e, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x6c, 0x00, // ....MainScene.l. + /*00a0*/ 0x00, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .ement.......... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x50, 0x6f, 0x73, // ...........InPos + /*00c0*/ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x35, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x63, 0x65, 0x6e, 0x65, // ition5Scene.cene + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ........... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000nu0va8bom InPosition5Scene MainScene", cbEvents.front()); + cbEvents.pop(); + } + { + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x46, 0x63, 0x00, 0x00, 0x00, // #spit#.....Fc... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x15, 0xa8, 0x00, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ........00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6f, 0x34, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000o40va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x00, 0x4d, 0x61, 0x69, 0x6e, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x6c, 0x00, // ....MainScene.l. + /*00a0*/ 0x00, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .ement.......... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6e, 0x50, 0x6f, 0x73, // ...........InPos + /*00c0*/ 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x36, 0x53, 0x63, 0x65, 0x6e, 0x65, 0x00, 0x63, 0x65, 0x6e, 0x65, // ition6Scene.cene + /*00d0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00e0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00f0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // ........... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000o40va8bom InPosition6Scene MainScene", cbEvents.front()); + cbEvents.pop(); + } + CHECK(cbEvents.empty()); + + { // In InPosition1Scene + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7b, 0x63, 0x00, 0x00, 0x00, // #spit#.....{c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x37, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000m70va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // 0000000000000000 + /*00f0*/ 0x6d, 0x37, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // m70va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb1, 0xb9, 0x04, 0x00, // ................ + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0xed, 0x35, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .....5.......... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000m70va8bom InPosition1Scene play 00000m8uedyyt0000000000000000m70va8bom", cbEvents.front()); + cbEvents.pop(); + } + { // In InPosition2Scene + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7d, 0x63, 0x00, 0x00, 0x00, // #spit#.....}c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x68, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000mh0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // 0000000000000000 + /*00f0*/ 0x6d, 0x68, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // mh0va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x72, 0xef, 0x06, 0x00, // ............r... + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x26, 0x2d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ....&-.......... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000mh0va8bom InPosition2Scene play 00000m8uedyyt0000000000000000mh0va8bom", cbEvents.front()); + cbEvents.pop(); + } + { // In InPosition3Scene + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x7f, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x72, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000mr0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // 0000000000000000 + /*00f0*/ 0x6d, 0x72, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // mr0va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xef, 0xfc, 0x08, 0x00, // ................ + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x52, 0x2e, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ....R........... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000mr0va8bom InPosition3Scene play 00000m8uedyyt0000000000000000mr0va8bom", cbEvents.front()); + cbEvents.pop(); + } + { // In InPosition4Scene + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x81, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6e, 0x6b, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000nk0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // 0000000000000000 + /*00f0*/ 0x6e, 0x6b, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // nk0va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0xff, 0x0a, 0x00, // ................ + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x36, 0x33, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ....63.......... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000nk0va8bom InPosition4Scene play 00000m8uedyyt0000000000000000nk0va8bom", cbEvents.front()); + cbEvents.pop(); + } + { // In InPosition5Scene + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x83, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6e, 0x75, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000nu0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // 0000000000000000 + /*00f0*/ 0x6e, 0x75, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // nu0va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87, 0x01, 0x0d, 0x00, // ................ + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x75, 0x29, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ....u).......... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000nu0va8bom InPosition5Scene play 00000m8uedyyt0000000000000000nu0va8bom", cbEvents.front()); + cbEvents.pop(); + } + { // In InPosition6Scene + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x85, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x6f, 0x34, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00000o40va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // 0000000000000000 + /*00f0*/ 0x6f, 0x34, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // o40va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9d, 0x05, 0x0f, 0x00, // ................ + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x13, 0x36, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // .....6.......... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK_MESSAGE(cbEvents.front() == "0: true 00000m8uedyyt0000000000000000o40va8bom InPosition6Scene play 00000m8uedyyt0000000000000000o40va8bom", cbEvents.front()); + cbEvents.pop(); + } + { // unknown + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x87, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x31, 0x61, 0x75, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00001au0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, // 0000000000000001 + /*00f0*/ 0x61, 0x75, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // au0va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x32, 0x06, 0x11, 0x00, // ............2... + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0x33, 0x53, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ....3S.......... + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK(cbEvents.empty()); + } + { // unknown + std::vector msg = { + /*0000*/ 0x23, 0x73, 0x70, 0x69, 0x74, 0x23, 0x00, 0x01, 0x00, 0x00, 0x00, 0x89, 0x63, 0x00, 0x00, 0x00, // #spit#......c... + /*0010*/ 0x34, 0x30, 0x35, 0x63, 0x37, 0x31, 0x30, 0x30, 0x2d, 0x36, 0x34, 0x65, 0x38, 0x2d, 0x34, 0x33, // 405c7100-64e8-43 + /*0020*/ 0x35, 0x64, 0x2d, 0x38, 0x61, 0x37, 0x62, 0x2d, 0x30, 0x30, 0x63, 0x35, 0x36, 0x32, 0x39, 0x66, // 5d-8a7b-00c5629f + /*0030*/ 0x39, 0x63, 0x63, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x30, 0x38, 0x36, 0x36, 0x63, 0x39, // 9ccf.....70866c9 + /*0040*/ 0x62, 0x2d, 0x61, 0x66, 0x31, 0x63, 0x2d, 0x34, 0x37, 0x36, 0x35, 0x2d, 0x61, 0x31, 0x37, 0x65, // b-af1c-4765-a17e + /*0050*/ 0x2d, 0x38, 0x62, 0x34, 0x35, 0x63, 0x39, 0x33, 0x66, 0x33, 0x61, 0x63, 0x61, 0x00, 0x00, 0x00, // -8b45c93f3aca... + /*0060*/ 0x00, 0x00, 0x1f, 0x53, 0x01, 0x00, 0x00, 0x01, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, // ...S....00000m8u + /*0070*/ 0x65, 0x64, 0x79, 0x79, 0x74, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, // edyyt00000000000 + /*0080*/ 0x30, 0x30, 0x30, 0x30, 0x31, 0x66, 0x6a, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, // 00001fj0va8bom.. + /*0090*/ 0x00, 0x01, 0x01, 0x70, 0x6c, 0x61, 0x79, 0x00, 0x2f, 0x20, 0x46, 0x61, 0x64, 0x65, 0x20, 0x4f, // ...play./ Fade O + /*00a0*/ 0x75, 0x74, 0x00, 0x63, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ut.cy........... + /*00b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00c0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*00d0*/ 0x00, 0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x6d, 0x38, 0x75, 0x65, 0x64, 0x79, 0x79, 0x74, // ...00000m8uedyyt + /*00e0*/ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, // 0000000000000001 + /*00f0*/ 0x66, 0x6a, 0x30, 0x76, 0x61, 0x38, 0x62, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x00, 0x69, 0x70, 0x00, // fj0va8bom....ip. + /*0100*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0110*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0120*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0130*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xad, 0x3a, 0x14, 0x00, // .............:.. + /*0140*/ 0x00, 0x00, 0x00, 0x00, 0xb4, 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0150*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x42, 0x0f, 0x00, // ............@B.. + /*0160*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0170*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0180*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*0190*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01a0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ................ + /*01b0*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // ...... + }; + spoofMsgCounter(msg, cnt++); + sut.checkParseMessageData(std::vector(msg.begin(), msg.end())); + CHECK(cbEvents.empty()); + } +} + + + diff --git a/components/spit/uTest/spitRepoTest.cpp b/components/spit/uTest/spitRepoTest.cpp new file mode 100644 index 0000000..9f96692 --- /dev/null +++ b/components/spit/uTest/spitRepoTest.cpp @@ -0,0 +1,128 @@ +/** + * @file spitRepoTest.cpp + * @author Gustice + * @brief + */ + +#include "doctest/doctest.h" +#include +#include + +#include "spit_repo.hpp" + +using namespace spit; +using namespace std; + +struct SomeType { + string id; + int val{}; + int tst{}; + string getID() { return id; } +}; + +class SpitRepoFix { + protected: + SpitRepo sut; +}; + +TEST_CASE_FIXTURE(SpitRepoFix, "Typical use case") { + CHECK_EQ(sut.getSize(), 0); + CHECK_EQ(sut.hasEntry("obj1"), false); + CHECK_EQ(sut.getEntry("obj1"), nullptr); + CHECK_THROWS(sut.getEntryRef("obj1")); + CHECK_EQ(sut.removeEntry("obj1"), false); + // CHECK_EQ(sut.removeEntry(1), false); + + auto obj1 = make_unique("obj1", 1, 0); + sut.addEntry(move(obj1)); + { + CHECK_EQ(sut.getSize(), 1); + CHECK_EQ(sut.hasEntry("obj1"), true); + CHECK_EQ(sut.getEntry("obj1")->val, 1); + CHECK_EQ(sut.getEntryRef("obj1").val, 1); + } + + auto obj2 = make_unique("obj2", 2, 0); + sut.addEntry(move(obj2)); + { + CHECK_EQ(sut.getSize(), 2); + CHECK_EQ(sut.hasEntry("obj2"), true); + CHECK_EQ(sut.getEntry("obj2")->val, 2); + CHECK_EQ(sut.getEntryRef("obj2").val, 2); + } + + auto obj3 = make_unique("obj3", 3, 0); + sut.addEntry(move(obj3)); + { + CHECK_EQ(sut.getSize(), 3); + CHECK_EQ(sut.hasEntry("obj3"), true); + CHECK_EQ(sut.getEntry("obj3")->val, 3); + CHECK_EQ(sut.getEntryRef("obj3").val, 3); + } + + auto obj4 = make_unique("obj4", 4, 0); + sut.addEntry(move(obj4)); + { + CHECK_EQ(sut.getSize(), 4); + CHECK_EQ(sut.hasEntry("obj4"), true); + CHECK_EQ(sut.getEntry("obj4")->val, 4); + CHECK_EQ(sut.getEntryRef("obj4").val, 4); + } + + { + auto *p = const_cast(sut.getEntry("obj3")); + auto &r = sut.getEntryRef("obj3"); + CHECK_EQ(p->val, 3); + CHECK_EQ(r.val, 3); + + CHECK_EQ(r.tst, 0); + p->tst = 20; + CHECK_EQ(r.tst, 20); + } + + { + const auto & repo = sut.getRepo(); + CHECK_EQ(repo.size(), 4); + CHECK_EQ(repo.at("obj1")->val, 1); + CHECK_EQ(repo.at("obj2")->val, 2); + CHECK_EQ(repo.at("obj3")->val, 3); + CHECK_EQ(repo.at("obj4")->val, 4); + } + + { + CHECK_EQ(sut.removeEntry("obj1"), true); + CHECK_EQ(sut.removeEntry("obj3"), true); + + const auto & repo = sut.getRepo(); + CHECK_EQ(sut.getSize(), 2); + CHECK_EQ(repo.size(), 2); + CHECK_EQ(repo.at("obj2")->val, 2); + CHECK_EQ(repo.at("obj4")->val, 4); + } + + auto obj5 = make_unique("obj5", 5, 0); + sut.addEntry(move(obj5)); + { + const auto & repo = sut.getRepo(); + CHECK_EQ(sut.getSize(), 3); + CHECK_EQ(repo.size(), 3); + CHECK_EQ(repo.at("obj2")->val, 2); + CHECK_EQ(repo.at("obj4")->val, 4); + CHECK_EQ(repo.at("obj5")->val, 5); + } + + { + sut.clearEntries(); + CHECK_EQ(sut.getSize(), 0); + } + + auto obj6 = make_unique("obj6", 6, 0); + sut.addEntry(move(obj6)); + { // Still works after clear + CHECK_EQ(sut.getSize(), 1); + CHECK_EQ(sut.hasEntry("obj6"), true); + CHECK_EQ(sut.getEntry("obj6")->val, 6); + CHECK_EQ(sut.getEntryRef("obj6").val, 6); + } + +} diff --git a/components/spit/uTest/spitTestMain.cpp b/components/spit/uTest/spitTestMain.cpp new file mode 100644 index 0000000..3e07ed7 --- /dev/null +++ b/components/spit/uTest/spitTestMain.cpp @@ -0,0 +1,11 @@ +/** + * @file uTestMain.cpp + * @author Gustice + * @brief + */ + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" +TEST_CASE("TestTest") { + CHECK(true == true); +} diff --git a/components/spit/uTest/spitTypeTest.cpp b/components/spit/uTest/spitTypeTest.cpp new file mode 100644 index 0000000..2764ff9 --- /dev/null +++ b/components/spit/uTest/spitTypeTest.cpp @@ -0,0 +1,88 @@ +/** + * @file spitRepoTest.cpp + * @author Gustice + * @brief + */ + +#include "doctest/doctest.h" +#include +#include + +#include "spit_type.hpp" + +using namespace spit; +using namespace std; + +class SpitTypetFix { + protected: +}; + +TEST_CASE_FIXTURE(SpitTypetFix, "check confirmend behaviour") { + { + SType sut("#Id#123"); + CHECK(sut.getID() == "#Id#123"); + + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.GroupName.set("G123") == true); + CHECK(sut.GroupName.get() == "G123"); + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.GroupName.set("G123") == false); + CHECK(sut.getConfirmed() == true); + CHECK(sut.GroupName.set("G321") == true); + CHECK(sut.GroupName.get() == "G321"); + CHECK(sut.getConfirmed() == false); + } + { + SType sut("#Id#123"); + CHECK(sut.getID() == "#Id#123"); + + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Fixed.set(true) == true); + CHECK(sut.Fixed.get() == true); + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Fixed.set(true) == false); + CHECK(sut.getConfirmed() == true); + CHECK(sut.Fixed.set(false) == true); + CHECK(sut.Fixed.get() == false); + CHECK(sut.getConfirmed() == false); + } + { + SType sut("#Id#123"); + CHECK(sut.getID() == "#Id#123"); + + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.DefaultDelay.set(123) == true); + CHECK(sut.DefaultDelay.get() == 123); + CHECK(sut.getConfirmed() == false); + sut.setConfirmed(true); + CHECK(sut.getConfirmed() == true); + CHECK(sut.DefaultDelay.set(123) == false); + CHECK(sut.getConfirmed() == true); + CHECK(sut.DefaultDelay.set(321) == true); + CHECK(sut.DefaultDelay.get() == 321); + CHECK(sut.getConfirmed() == false); + } + // ... + + { + SType sut("#Id#123", "grp123", "name123", true, false, 1234, 4321); + CHECK(sut.getID() == "#Id#123"); + CHECK(sut.GroupName.get() == "grp123"); + CHECK(sut.Name.get() == "name123"); + CHECK(sut.Fixed.get() == true); + CHECK(sut.ProjectIndepended.get() == false); + + CHECK(sut.DefaultDelay.get() == 1234); + CHECK(sut.DefaultDuration.get() == 4321); + } +} diff --git a/components/watchFace/CMakeLists.txt b/components/watchFace/CMakeLists.txt new file mode 100644 index 0000000..1f000f4 --- /dev/null +++ b/components/watchFace/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + INCLUDE_DIRS "include" + REQUIRES devHal +) diff --git a/components/watchFace/include/watchFace.hpp b/components/watchFace/include/watchFace.hpp new file mode 100644 index 0000000..7d502ac --- /dev/null +++ b/components/watchFace/include/watchFace.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include "stepperDrv.hpp" +#include "stepperMotor.hpp" +#include +#include "logging.hpp" + +class WatchMotor { + static constexpr uint8_t Positions = 6; + + public: + using AlmostFinishedCb = void (*)(uint8_t newIdx); + WatchMotor(IStepperMotor &stepper, AlmostFinishedCb onDone, ILogging &logger) + : _stepper(stepper), + _onFinCb(onDone), + _logger(logger) { + } + + void moveTo(uint8_t position, uint8_t profile, int minJumps, float accel, float speed) { + auto sign = position % 6; + + int minTurns = minJumps / 6; + + float corr = 0.0F; + float currentPos = _stepper.getScaledPos() * 6; + if ((_currentIndex > 4) && (currentPos < 2)) { + corr = 6 - _currentIndex - currentPos; + } else if ((_currentIndex < 2) && (currentPos > 2)) { + corr = _currentIndex + 6 - currentPos; + } else { + corr = _currentIndex - currentPos; + } + + int steps = 0; + if (_currentIndex < sign) { + steps = (sign - _currentIndex); + } else if (_currentIndex >= sign) { + steps = (sign + 6 - _currentIndex); + } + steps += minTurns * 6; + if (steps < minJumps) { + steps += 6; + } + + float delta = 2.0F * std::numbers::pi / 6 * steps; + + ESP_LOGD("MV", "Moving to %d by %.2f + %.2f", sign, delta, corr); + + _stepper.moveBy(delta + corr, profile, accel, speed, [&](int diff) { + _logger.logv(ILogging::Severity::Debug,"Almost done with head=%i",diff); + if (diff < 6) { + _logger.logv(ILogging::Severity::Warning,"Low headroom in Profile pipeline=%i",diff); + } + + if (_onFinCb) { + _onFinCb(position); + } + }); + _currentIndex = sign; + } + + float getScaled() { return _stepper.getScaledPos() * 6; } + uint8_t getIndex() { return _currentIndex; } + + private: + IStepperMotor &_stepper; + uint8_t _currentIndex{}; + AlmostFinishedCb _onFinCb{}; + ILogging &_logger; +}; diff --git a/components/watchFace/src/watchFace.cpp b/components/watchFace/src/watchFace.cpp new file mode 100644 index 0000000..569eacb --- /dev/null +++ b/components/watchFace/src/watchFace.cpp @@ -0,0 +1 @@ +// Dummy file diff --git a/components/watchFace/uTest/CMakeLists.txt b/components/watchFace/uTest/CMakeLists.txt new file mode 100644 index 0000000..78fbf2b --- /dev/null +++ b/components/watchFace/uTest/CMakeLists.txt @@ -0,0 +1,5 @@ +UnitTestsFrame() +target_link_libraries(${moduleName} PUBLIC + devHal + bases +) \ No newline at end of file diff --git a/components/watchFace/uTest/WatchFaceUTest.cpp b/components/watchFace/uTest/WatchFaceUTest.cpp new file mode 100644 index 0000000..9ef3476 --- /dev/null +++ b/components/watchFace/uTest/WatchFaceUTest.cpp @@ -0,0 +1,112 @@ +/** + * @file TestableUTest.cpp + * @author Gustice + * @brief + * + * @copyright Copyright (c) 2024 + */ + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" +#include +#include + +#include "watchFace.hpp" + +class StepperMotorMock : public IStepperMotor { + public: + void moveBy(float angle, uint8_t profile = 0) override { + lastAngle = angle; + lastProfile = profile; + } + void moveBy(float angle, uint8_t profile, float accel, float speed, std::function profileDone = nullptr) override { + lastAngle = angle; + lastProfile = profile; + } + float getScaledPos() const override { + return nextPos; + } + uint16_t getQuanted() const override { + return 0; + } + + float lastAngle{}; + uint8_t lastProfile{}; + float lastAccel{}; + float lastSpeed{}; + float nextPos{}; +}; + +class LoggerMock : public ILogging { + void output(Severity severity, std::string message) override { + // do nothing + } +}; + +void onFin(uint8_t idx) { +} + +class WatchFaceFixture { + using PositionStore = StepperMotor::Position<50, 5>; + + public: + protected: + static constexpr int MicroSteps = 32; + StepperMotorMock motor; + LoggerMock logger; + WatchMotor sut{motor, onFin, logger}; + + private: +}; + +TEST_CASE_FIXTURE(WatchFaceFixture, "Moving simple steps") { + auto next = 0*std::numbers::pi/3; + motor.nextPos = next; + sut.moveTo(1, 0, 0, 1, 1); + auto delta = 1*std::numbers::pi/3; + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 1.0F/6; + sut.moveTo(2, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 2.0F/6; + sut.moveTo(3, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 3.0F/6; + sut.moveTo(4, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 4.0F/6; + sut.moveTo(5, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 5.0F/6; + sut.moveTo(6, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 6.0F/6; + sut.moveTo(1, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 1.0F/6; + sut.moveTo(2, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance +} + +TEST_CASE_FIXTURE(WatchFaceFixture, "Moving multiple turns") { + auto next = 0*std::numbers::pi/3; + motor.nextPos = next; + sut.moveTo(1, 0, 0, 1, 1); + auto delta = 1*std::numbers::pi/3; + CHECK(motor.lastAngle == doctest::Approx(delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 1.0F/6; + sut.moveTo(1, 0, 0, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(6*delta).epsilon(0.000625)); // MicroStep tolerance + + motor.nextPos = 1.0F/6; + sut.moveTo(1, 0, 7, 1, 1); + CHECK(motor.lastAngle == doctest::Approx(12*delta).epsilon(0.000625)); // MicroStep tolerance +} \ No newline at end of file diff --git a/core b/core new file mode 160000 index 0000000..e584259 --- /dev/null +++ b/core @@ -0,0 +1 @@ +Subproject commit e584259b318da9775d54d511bbf151e570324247 diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..eeb3646 --- /dev/null +++ b/cspell.json @@ -0,0 +1,17 @@ +{ + "version": "0.2", + "ignorePaths": [], + "dictionaryDefinitions": [], + "dictionaries": [], + "words": [ + "Gustice", + "LOGD", + "LOGI", + "LOGW", + "LOGE", + "GPIO", + + ], + "ignoreWords": [], + "import": [] +} diff --git a/dependencies.lock b/dependencies.lock new file mode 100644 index 0000000..545989d --- /dev/null +++ b/dependencies.lock @@ -0,0 +1,21 @@ +dependencies: + frozen-eye/esp_telnet: + component_hash: e00a16d6372fa31df46b32e330847d6e09af2ec184d8c7cac4d183b3daac02e5 + dependencies: + - name: idf + require: private + version: '>=4.0' + source: + registry_url: https://components.espressif.com/ + type: service + version: 0.0.2 + idf: + source: + type: idf + version: 5.4.3 +direct_dependencies: +- frozen-eye/esp_telnet +- idf +manifest_hash: f3dbb2beffe19f6e4fa3818b79ea2bfac679eaaf47effee78f152007a2be54f5 +target: esp32 +version: 2.0.0 diff --git a/doc/Gegengier_Engineering_CatasWatch.dvd b/doc/Gegengier_Engineering_CatasWatch.dvd new file mode 100644 index 0000000000000000000000000000000000000000..34476a7807df0012d7e46b8df0511901d83588e2 GIT binary patch literal 168187 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 literal 0 HcmV?d00001 diff --git a/doc/InitRun.md b/doc/InitRun.md new file mode 100644 index 0000000..87c6d2d --- /dev/null +++ b/doc/InitRun.md @@ -0,0 +1,188 @@ +I (23) boot: ESP-IDF v5.3 2nd stage bootloader +I (24) boot: compile time Feb 15 2025 17:51:30 +I (24) boot: chip revision: v0.0 +I (26) boot.esp32c6: SPI Speed : 80MHz +I (31) boot.esp32c6: SPI Mode : DIO +I (36) boot.esp32c6: SPI Flash Size : 8MB +I (41) boot: Enabling RNG early entropy source... +I (46) boot: Partition Table: +I (50) boot: ## Label Usage Type ST Offset Length +I (57) boot: 0 nvs WiFi data 01 02 00009000 00006000 +I (64) boot: 1 phy_init RF data 01 01 0000f000 00001000 +I (72) boot: 2 factory factory app 00 00 00010000 00100000 +I (79) boot: End of partition table +I (83) esp_image: segment 0: paddr=00010020 vaddr=42070020 size=2b2e0h (176864) map +I (128) esp_image: segment 1: paddr=0003b308 vaddr=40800000 size=04d10h ( 19728) load +I (134) esp_image: segment 2: paddr=00040020 vaddr=42000020 size=6bfe0h (442336) map +I (226) esp_image: segment 3: paddr=000ac008 vaddr=40804d10 size=094b0h ( 38064) load +I (235) esp_image: segment 4: paddr=000b54c0 vaddr=4080e1c0 size=01bd0h ( 7120) load +I (241) boot: Loaded app from partition at offset 0x10000 +I (242) boot: Disabling RNG early entropy source... +I (256) cpu_start: Unicore app +I (265) cpu_start: Pro cpu start user code +I (265) cpu_start: cpu freq: 160000000 Hz +I (265) app_init: Application information: +I (268) app_init: Project name: X_WatchController_FW +I (274) app_init: App version: 858b7d8-dirty +I (279) app_init: Compile time: Apr 8 2025 22:42:59 +I (285) app_init: ELF file SHA256: ab11b461f... +I (290) app_init: ESP-IDF: v5.3 +I (295) efuse_init: Min chip rev: v0.0 +I (300) efuse_init: Max chip rev: v0.99 +I (305) efuse_init: Chip rev: v0.0 +I (309) heap_init: Initializing. RAM available for dynamic allocation: +I (317) heap_init: At 40812120 len 0006A4F0 (425 KiB): RAM +I (323) heap_init: At 4087C610 len 00002F54 (11 KiB): RAM +I (329) heap_init: At 50000000 len 00003FE8 (15 KiB): RTCRAM +I (336) spi_flash: detected chip: generic +I (340) spi_flash: flash io: dio +I (345) sleep: Configure to isolate all GPIO pins in sleep state +I (351) sleep: Enable automatic switching of GPIO sleep configuration +I (358) coexist: coex firmware version: dab85ae96 +I (385) coexist: coexist rom version 5b8dcfa +I (386) main_task: Started on CPU0 +I (386) main_task: Calling app_main() +I (396) esp_eth.netif.netif_glue: 42:4c:ca:ff:fe:4e +I (396) esp_eth.netif.netif_glue: ethernet attached to netif +I (406) Watch: Ethernet Started +I (406) Watch: Start Lighting +I (406) gpio: GPIO[10]| InputEn: 1| OutputEn: 1| OpenDrain: 1| Pullup: 1| Pulldown: 0| Intr:0 +I (416) gpio: GPIO[11]| InputEn: 1| OutputEn: 1| OpenDrain: 1| Pullup: 1| Pulldown: 0| Intr:0 +I (426) Watch: Start Stepper +I (426) gpio: GPIO[18]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (436) gpio: GPIO[8]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (446) gpio: GPIO[15]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (456) TMR: Create timer handle +I (456) TMR: Setup timer +I (456) Stepper: Find Home +I (466) Stepper: Homing ... +I (466) Stepper: Fast find right side of home sensor +I (486) gpio: GPIO[1]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0 +I (2206) Stepper: Find right side of home sensor +I (2376) Stepper: Find end of sensor +I (2406) Watch: Ethernet Link Up +I (2406) Watch: Ethernet Link Up +I (2406) Watch: Ethernet HW Addr 00:00:00:00:00:00 +I (3406) esp_netif_handlers: eth ip: 172.16.17.56, mask: 255.255.0.0, gw: 172.16.0.1 +I (3406) Watch: Ethernet Got IP Address +I (3406) Watch: ~~~~~~~~~~~ +I (3406) Watch: ETHIP:172.16.17.56 +I (3416) Watch: ETHMASK:255.255.0.0 +I (3416) Watch: ETHGW:172.16.0.1 +I (3416) Watch: ~~~~~~~~~~~ +I (5186) Stepper: Find left side of home sensor +I (5646) Stepper: Move back to mid position 116 +I (6806) Stepper: Reset position +I (7086) Stepper: Got to 1 +I (7086) Watch: Fist Position is 0.03 +I (7106) Watch: Changing picto-Illum to pos = 0 +I (7106) Watch: Current color set r128 g128 b128 +I (13426) Watch: Start net services +I (13426) Watch: Starting Telnet task +I (13426) TCP: Socket created +I (13426) Watch: Starting ArtNet task +I (13426) TCP: Socket bound, port 23 +I (13426) Watch: Starting SPIT task +I (13426) UDP: Starting syslog session to 172.16.16.10 +I (13426) UDP: Socket bound, port 6454 +I (13436) Telnet: Socket waiting for connect +I (13436) SPIT: Connected to eth +I (13456) SPIT: - IPv4 address: 172.16.17.56, +I (13456) SPIT: - IPv4 netmask: 255.255.0.0, +I (13446) SysLog: Starting Syslog Task +I (13466) Watch: NEW SPIT-Type ID: ActionStartShuffle GroupName: CatasWatchMovement Name: ActionStartShuffle confirmed: 0 +I (13466) UDP: Socket bound, port 514 +I (13486) Watch: NEW SPIT-Type ID: ActionMoveTo1 GroupName: CatasWatchMovement Name: ActionMoveTo1 confirmed: 0 +I (13496) Watch: NEW SPIT-Type ID: ActionMoveTo2 GroupName: CatasWatchMovement Name: ActionMoveTo2 confirmed: 0 +I (13506) Watch: NEW SPIT-Type ID: ActionMoveTo3 GroupName: CatasWatchMovement Name: ActionMoveTo3 confirmed: 0 +I (13516) Watch: NEW SPIT-Type ID: ActionMoveTo4 GroupName: CatasWatchMovement Name: ActionMoveTo4 confirmed: 0 +I (13526) Watch: NEW SPIT-Type ID: ActionMoveTo5 GroupName: CatasWatchMovement Name: ActionMoveTo5 confirmed: 0 +I (13536) Watch: NEW SPIT-Type ID: ActionMoveTo6 GroupName: CatasWatchMovement Name: ActionMoveTo6 confirmed: 0 +I (13546) SPIT: Starting Socket managing +I (17656) Watch: SERVER DETECTED +I (17656) Watch: Name: liveShowSPIT +I (17656) Watch: ID: 698a3e4f-1ae9-4dd8-9015-29332d29629e +I (17656) Watch: Computer: GgP17 +I (17656) Watch: IP: 172.16.16.10 +I (17666) Watch: Port: 15120 +I (17666) Watch: Addition: +I (17666) Watch: ClientIP: 172.16.17.56 +I (17676) SPIT: connect to 172.16.16.10 / 15120 +I (17676) Watch: MySPITApp: eventConnectionStatus 0->0 +I (17686) Watch: MySPITApp: eventConnectionStatus 0->4 +I (18196) Watch: eventProjectReport ID: CatasWatchProject +I (18196) Watch: eventStartProjectTransfer +W (18306) SPIT: TypeVeto e019646a-4e24-4608-954e-da4ca6761bb6 / ToIgnore 43 +I (18326) Watch: NEW SPIT-Object ID: 00000m8uedyyt0000000000000002sb0va8bom Name: -> SPITType: ActionStartShuffle +I (18336) Watch: NEW SPIT-Object ID: 00000m8uedyyt0000000000000002rd0va8bom Name: -> SPITType: ActionMoveTo1 +I (18356) Watch: NEW SPIT-Object ID: 00000m8uedyyt0000000000000002vl0va8bom Name: -> SPITType: ActionMoveTo2 +I (18366) Watch: NEW SPIT-Object ID: 00000m8uedyyt0000000000000002wh0va8bom Name: -> SPITType: ActionMoveTo3 +I (18386) Watch: NEW SPIT-Object ID: 00000m8uedyyt0000000000000002yl0va8bom Name: -> SPITType: ActionMoveTo4 +I (18396) Watch: NEW SPIT-Object ID: 00000m8uedyyt0000000000000002tc0va8bom Name: -> SPITType: ActionMoveTo5 +I (18416) Watch: NEW SPIT-Object ID: 00000m8uedyyt0000000000000002zi0va8bom Name: -> SPITType: ActionMoveTo6 +I (18446) Watch: Min heap size: 297568 / FreeHeap 299880 +I (18446) Watch: NEW SPIT-REMOTE-Type ID: playControlStart GroupName: Playcontrol Name: start confirmed: 1 +I (18466) Watch: NEW SPIT-REMOTE-Type ID: playControlPause GroupName: Playcontrol Name: pause confirmed: 1 +I (18476) Watch: NEW SPIT-REMOTE-Type ID: nextSceneBridge GroupName: Playcontrol Name: next scene confirmed: 1 +I (18496) Watch: NEW SPIT-REMOTE-Type ID: nextMainSceneBridge GroupName: Playcontrol Name: next main scene confirmed: 1 +I (18506) Watch: NEW SPIT-REMOTE-Type ID: previuosSceneBridge GroupName: Playcontrol Name: previous scene confirmed: 1 +I (18526) Watch: NEW SPIT-REMOTE-Type ID: previousMainSceneBridge GroupName: Playcontrol Name: previous main scene confirmed: 1 +I (18546) Watch: NEW SPIT-REMOTE-Type ID: mainmixerSound_Volume00 GroupName: Main Mixer Name: Sound confirmed: 1 +I (18556) Watch: NEW SPIT-REMOTE-Type ID: mainmixerDMX_Dimmer00 GroupName: Main Mixer Name: confirmed: 1 +I (18566) Watch: NEW SPIT-REMOTE-Type ID: mainmixerDMX_RGB00 GroupName: Main Mixer Name: confirmed: 1 +I (18586) Watch: NEW SPIT-REMOTE-Type ID: mainmixerDMX_RGB10 GroupName: Main Mixer Name: confirmed: 1 +I (18606) Watch: NEW SPIT-REMOTE-Type ID: mainmixerDMX_RGB20 GroupName: Main Mixer Name: confirmed: 1 +I (18616) Watch: NEW SPIT-REMOTE-Type ID: mainmixerDMX_RGB30 GroupName: Main Mixer Name: confirmed: 1 +I (18636) Watch: NEW SPIT-REMOTE-Type ID: mainmixerDMX_RGB40 GroupName: Main Mixer Name: confirmed: 1 +I (18646) Watch: NEW SPIT-REMOTE-Type ID: mainmixerDMX_RGB50 GroupName: Main Mixer Name: confirmed: 1 +I (18666) Watch: NEW SPIT-REMOTE-Type ID: mainmixerMedia_SoundVolume00 GroupName: Main Mixer Name: Media confirmed: 1 +I (18686) Watch: NEW SPIT-REMOTE-Type ID: mainmixerMedia_Transparency00 GroupName: Main Mixer Name: Media confirmed: 1 +I (18696) Watch: NEW SPIT-REMOTE-Type ID: selectedJingles GroupName: Playcontrol Name: selected Jingles confirmed: 1 +I (18716) Watch: NEW SPIT-REMOTE-Type ID: selectedJingleGroups GroupName: Playcontrol Name: selected Jinglegroups confirmed: 1 +I (18726) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000000lo0va8bom GroupName: MainScene Name: ChangingPositionScene confirmed: 1 +I (18746) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000000m70va8bom GroupName: MainScene Name: InPosition1Scene confirmed: 1 +I (18756) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000000mh0va8bom GroupName: MainScene Name: InPosition2Scene confirmed: 1 +I (18776) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000000mr0va8bom GroupName: MainScene Name: InPosition3Scene confirmed: 1 +I (18786) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000000nk0va8bom GroupName: MainScene Name: InPosition4Scene confirmed: 1 +I (18806) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000000nu0va8bom GroupName: MainScene Name: InPosition5Scene confirmed: 1 +I (18826) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000000o40va8bom GroupName: MainScene Name: InPosition6Scene confirmed: 1 +I (18836) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000001au0va8bom GroupName: MainScene Name: StartShuffle confirmed: 1 +I (18856) Watch: NEW SPIT-REMOTE-Type ID: 00000m8uedyyt0000000000000001fj0va8bom GroupName: MainScene Name: Fence confirmed: 1 +I (18866) Watch: NEW SPIT-REMOTE-Object ID: playControlStart Name: start -> SPITType: start +I (18886) Watch: NEW SPIT-REMOTE-Object ID: playControlPause Name: pause -> SPITType: pause +I (18906) Watch: NEW SPIT-REMOTE-Object ID: nextSceneBridge Name: next scene -> SPITType: next scene +I (18916) Watch: NEW SPIT-REMOTE-Object ID: nextMainSceneBridge Name: next main scene -> SPITType: next main scene +I (18936) Watch: NEW SPIT-REMOTE-Object ID: previuosSceneBridge Name: previous scene -> SPITType: previous scene +I (18946) Watch: NEW SPIT-REMOTE-Object ID: previousMainSceneBridge Name: previous main scene -> SPITType: previous main scene +I (18966) Watch: NEW SPIT-REMOTE-Object ID: mainmixerSound_Volume00 Name: Sound Volume -> SPITType: Sound +I (18976) Watch: NEW SPIT-REMOTE-Object ID: mainmixerDMX_Dimmer00 Name: Dimmer -> SPITType: +I (18996) Watch: NEW SPIT-REMOTE-Object ID: mainmixerDMX_RGB00 Name: Red -> SPITType: +I (19006) Watch: NEW SPIT-REMOTE-Object ID: mainmixerDMX_RGB10 Name: Green -> SPITType: +I (19026) Watch: NEW SPIT-REMOTE-Object ID: mainmixerDMX_RGB20 Name: Blue -> SPITType: +I (19036) Watch: NEW SPIT-REMOTE-Object ID: mainmixerDMX_RGB30 Name: Cyan -> SPITType: +I (19056) Watch: NEW SPIT-REMOTE-Object ID: mainmixerDMX_RGB40 Name: Magenta -> SPITType: +I (19076) Watch: NEW SPIT-REMOTE-Object ID: mainmixerDMX_RGB50 Name: Yellow -> SPITType: +I (19086) Watch: NEW SPIT-REMOTE-Object ID: mainmixerMedia_SoundVolume00 Name: Sound Volume -> SPITType: Media +I (19106) Watch: NEW SPIT-REMOTE-Object ID: mainmixerMedia_Transparency00 Name: Media Transparency -> SPITType: Media +I (19116) Watch: NEW SPIT-REMOTE-Object ID: selectedJinglesps Name: Play / Stop -> SPITType: selected Jingles +I (19136) Watch: NEW SPIT-REMOTE-Object ID: selectedJinglespf Name: Play / Fade Out -> SPITType: selected Jingles +I (19146) Watch: NEW SPIT-REMOTE-Object ID: selectedJingleGroupsps Name: Play / Stop -> SPITType: selected Jinglegroups +I (19166) Watch: NEW SPIT-REMOTE-Object ID: selectedJingleGroupspf Name: Play / Fade Out -> SPITType: selected Jinglegroups +I (19176) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000000lo0va8bom Name: play -> SPITType: ChangingPositionScene +W (19196) SPIT: Finished searching +W (19196) SPIT: Start working +I (19196) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000000m70va8bom Name: play -> SPITType: InPosition1Scene +I (19216) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000000mh0va8bom Name: play -> SPITType: InPosition2Scene +I (19226) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000000mr0va8bom Name: play -> SPITType: InPosition3Scene +I (19246) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000000nk0va8bom Name: play -> SPITType: InPosition4Scene +I (19256) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000000nu0va8bom Name: play -> SPITType: InPosition5Scene +I (19276) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000000o40va8bom Name: play -> SPITType: InPosition6Scene +I (19286) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000001au0va8bom Name: play -> SPITType: StartShuffle +I (19306) Watch: NEW SPIT-REMOTE-Object ID: 00000m8uedyyt0000000000000001fj0va8bom Name: play -> SPITType: Fence +I (19316) Watch: eventEndProjectTransfer +I (19316) Watch: eventProjectReport ID: CatasWatchProject Name: FramesPerSecond: 44100 +I (23446) Watch: Min heap size: 278024 / FreeHeap 282460 +I (28446) Watch: Min heap size: 278024 / FreeHeap 282460 +I (33446) Watch: Min heap size: 278024 / FreeHeap 282460 +I (38446) Watch: Min heap size: 278024 / FreeHeap 282460 +I (43446) Watch: Min heap size: 278024 / FreeHeap 282460 \ No newline at end of file diff --git a/doc/LSS-Icon.afdesign b/doc/LSS-Icon.afdesign new file mode 100644 index 0000000000000000000000000000000000000000..732dad7447eba8bb6d5ba68175d8614ec1c5bf4d GIT binary patch literal 89027 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 literal 0 HcmV?d00001 diff --git a/doc/LSS-Icon.jpg b/doc/LSS-Icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7c24febbcb50dc933fe4685877891de335fed828 GIT binary patch literal 26214 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 UvrIGaP|JS?g#I0E+5VpYA6`9TTL1t6 literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.dpr b/doc/LiveShowProject/CatasWatchProject.dpr new file mode 100644 index 0000000000000000000000000000000000000000..f698cdea94a943c95a18c615eef6340c0c0306c1 GIT binary patch literal 38 acmZ4UmVvcgfq{X65r~`)G;WPkI8v;E5 literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.ldv b/doc/LiveShowProject/CatasWatchProject.ldv new file mode 100644 index 0000000000000000000000000000000000000000..7091cc34d8843f6ec71581f90bf0efd9857dcb0b GIT binary patch literal 19073 zcmeI4&1(}u6u@WOrdDlf1$$IH=}mTPnidu72Z&xos30CZq-mP8O*V9ss_CH$Dtc59 zJbBcU9t1D`1>(hn9z5#RgA^)=pi*bv>`UIZn{1ly(ye&|J8!-+$;{6C?VFFy%kLtw zBt(ypZdhH$#MALht1l8CWTabOyXBP-hfU>*68@4i31>O-t&7ph!QjE@rgU~V}^klAbae^LIG;6 z)hv3>Bp%A5O`WR6Xx=8PvVd(;WrgU&@?+^@rEFG7^C9Srn#sBH{H?Mq+oiu<{bc1_ z*(-zkn;}(oh1(t5mG0ivMlH&b7U_;(*CK3<&V<&uEefdlMr1WlMOAazjH+g*5LwN| z)@w!x=t0@qZA`UZ{l+051eM|#D&WvX8*yU!^Q7nPYxn~6jG4<2M7cg)#+fJK^Cv*- znJ46-5lfzE2n5{5O+W?ojy&iBYL1pCc^{sP`17Q9#X)3%G%kGdNX*1^8E2j_Pt>po z7~(s?J^=_2Qfc!x7Dfu_U0itL`0(VIKTrDptiVr4&p7!Df+*Lg%Q*9dd7_5JHt|G5 zAeKf-P?zjb`^292;Yre;Cwtb05E&qmBPcPG#F;0|6E!S0!;|`&fEqo@xw74bALa?Q ztDH#ZJNBXvPe%QDGGIK=_t*828z?c8#F-}@!jn1#+SPhz2TXdXUaD|B7G**2Vlv+; zC4G33^5@B5;u#_Xq>x!ebPT!NjsIv z{Nl_^VdGx$zTOeUg_hTHUWvJw&Mcujv0?2k%Xx!d6OhLMc*7XRT=S_E&vmHAM>^NB zX6Dc~Nu1BxnT-(HfBY3Qyb^OUogvZyBCaJs%z(`jrn%ZUyh}{a!`vD~ z`lOE64xQwe3dzjEG_75Qoq4mml*-se;K5$ee>Gjn>o(AdMuCIq65Wiz?00BPhCTDEY1@MfgK}5+qcpB7zIOW-J zhyz1WByQh^020S2{m>bWeM%hh`zI7^i8%E&0dM7cnb6e=e^&D8T9fS@f(4W99NHUO zvvUG$94i?naWn*W-TVj)sA1>+y7T>BLa3MrJD-=KKug%!Se1LKo{1A+iSW#dC>axn zbjB8ERsew@?2L|uY3S^(eghEHiF3pU44xzpaaQ4tC&9##UBqiSL=||mnskQgw0@Y5 zOb5t-3V0-ia{{Kp)T+7=CUN#I>vt4u#JQ_pk8J8Z#CiM$$}(|eH;@6stH=KnaS#G( z;Ll+_|n>+t~LI=hR;)$i$J|z{DYs^@xcB z5WrXA6$12ZpUAek(7A6R`P(?wR4z>73_ex=0U`ed!ribkN}F59IeQ5{&cu=3(BZ@Z z2oUDTV8qyA;<()%((4h(DJqH;s++5GQ)?|MbPzNsJppGK9yR7?fY;`B>y5%ZEX>3L-j{ zz&`Kw{4hyeT(59#RnEK)SwC!9tl4JGns?Rq+a9YoY-aNe_z0k1s8hca>{3Ibq$ja7$SKX?7>Z&dp9u-{fld!0&_LMHLs+5r=o>1`LQzcQ z>44ysG!V*q2&={;d`bxAyrL-5Xr1S&fM8njldxR(t*&2h1&-s6pOI_XB0NexZK{eY z*8d3r{|L|u4oE0^L8pBmOy>fRf^lTC#<>0b-V2&ZeRxw9Rjg04FHUd~(LISNc8L{^ Mp>ZLaOmt3u0mOM?-T(jq literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.lsb b/doc/LiveShowProject/CatasWatchProject.lsb new file mode 100644 index 0000000000000000000000000000000000000000..cd59ea5fb08a89d3c2ae7d3861b776237730c454 GIT binary patch literal 17935 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 Dn*;=U literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.lse b/doc/LiveShowProject/CatasWatchProject.lse new file mode 100644 index 0000000000000000000000000000000000000000..817353f8fedd33a188319f37beef1f8f799a0710 GIT binary patch literal 96 zcmZ4UmVvcAih+TF35XfNw3-13X literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.lsh b/doc/LiveShowProject/CatasWatchProject.lsh new file mode 100644 index 0000000000000000000000000000000000000000..64ea680e147538524fcc43d03595724de951c6f6 GIT binary patch literal 317 zcmZ4UmVvd3fq~J4fq{Vuh#A4OngIypW|SnEB&L^xIS9fqJH;TWw8W&i0<2!a#W%tu zu_&cHu_!elF*!RiJyp-g(J$UJuOzi7EipNjK@f$jmz7uqvFE@VpylPez^3pz7M1v= zmMG{d6gxWmfn34JAZTErYwW0NVWDeks_SB|>*@#*hv)_>0RxDuVJ@!9G09C#K{yu5 hGBhwKuShHf`I4D|6=Vg72?2}@1T>prXf}lC1OSJTK}!Gt literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.lsj b/doc/LiveShowProject/CatasWatchProject.lsj new file mode 100644 index 0000000000000000000000000000000000000000..d78b90d481e8100684f4487a2cf372d7ef35317e GIT binary patch literal 38 bcmZ4UmVvcgfq{X635Xei7zAK6NDK@BQ33)y literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.lsp b/doc/LiveShowProject/CatasWatchProject.lsp new file mode 100644 index 0000000000000000000000000000000000000000..e725390729ac7f051b454d9d42d4963f06b08ac2 GIT binary patch literal 661 zcmZ4UmVvd3fq|)q0Z4$cpmSnLVsUt4NpeO&QGQlxatTAMenx(7s(yNDaY<%!s(w~} zQn7vkM6OsLq$obBJTo~vu{6FsF|#ByFFhzfKUd!^JU${GY>sn&UP)1YPEKl(eoB6_ zzE5UZYH&t=Im~zl#=;$0IbFrv(@r{5R9ZacPEGTn}Z)!?r;-GPj zPikIzNe0|G28LE9##Uy=MoAlT(9B pQh>SV>)ElN{UDl1FC90Xxlo@r2)Xpxkk%fJzySdyF(nwQDI zYm-run`39A52hHP24s{ZnIxu{BlLnB z>|B`H=83r_#RipVjx#7SF)+z0&aNm18pi>|P_N)pZH7?oyapJU<+eaQj3Bo#GO*=j z=A|YU!D&bk1Emna{sV*!qX}7;gw;Bb8Khaa73u^6zDuW)b^D34E(c*bKo i{9J_d=n>at^t7(L)1cUZ$`*nWB?u5(oPZTGFaQ7rb@lfE literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.lsv b/doc/LiveShowProject/CatasWatchProject.lsv new file mode 100644 index 0000000000000000000000000000000000000000..da8a1a1427877d56088da1588efb7ccd33992fc9 GIT binary patch literal 1723 zcmcJPKTE?v7{=eLNztNMp^mOj4sETqsNm3Q?VtoQIZ3QZTTL%e6QNEHf~#M^(ajEm zi=V^KAzdAWddb~ubM(ML?uEPOp7+V0A4hU8pJ3hs7{DqM4+1%>etc%PnkNU1=0+n9 z@**^ZhdI_*V8evJ|xAEh(7Zg!3PTPO*Q!Z_ZZnY@ZOiC7!uKEULQEU zQh-;~;JHcsKTQh(Q87t*jo^_0XNN#$1gQ^wOHIE-9uP25q(sFq<@M1A6~(q!)!@tK z6S8seYC%#AiRd%04ZP&ww!@+wI)l*eMxN)-o`);hGIDV}?Z}EL_H{ygm$=*Jp-bDd Uw1u4!@^F28lND3!|A!WS0RB(GT>RPhL5zS;FpVh3V32S(bR+1dHV>-}c!{d6u`AA~Tj2_YKP4A$)zwkw@= zvX!?bZge1nt&!}F#{mzrZeye0d2eqWc>{5IxBIf&Ka>6)GIl&S@IB|%{@%7&rSA4l z_mDC^BGc4!=4dE~<~{Sd-IM;%ytyxXYv!IWrMqTsJ0p3^3_NqtKQkHbIi4@t{_)Z4 z!I%=YsTrb47&4Nzr$e|V6Cu9A$lmSJL14Q*+h}V7mxhRX0&K-rcXRFUQ!@FMIh zjO1|*aZ#!{Ejkjt*;S|pEJ8=3;V}<{HKR+ZMreqRM8jt&PAD&>8ZZkTiGnM;q@SYt zS0RAW@Xpy;npp6xl7)#z za7e8t(STbi9HUbCKZtH*MDL7kmshHZPRDS9=%=7$xkNL`NRMY$`*;(8NsdTscuXeA z!bFD@j8>Cqi=b!@3Y-b0CO-7-F?+A4@l|^$=52F8D#&v*l8T zY!IfcI%N@qYoEpEvHe$zAD6*^PU* zKNv{5Dr+soRb|~hcrywD>8oqE=Sp?)7VVw_sB>@RfXYLKlcrLyiNbjTbIvEE#$Omk zleYSW6CsU)$7GT$^l=U;FliSk9E;K@I2q-VFPzhlg_w#}!*5#SX5Hk!u&%g2TqyOF literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.rbo b/doc/LiveShowProject/CatasWatchProject.rbo new file mode 100644 index 0000000000000000000000000000000000000000..f009b0c0e861c8c9a3c1f0aa69786f2a5822257c GIT binary patch literal 539 zcmZ4UmVvd3fq_Ygfq{V$h?#(x8OjDyKp>Etn3-3cUz(RvmY-9an`#JBh6+F;Ap9Q* zBddb385yL21{Eczdb)&U=BDOk=B4_jR+I!Mr{<*^!xSNm28n<$A!C^sjCOa4a0ya?T literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.rtf b/doc/LiveShowProject/CatasWatchProject.rtf new file mode 100644 index 0000000..1740ec3 --- /dev/null +++ b/doc/LiveShowProject/CatasWatchProject.rtf @@ -0,0 +1,5 @@ +{\rtf1\ansi +{\fonttbl\f0\fnil Monospaced;} + +\par +} diff --git a/doc/LiveShowProject/CatasWatchProject.spi b/doc/LiveShowProject/CatasWatchProject.spi new file mode 100644 index 0000000000000000000000000000000000000000..95c8942f8ff032614ea67163b8d843a47bf2d5a5 GIT binary patch literal 2335 zcmZ4UmVvd3fq{hq2$-Ol14uJ6@H!@!Waj7j=9i_0 z^GeVZfeb+dXy%v@V~z=$86Ym2ImX18V@%i_Gh)m!BW#WlG3FQ%Hph?{a}2SXBLs=B z;F83mlHiQew6vU59EM?0iPc0AOcOm_fM)V|=9Q!tIp^nj73b$M5N8@FO@Y!ED}$N= z2;^o|<(MQFrhqv}M1Gk;WtCZSUM|>0l2}d0ZX?iPKp@%qzUScwt z2v$ZLFIAM{@sbIumk>p;PMs@?(;08CMd4%NAbSZWO%pE}8d%`*k}0Z}Oo2{;Bs^n_ zw=idcX~r9cD10m&`gzI70FRf>zy^7B*_S2HQ7qwukCuz3ks2r%-{CaR2c hQ2m9i5XUA4bGgGhTNo2eGj*&);bY-o^C5|z0{{-}ulWD~ literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.ssj b/doc/LiveShowProject/CatasWatchProject.ssj new file mode 100644 index 0000000000000000000000000000000000000000..e9143e6dfd1e873bd21025a6b49afa3224a39f63 GIT binary patch literal 22 UcmZ4UmVvcgfPsO535ekU04z2FC;$Ke literal 0 HcmV?d00001 diff --git a/doc/LiveShowProject/CatasWatchProject.uip b/doc/LiveShowProject/CatasWatchProject.uip new file mode 100644 index 0000000000000000000000000000000000000000..efec254dc4421d6b965b24d8b4cf82627f657637 GIT binary patch literal 150 zcmZ4UmVvc=0s{jB6A&{ps2PAju0?5TN@ZmUn1dh;%QFqi5-pPQbHVC~RZFZoprJ4T E0H>)U;s5{u literal 0 HcmV?d00001 diff --git a/esp_idf_project_configuration.json b/esp_idf_project_configuration.json new file mode 100644 index 0000000..dd82b06 --- /dev/null +++ b/esp_idf_project_configuration.json @@ -0,0 +1,54 @@ +{ + "App": { + "build": { + "compileArgs": [], + "ninjaArgs": [], + "buildDirectoryPath": "", + "sdkconfigDefaults": [], + "sdkconfigFilePath": "" + }, + "env": {}, + "idfTarget": "", + "flashBaudRate": "", + "monitorBaudRate": "", + "openOCD": { + "debugLevel": -1, + "configs": [], + "args": [] + }, + "tasks": { + "preBuild": "", + "preFlash": "", + "postBuild": "", + "postFlash": "" + } + }, + "UnitTest": { + "build": { + "compileArgs": [], + "ninjaArgs": [], + "buildDirectoryPath": "build_uTest", + "sdkconfigDefaults": [ + "test/unitOnTarget/sdkconfig.defaults" + ], + "sdkconfigFilePath": "" + }, + "env": { + "UNIT_TESTS": "All" + }, + "idfTarget": "", + "flashBaudRate": "", + "monitorBaudRate": "", + "openOCD": { + "debugLevel": -1, + "configs": [], + "args": [] + }, + "tasks": { + "preBuild": "", + "preFlash": "", + "postBuild": "", + "postFlash": "" + } + } +} diff --git a/eth2ap.png b/eth2ap.png deleted file mode 100644 index 39e59aa14fccc872f69dc5d7a752ef6aeded9f6b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88732 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 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index ff186a4..286959e 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,2 +1,2 @@ -idf_component_register(SRCS "ethernet_example_main.c" +idf_component_register(SRCS "lightsMain.cpp" INCLUDE_DIRS ".") diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index 885ccf6..0be2ccc 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -1,25 +1,25 @@ menu "Example Configuration" - config EXAMPLE_WIFI_SSID + config DEVICE_WIFI_SSID string "Wi-Fi SSID" default "eth2ap" help Set the SSID of Wi-Fi ap interface. - config EXAMPLE_WIFI_PASSWORD + config DEVICE_WIFI_PASSWORD string "Wi-Fi Password" default "12345678" help Set the password of Wi-Fi ap interface. - config EXAMPLE_WIFI_CHANNEL + config DEVICE_WIFI_CHANNEL int "WiFi channel" range 1 13 default 1 help Set the channel of Wi-Fi ap. - config EXAMPLE_MAX_STA_CONN + config DEVICE_MAX_STA_CONN int "Maximum STA connections" default 4 help diff --git a/main/config.hpp b/main/config.hpp new file mode 100644 index 0000000..a2f3ce7 --- /dev/null +++ b/main/config.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include + +struct { + const char *syslogDest = "syslogDst"; // example: "172.16.16.10" + const char *mqttBroker = "mqttBroker"; // example: "172.16.16.10" + const char *mqttPrefix = "mqttPrefix"; // example: "SomeTag" + const char *spitServer = "spitServer"; // example: "172.16.16.10" + const char *motorPower = "motorPower"; // example: "50" +} ConfigKeys; + +// MQTT Topics +struct { + // Subscriptions: + const char *startShow = /* / */ "elixirLights/startShow"; + // Publishes: + const char *showDone = /* / */ "elixirLights/showDone"; +} MqttTopics; + +struct { + const char *expectedServer = ""; + struct { + const char *group = "CatasWatchMovement"; + const char *startShow = "StartShow"; + } Provided; + struct { + const char *showDone = "ShowDone"; + } Expected; +} SpitTypes; \ No newline at end of file diff --git a/main/ethernet_example_main.c b/main/ethernet_example_main.c deleted file mode 100644 index 27ed39a..0000000 --- a/main/ethernet_example_main.c +++ /dev/null @@ -1,222 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Unlicense OR CC0-1.0 - */ -/* eth2ap (Ethernet to Wi-Fi AP packet forwarding) Example - - This example code is in the Public Domain (or CC0 licensed, at your option.) - - Unless required by applicable law or agreed to in writing, this - software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR - CONDITIONS OF ANY KIND, either express or implied. -*/ -#include -#include -#include "sdkconfig.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "freertos/queue.h" -#include "esp_event.h" -#include "esp_log.h" -#include "esp_eth_driver.h" -#include "esp_wifi.h" -#include "nvs_flash.h" -#include "esp_private/wifi.h" -#include "ethernet_init.h" - -static const char *TAG = "eth2ap_example"; -static esp_eth_handle_t s_eth_handle = NULL; -static QueueHandle_t flow_control_queue = NULL; -static bool s_sta_is_connected = false; -static bool s_ethernet_is_connected = false; -static uint8_t s_eth_mac[6]; - -#define FLOW_CONTROL_QUEUE_TIMEOUT_MS (100) -#define FLOW_CONTROL_QUEUE_LENGTH (40) -#define FLOW_CONTROL_WIFI_SEND_TIMEOUT_MS (100) - -typedef struct { - void *packet; - uint16_t length; -} flow_control_msg_t; - -// Forward packets from Wi-Fi to Ethernet -static esp_err_t pkt_wifi2eth(void *buffer, uint16_t len, void *eb) -{ - if (s_ethernet_is_connected) { - if (esp_eth_transmit(s_eth_handle, buffer, len) != ESP_OK) { - ESP_LOGE(TAG, "Ethernet send packet failed"); - } - } - esp_wifi_internal_free_rx_buffer(eb); - return ESP_OK; -} - -// Forward packets from Ethernet to Wi-Fi -// Note that, Ethernet works faster than Wi-Fi on ESP32, -// so we need to add an extra queue to balance their speed difference. -static esp_err_t pkt_eth2wifi(esp_eth_handle_t eth_handle, uint8_t *buffer, uint32_t len, void *priv) -{ - esp_err_t ret = ESP_OK; - flow_control_msg_t msg = { - .packet = buffer, - .length = len - }; - if (xQueueSend(flow_control_queue, &msg, pdMS_TO_TICKS(FLOW_CONTROL_QUEUE_TIMEOUT_MS)) != pdTRUE) { - ESP_LOGE(TAG, "send flow control message failed or timeout"); - free(buffer); - ret = ESP_FAIL; - } - return ret; -} - -// This task will fetch the packet from the queue, and then send out through Wi-Fi. -// Wi-Fi handles packets slower than Ethernet, we might add some delay between each transmitting. -static void eth2wifi_flow_control_task(void *args) -{ - flow_control_msg_t msg; - int res = 0; - uint32_t timeout = 0; - while (1) { - if (xQueueReceive(flow_control_queue, &msg, pdMS_TO_TICKS(FLOW_CONTROL_QUEUE_TIMEOUT_MS)) == pdTRUE) { - timeout = 0; - if (s_sta_is_connected && msg.length) { - do { - vTaskDelay(pdMS_TO_TICKS(timeout)); - timeout += 2; - res = esp_wifi_internal_tx(WIFI_IF_AP, msg.packet, msg.length); - } while (res && timeout < FLOW_CONTROL_WIFI_SEND_TIMEOUT_MS); - if (res != ESP_OK) { - ESP_LOGE(TAG, "WiFi send packet failed: %d", res); - } - } - free(msg.packet); - } - } - vTaskDelete(NULL); -} - -// Event handler for Ethernet -static void eth_event_handler(void *arg, esp_event_base_t event_base, - int32_t event_id, void *event_data) -{ - switch (event_id) { - case ETHERNET_EVENT_CONNECTED: - ESP_LOGI(TAG, "Ethernet Link Up"); - s_ethernet_is_connected = true; - esp_eth_ioctl(s_eth_handle, ETH_CMD_G_MAC_ADDR, s_eth_mac); - esp_wifi_set_mac(WIFI_IF_AP, s_eth_mac); - ESP_ERROR_CHECK(esp_wifi_start()); - break; - case ETHERNET_EVENT_DISCONNECTED: - ESP_LOGI(TAG, "Ethernet Link Down"); - s_ethernet_is_connected = false; - ESP_ERROR_CHECK(esp_wifi_stop()); - break; - case ETHERNET_EVENT_START: - ESP_LOGI(TAG, "Ethernet Started"); - break; - case ETHERNET_EVENT_STOP: - ESP_LOGI(TAG, "Ethernet Stopped"); - break; - default: - break; - } -} - -// Event handler for Wi-Fi -static void wifi_event_handler(void *arg, esp_event_base_t event_base, - int32_t event_id, void *event_data) -{ - static uint8_t s_con_cnt = 0; - switch (event_id) { - case WIFI_EVENT_AP_STACONNECTED: - ESP_LOGI(TAG, "Wi-Fi AP got a station connected"); - if (!s_con_cnt) { - s_sta_is_connected = true; - esp_wifi_internal_reg_rxcb(WIFI_IF_AP, pkt_wifi2eth); - } - s_con_cnt++; - break; - case WIFI_EVENT_AP_STADISCONNECTED: - ESP_LOGI(TAG, "Wi-Fi AP got a station disconnected"); - s_con_cnt--; - if (!s_con_cnt) { - s_sta_is_connected = false; - esp_wifi_internal_reg_rxcb(WIFI_IF_AP, NULL); - } - break; - default: - break; - } -} - -static void initialize_ethernet(void) -{ - uint8_t eth_port_cnt = 0; - esp_eth_handle_t *eth_handles; - ESP_ERROR_CHECK(example_eth_init(ð_handles, ð_port_cnt)); - if (eth_port_cnt > 1) { - ESP_LOGW(TAG, "multiple Ethernet devices detected, the first initialized is to be used!"); - } - s_eth_handle = eth_handles[0]; - free(eth_handles); - ESP_ERROR_CHECK(esp_eth_update_input_path(s_eth_handle, pkt_eth2wifi, NULL)); - bool eth_promiscuous = true; - ESP_ERROR_CHECK(esp_eth_ioctl(s_eth_handle, ETH_CMD_S_PROMISCUOUS, ð_promiscuous)); - ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event_handler, NULL)); - ESP_ERROR_CHECK(esp_eth_start(s_eth_handle)); -} - -static void initialize_wifi(void) -{ - ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL)); - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); - ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); - wifi_config_t wifi_config = { - .ap = { - .ssid = CONFIG_EXAMPLE_WIFI_SSID, - .ssid_len = strlen(CONFIG_EXAMPLE_WIFI_SSID), - .password = CONFIG_EXAMPLE_WIFI_PASSWORD, - .max_connection = CONFIG_EXAMPLE_MAX_STA_CONN, - .authmode = WIFI_AUTH_WPA_WPA2_PSK, - .channel = CONFIG_EXAMPLE_WIFI_CHANNEL // default: channel 1 - }, - }; - if (strlen(CONFIG_EXAMPLE_WIFI_PASSWORD) == 0) { - wifi_config.ap.authmode = WIFI_AUTH_OPEN; - } - ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); - ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config)); -} - -static esp_err_t initialize_flow_control(void) -{ - flow_control_queue = xQueueCreate(FLOW_CONTROL_QUEUE_LENGTH, sizeof(flow_control_msg_t)); - if (!flow_control_queue) { - ESP_LOGE(TAG, "create flow control queue failed"); - return ESP_FAIL; - } - BaseType_t ret = xTaskCreate(eth2wifi_flow_control_task, "flow_ctl", 2048, NULL, (tskIDLE_PRIORITY + 2), NULL); - if (ret != pdTRUE) { - ESP_LOGE(TAG, "create flow control task failed"); - return ESP_FAIL; - } - return ESP_OK; -} - -void app_main(void) -{ - esp_err_t ret = nvs_flash_init(); - if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { - ESP_ERROR_CHECK(nvs_flash_erase()); - ret = nvs_flash_init(); - } - ESP_ERROR_CHECK(ret); - ESP_ERROR_CHECK(esp_event_loop_create_default()); - ESP_ERROR_CHECK(initialize_flow_control()); - initialize_wifi(); - initialize_ethernet(); -} diff --git a/main/idf_component.yml b/main/idf_component.yml index 8453ecc..23052c8 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,3 +1,6 @@ +## IDF Component Manager Manifest File +# https://docs.espressif.com/projects/idf-component-manager/en/latest/reference/manifest_file.html +# https://components.espressif.com + dependencies: - ethernet_init: - path: ${IDF_PATH}/examples/ethernet/basic/components/ethernet_init + frozen-eye/esp_telnet: "^0.0.2" diff --git a/main/lighsMain.hpp b/main/lighsMain.hpp new file mode 100644 index 0000000..2d7f4ce --- /dev/null +++ b/main/lighsMain.hpp @@ -0,0 +1,49 @@ +/** + * @file watchMain.hpp + * @author Gustice + * + * @copyright Copyright (c) 2024 + */ +#pragma once + + +#include +#include +#include +#include "sdkconfig.h" +#include "esp_event.h" +#include "esp_log.h" +#include "esp_eth_driver.h" +#include "esp_wifi.h" +#include "nvs_flash.h" +#include "esp_private/wifi.h" +#include "ethernet.hpp" +#include "esp_netif.h" +#include "esp_eth.h" + +// Component header +#include "Task.hpp" +#include "cliWrapper.hpp" +#include "udpSockets.hpp" +#include "tcpSockets.hpp" +#include "syslog.hpp" +#include "telnetServer.hpp" +#include "Mutex.hpp" +#include "Queue.hpp" +#include "Semaphore.hpp" +#include "Task.hpp" +#include "Artnet.h" +#include "Processor.hpp" +#include "nvsFunctions.hpp" +#include "mqttClient.hpp" +#include "watchFace.hpp" +#include "random.hpp" +#include "sad_DetectedServer.hpp" +#include "spit_object.hpp" +#include "spit_player.hpp" +#include "spit_type.hpp" +#include "spit_client.hpp" +#include +#include +#include +#include diff --git a/main/lightsMain.cpp b/main/lightsMain.cpp new file mode 100644 index 0000000..6195887 --- /dev/null +++ b/main/lightsMain.cpp @@ -0,0 +1,651 @@ +/** + * @author Gustice + * @brief ChaosWatch Application + */ + +// using Elite2D-EXMU +// Gledopto Series With Ethernet +// { // 13 +// 1, // eth_address, +// 5, // eth_power, +// 23, // eth_mdc, +// 33, // eth_mdio, +// ETH_PHY_LAN8720, // eth_type, +// ETH_CLOCK_GPIO0_IN // eth_clk_mode as input +// }, + +#include "config.hpp" +#include "lighsMain.hpp" + +using namespace spit; +using namespace std; + +string ClientName = "LeoLightsDev"; +string ProjectName = "LeoLightsProj"; + +extern "C" { // This switch allows the ROS C-implementation to find this main +void app_main(void); +} + +struct DeviceState { + Color color{0x80, 0x80, 0x80}; + vector counts; + vector times; +}; +struct StartCommand { + uint8_t pos; // 0 for none / Valid indexes 1 .. 6 +}; +static fos::Queue commandQ(2); + +DeviceState State; +static const char* TAG = "Light"; + +unique_ptr pShell; + +void onCommand(string name, Cli::CArgs args) { + ESP_LOGI(TAG, "Invoking: %s", name.c_str()); +} + +void startUpdate(Cli::CArgs args) { + ESP_LOGI(TAG, "Invoking Update"); + if (args.size() != 1) { + pShell->writeLine("Need URL to application"); + return; + } + // @ToDo: Start Update here +} + +void getConfig(Cli::CArgs args) { + if (args.size() != 1) { + pShell->writeLine("Needs string key for query"); + return; + } + auto key = args[0]; + auto& store = NvsApi::getInstance(); + auto val = store.getValue(key); + pShell->writeLine(key + ": " + val + ""); +} + +void setConfig(Cli::CArgs args) { + if (args.size() != 2) { + pShell->writeLine("Needs string key and value"); + return; + } + auto key = args[0]; + auto val = args[1]; + + auto& store = NvsApi::getInstance(); + pShell->writeLine(string{"Storing value = '"} + val + "'"); + store.setValue(key, val); +} + +void setLogServerConfig(Cli::CArgs args) { + ESP_LOGI(TAG, "on showLogServerConfig:"); + auto& store = NvsApi::getInstance(); + if (args.size() != 1) { + auto logger = store.getValue("syslogDst"); + pShell->writeLine(string{"syslog destination = '"} + logger + "'"); + pShell->writeLine("provide argument to change syslog server"); + return; + } + pShell->writeLine(string{"setting syslog destination to = '"} + args[0] + "'"); + store.setValue("syslogDst", args[0]); +} + +void testLights(Cli::CArgs args) { + ESP_LOGI(TAG, "on testLights:"); + commandQ.enqueue(make_unique()); +} + +void showProcessValues(Cli::CArgs args) { + pShell->writeLine("Process values:"); + char buff[256]; + + sprintf(buff, "%02x,%02x,%02x", State.color.getRed(), State.color.getGreen(), State.color.getBlue()); + std::stringstream ssC; + for (auto& c : State.counts) { + ssC << string{c} << ","; + } + std::stringstream ssT; + for (auto& t : State.times) { + ssT << string{t} << ","; + } + pShell->writeLine(string{" Color RGB: "} + buff); + pShell->writeLine(string{" LED-Counts: "} + ssC.str()); + pShell->writeLine(string{" Switch-Times: "} + ssT.str()); +} + +void setSyslogLevel(Cli::CArgs args) { + if (args.size() != 1) { + pShell->writeLine("Needs SyslogLevel Warning = 4, Notice = 5, Informational = 6, Debug = 7"); + return; + } + + try { + auto lvl = stoi(args[0]); + if (lvl >= (int)Syslog::Severity::Warning && lvl <= (int)Syslog::Severity::Debug) { + Syslog::setSeverity((Syslog::Severity)lvl); + } else { + pShell->writeLine("New level was not accepted"); + } + } catch (const exception& e) { + ESP_LOGI(TAG, "Failed to parse %s", args[0].c_str()); + } +} + +/** + * @brief Telnet socket via TCP/IP + * + * @details The ini-file for TeraTerm only changes CRReceive and CRSend values both to "=CR" + * @param arg ignored task argument + */ +static void telnetSocketTask(int addrFamily) { + ESP_LOGI(TAG, "Starting Telnet task"); + try { + TelnetServer telnet(addrFamily); + telnet.run( + [](TcpSession& session) { + pShell = make_unique(session, [](string n, Cli::CArgs a) { onCommand(n, a); }); + pShell->addBinding( + {"update", "Start OTA-Update-Process", + [](Cli::CArgs a) { startUpdate(a); }}); + pShell->addBinding( + {"setLogserver", "Set log server config ip", + [](Cli::CArgs a) { setLogServerConfig(a); }}); + pShell->addBinding( + {"setLogLevel", "Set severity log server", + [](Cli::CArgs a) { setSyslogLevel(a); }}); + + pShell->addBinding( + {"getcfg", "Get configuration value for key", + [](Cli::CArgs a) { getConfig(a); }}); + pShell->addBinding( + {"setcfg", "Set configuration value by key and value", + [](Cli::CArgs a) { setConfig(a); }}); + + pShell->addBinding( + {"setDyn", "set dynamics of movement (velocity,accelerations) as floats", + [](Cli::CArgs a) { testLights(a); }}); + pShell->addBinding( + {"showData", "Show process values that are input by DMX, MQTT and SPIT", + [](Cli::CArgs a) { showProcessValues(a); }}); + }, + [](TcpSession& session) { + pShell->process(); + Syslog::setupTask(session.getOther()); + }); + } catch (const exception& e) { + ESP_LOGE("TCP", "%s", e.what()); + } + vTaskDelete(NULL); +} + +void startShow(string data) { + ESP_LOGI(TAG, "StartShuffle command %s", data.c_str()); + try { + Syslog::logv(Syslog::Severity::Informational, "got Shuffle-Command"); + commandQ.enqueue(make_unique()); + } catch (const exception& e) { + ESP_LOGE(TAG, "Error to process data %s, with ex: %s", data.c_str(), e.what()); + } +} + +/** + * @brief DMX-Socket for ArtNet TCP/IP + * + * @details Tested with [Q Light Controller+](https://www.qlcplus.org/) + * @param args ignored task argument + */ +static void dmxSocketSenderTask(UdpSocket& socket) { + try { + ArtnetSender artnet(socket); + array data; + while (socket.isActive()) { + data[0]++; + artnet.send("", 0, data.data(), data.size()); + fos::Task::delayMs(500); + } + + } catch (const exception& e) { + ESP_LOGE("UDP", "%s", e.what()); + } +} + +fos::Semaphore etherActive; +fos::Semaphore spitProjectFinish; + +/** Event handler for IP_EVENT_ETH_GOT_IP */ +void got_ip_event_handler(void* arg, esp_event_base_t event_base, + int32_t event_id, void* event_data) { + ip_event_got_ip_t* event = (ip_event_got_ip_t*)event_data; + const esp_netif_ip_info_t* ip_info = &event->ip_info; + + ESP_LOGI(TAG, "Ethernet Got IP Address"); + ESP_LOGI(TAG, "~~~~~~~~~~~"); + ESP_LOGI(TAG, "ETHIP:" IPSTR, IP2STR(&ip_info->ip)); + ESP_LOGI(TAG, "ETHMASK:" IPSTR, IP2STR(&ip_info->netmask)); + ESP_LOGI(TAG, "ETHGW:" IPSTR, IP2STR(&ip_info->gw)); + ESP_LOGI(TAG, "~~~~~~~~~~~"); + etherActive.give(); +} + +static void initialize_ethernet(void) { + uint8_t eth_port_cnt = 0; + esp_eth_handle_t* eth_handles; + ESP_ERROR_CHECK(eth_init(ð_handles, ð_port_cnt)); + if (eth_port_cnt > 1) { + ESP_LOGW(TAG, "multiple Ethernet devices detected, the first initialized is to be used!"); + } + + static esp_eth_handle_t s_eth_handle = NULL; + s_eth_handle = eth_handles[0]; + free(eth_handles); + + // Initialize TCP/IP network interface aka the esp-netif (should be called only once in application) + ESP_ERROR_CHECK(esp_netif_init()); + // Create default event loop that running in background + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // Use ESP_NETIF_DEFAULT_ETH when just one Ethernet interface is used and you don't need to modify + // default esp-netif configuration parameters. + esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); + esp_netif_t* eth_netif = esp_netif_new(&cfg); + // Attach Ethernet driver to TCP/IP stack + ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(s_eth_handle))); + + ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event_handler, NULL)); + ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL)); + ESP_ERROR_CHECK(esp_eth_start(s_eth_handle)); +} + +namespace Effect { +extern const WaveForm_t OffsetPulseWave{gau8_offsetPulse, cu16_TemplateLength}; +extern const WaveForm_t InitSlopeWave{gau8_initSlope, cu16_TemplateLength}; +extern const WaveForm_t OffsetSlopeWave{gau8_offsetSlope, cu16_TemplateLength}; +extern const WaveForm_t FullSlopeWave{gau8_fullSlope, cu16_TemplateLength}; +extern const WaveForm_t FullPulseWave{gau8_fullPulse, cu16_TemplateLength}; + +const Macro macDemoStart = { + {64, 1, &InitSlopeWave, StateMachine::UpdateWave, 0xFF}, + {32, 1, StateMachine::UpdateIdle, 0xFF}, +}; + +const Macro macDemoPulse = { + {64, 1, &InitSlopeWave, StateMachine::UpdateWave, 0xFF}, + {32, 2, StateMachine::UpdateIdle, 0xFF}, + {64, 1, &OffsetPulseWave, StateMachine::UpdateWave, 0xFF}, +}; + +const Macro macDemoFlicker = { + {16, 1, &InitSlopeWave, StateMachine::UpdateWave, 0xFF}, + {32, 1, StateMachine::UpdateFlicker, 0xFF}, +}; +} // namespace Effect + +void LightingTask() { + ESP_LOGI(TAG, "Start Lighting"); + auto const& colorSource = State.color; + array processors{ + Effect::EffectProcessor(8), + Effect::EffectProcessor(8)}; + processors[0].setEffect(Effect::macDark, &colorSource, 0); + processors[1].setEffect(Effect::macDark, &colorSource, 0); + + const array PictoEffects{ + Effect::macDemoPulse, + Effect::macDemoPulse}; + + while (true) { + // ESP_LOGI(TAG, "Current color set r%i g%i b%i ", + // State.color.getRed(), State.color.getGreen(), State.color.getBlue()); + // processors[0].setEffect(Effect::macDark, &colorSource, 0); + // auto idx = 0; + // Syslog::logv(Syslog::Severity::Informational, "Running show"); + // processors[idx].setEffect(PictoEffects[idx], &colorSource, 0); + + // for (size_t i = 0; i < processors.size(); i++) { + // auto col = processors[i].tick(); + // } + // fos::Task::delayMs(40); + static int cnt{}; + fos::Task::delayMs(1000); + printf("running %d\n", cnt++); + } +} + +struct LocalSpitObj { + string iD; + string groupName; + string name; +}; +map ProvidedSpitActions; +map ExpectedObjects; + +void onServerEvent(SClient::ServerEvent event, const SadDetectedServer& p_DetectedServer) { + switch (event) { + case SClient::ServerEvent::Detect: + ESP_LOGI(TAG, "SERVER DETECTED"); + ESP_LOGI(TAG, " Name: %s", p_DetectedServer.getName().data()); + ESP_LOGI(TAG, " ID: %s", p_DetectedServer.getID().data()); + ESP_LOGI(TAG, " Computer: %s", p_DetectedServer.getComputerName().data()); + ESP_LOGI(TAG, " IP: %s", p_DetectedServer.getIP().data()); + ESP_LOGI(TAG, " Port: %s", p_DetectedServer.getPort().data()); + ESP_LOGI(TAG, " Addition: %s", p_DetectedServer.getAdditions().data()); + ESP_LOGI(TAG, " ClientIP: %s", p_DetectedServer.getClientIP().data()); + break; + case SClient::ServerEvent::Lost: + ESP_LOGW(TAG, "Server lost Name: %s IP: %s", p_DetectedServer.getName().data(), p_DetectedServer.getIP().data()); + + break; + case SClient::ServerEvent::Changed: + ESP_LOGW(TAG, "Server lost Name: %s IP: %s", p_DetectedServer.getName().data(), p_DetectedServer.getIP().data()); + break; + + default: + break; + } +} + +void onConnectionStatus(string p_ServerID, string p_ServerComputerName, string p_ServerName, int p_ServerStepTime, + SClient::Connection p_ConnectionStatus, SClient::Connection p_OldConnectionStatus) { + ESP_LOGI(TAG, "MySPITApp: eventConnectionStatus %d->%d ", (int)p_OldConnectionStatus, (int)p_ConnectionStatus); +} + +// The transfer of the server's SPITTypes and SPITObjects is finished. +void onProjectUpdate(SClient::ProjUpdate state, string projectId) { + switch (state) { + case SClient::ProjUpdate::ServerLoaded: + ESP_LOGI(TAG, "eventProjectReport ID: %s ", projectId.data()); + break; + case SClient::ProjUpdate::StartTransfer: + ESP_LOGI(TAG, "eventStartProjectTransfer"); + break; + case SClient::ProjUpdate::EndTransfer: + ESP_LOGI(TAG, "eventEndProjectTransfer"); + spitProjectFinish.give(); + break; + case SClient::ProjUpdate::ServerClosed: + ESP_LOGI(TAG, "eventProjectClosed ID: %s", projectId.data()); + // If we don't save SPITServer's project specific information + // perhaps close all SPITObjects and RemoteSPITObjects to avoid conflicts, when the SPITServer loads a new project + // SPITObject::clearSPITObjects(); + // SPITObject::clearSPITObjectsRemote(); + break; + + default: + break; + } +} + +bool onCheckSpitTypeVeto(bool knownType, bool createdByClient) { + return true; // Bypass veto + if (!knownType || !createdByClient) { // Don't allow foreign SPITTypes + return false; + } + return true; +} + +bool onCheckSpitObjectVeto(bool knownType) { + return true; // Bypass veto + if (!knownType) { // Don't allow objects of unregistered SPITTypes + return false; + } + + return true; +} + +// Called when the server has created, loaded, changed or deleted a (Remote) SPITType. +// Remote SPITType are used in Remote SPITObjects. A Remote SPITObject is used to remote control the SPITServer. +// May also occur when the server confirms a SPITType created or changed by the client. +void onSpitType(SClient::ObjectEvent event, bool isRemote, const SType& sType) { + if (isRemote) { + if (event == SClient::ObjectEvent::Created) { + ESP_LOGI(TAG, "NEW SPIT-REMOTE-Type ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); + auto typeName = sType.Name.get(); + if (ExpectedObjects.contains(typeName)) { + ExpectedObjects[typeName] = sType.getID(); + } + } else if (event == SClient::ObjectEvent::Changed) { + // we had to inform all Remote SPITObjects using the p_SPITType + // perhaps the SPITObject uses information from the SPITType + // SPITObject *ptrSPITObject; + // int intSPITObjectCount; + // intSPITObjectCount = SPITObject::getSPITObjectRemoteCount(); + // for (int intObject = 0; intObject < intSPITObjectCount; intObject++) { + // ptrSPITObject = SPITObject::getSPITObjectRemote(intObject); + // if (ptrSPITObject == nullptr) + // continue; + // if (ptrSPITObject->getSPITTypeID() == p_SPITType.getID()) { + // // call ecentSPITObjectRemote + // // onSpitObjectRemote(SClient::ObjectEvent::Changed, p_SPITType, *ptrSPITObject); + // } + // } + // ESP_LOGI(TAG, "SPIT-REMOTE-Type CHANGED ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); + } else if (event == SClient::ObjectEvent::Deleted) { + ESP_LOGD(TAG, "SPIT-REMOTE-Type Removed ID: %s GroupName: %s Name: %s\n", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data()); + } + } else { + if (event == SClient::ObjectEvent::Created) { + // Whenever you provide an action for this SPITType take this over in your code. + // call _SpitClient.setActionSPITType(sType.getID(), true); + // ESP_LOGI(TAG, "NEW SPIT-Type ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); + } else if (event == SClient::ObjectEvent::Changed) { + // ESP_LOGI(TAG, "SPIT-Type CHANGED ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); + } else if (event == SClient::ObjectEvent::Deleted) { + // ESP_LOGI(TAG, "SPIT-Type Removed ID: %s GroupName: %s Name: %s\n", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data()); + } + } +} + +// Called when the server has created, loaded or changed a SPITObject. +// A Remote SPITObject is used to remote control the SPITServer. +// May also occur when the server confirms a SPITObject created by the client, what the client should never do!!. +// false = server changed a existing SPITObject +// @param p_SPITType a reference to a copy of the associated SPITType instance. +// @param p_SPITObject a reference to a copy of the SPITObject instance. +void onSpitObject(SClient::ObjectEvent event, bool isRemote, const SType& sType, const SObject& sObject) { + if (isRemote) { + if (event == SClient::ObjectEvent::Created) { + // ESP_LOGI(TAG, "NEW SPIT-REMOTE-Object ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); + // This is a remote SPITObject which can be used to remote control the server. + // If you want you can list this for later use + } else if (event == SClient::ObjectEvent::Changed) { + // ESP_LOGI(TAG, "SPIT-REMOTE-Object CHANGED ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); + } else if (event == SClient::ObjectEvent::Deleted) { + ESP_LOGD(TAG, "SPIT-REMOTE-Object Removed ID: %s Name: %s\n", sObject.getID().data(), sObject.Name.get().data()); + // This is a remote SPITObject + // If you have listet this prior you should remove it from the list + } + } else { + if (event == SClient::ObjectEvent::Created) { + // ESP_LOGI(TAG, "NEW SPIT-Object ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); + // check your actions + // Whenever you provide an action for this SPITObject take this over in your code. + // call _SpitClient.setActionSPITObject(sObject.getID(), true); + + // for our little excample as described in loadClientProject: + // We have only actions for SPITTypes - means every SPITObject with the associated "Actionx"-SPITTypes will trigger the action when it is played + //-> no need to do something here, because setActionSPITType has been set prior and the SPITObject will automatically inherits this + // SPITObjects with other SPITTypes asscciated will trigger no actions + + } else if (event == SClient::ObjectEvent::Changed) { + // ESP_LOGI(TAG, "SPIT-Object CHANGED ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); + } else if (event == SClient::ObjectEvent::Deleted) { + ESP_LOGD(TAG, "SPIT-Object Removed ID: %s Name: %s\n", sObject.getID().data(), sObject.Name.get().data()); + // Check your actions + // take the removal of the SPITObject over in your code + + // CAUTION: Deleted SPITObjects can be added later again (perhaps per UNDO on the SPITServer) + //-> don't remove your assigned action immediately - keep it to reassign it later + } + } +} + +// SPITServer may use several methods for playing SPITObjects. +// e.g. liveSHOWsoftware has the timeline and jingles which plays objects, where the timeline the main player is. +// SPIT distinguishes between the different players. All eventPlay... functions contain the information which player plays the SPIT object. +void onPlay(string p_PlayerID, string p_PlayerName, char p_PlayerType, bool p_PlayerIsRunning, uint64_t p_TimeFrames) { + ESP_LOGD(TAG, "eventPlay Player: %s Runs: %d Time: %lld", p_PlayerName.data(), p_PlayerIsRunning, p_TimeFrames); + + switch (p_PlayerType) { + case SClient::PlayerType::PTYPE_MAIN: + // p_FrameTimes contains the position inside the server's project, we can use it like a timecode + // int intMilliseconds = (int)(((double)p_TimeFrames / (double)m_ServerFramesPerSecond) * 1000); + break; + case SClient::PlayerType::PTYPE_CONTINOUS: + // The olayer is playing only single Object sequences sometime during the project + break; + case SClient::PlayerType::PTYPE_DISCRETE: + // Only discrete values where submitted, e.g the fader position of remote faders. + break; + } +} + +void onPlaySpitType(string p_PlayerID, string spitTypeId, bool p_PlayerIsRunning, bool p_Started, bool p_Ended, double p_Value) { + // double doubleValue = ((double)p_PlayObject.getValue() / 1000000) * ((double)p_PlayObject.getValueFactor() / 1000000); + ESP_LOGD(TAG, "eventPlayObject Player: %s is running: %d Object Started: %d Stopped: %d", p_PlayerID.data(), p_PlayerIsRunning, p_Started, p_Ended); + // ESP_LOGD(TAG, " SPITType: %s SPITObject: %s Position: %lld Value: %f\n", spitTypeId.data(), p_SPITObject.getName().data(), p_PlayObject.getFrameInside(), doubleValue); + ESP_LOGD(TAG, " SPITType: %s Value: %f\n", spitTypeId.data(), p_Value); + + if (!ProvidedSpitActions.contains(spitTypeId)) { + ESP_LOGE(TAG, "SPIT ID %s is not accepted", spitTypeId.c_str()); + return; + } + if (p_Started == true) { + auto command = ProvidedSpitActions[spitTypeId]; + // ESP_LOGI(TAG, "Player: %s is running: %s Object", p_PlayerID.data(), spitTypeId.c_str()); + commandQ.enqueue(make_unique()); + Syslog::logv(Syslog::Severity::Informational, "Got SPIT command for move ot : r%d", command.pos); + } +} + +/** + * Called when a remote SpitObject which value function is changed by the server. + * This allows the Client to adjust its display for the remote command. + * @param p_SPITType the assigned remote SPITType of p_SPITObject + * @param p_SPITObject the remote SPITObject which is changed + * @param p_FrameInsideObject can be discarded + * @param p_Value the new value + */ +void onPlayRemoteObjectStatus(const SType& sType, const SObject& sObject, uint64_t frameInsideObject, int value) { + ESP_EARLY_LOGD(TAG, "eventPlayRemoteObjectStatus RemoteSPITType: %s RemoteSPITObject: %s Position: %lld Value: %d", sType.Name.get().data(), sObject.Name.get().data(), frameInsideObject, value); +} + +// void SpitClientApp() { +// ESP_LOGI(TAG, "Starting SPIT task"); + +// auto& provided = SpitTypes.Provided; +// auto& expected = SpitTypes.Expected; +// vector ProvidedObj; +// ProvidedSpitActions[provided.startShuffle] = MoveCommand{.type = MoveType::Shuffle, .pos = 0}; +// ProvidedObj.push_back(LocalSpitObj{.iD = provided.startShuffle, .groupName = provided.group, .name = provided.startShuffle}); +// ExpectedObjects[expected.changingPos] = ""; +// for (size_t i = 0; i < 6; i++) { +// ProvidedSpitActions[provided.moveToX[i]] = MoveCommand{.type = MoveType::StepTo, .pos = (uint8_t)(i + 1)}; +// ProvidedObj.push_back(LocalSpitObj{.iD = provided.moveToX[i], .groupName = provided.group, .name = provided.moveToX[i]}); +// ExpectedObjects[expected.inPosX[i]] = ""; +// } + +// auto ip = spit::Socket::getAdaptersInfo(); +// spit::Socket socket(ip); +// SClient spitClient(ClientName, ProjectName, socket, +// { +// .onServerEvent = onServerEvent, +// .onConnectionStatus = onConnectionStatus, +// .onProjectUpdate = onProjectUpdate, +// .onSpitType = onSpitType, +// .onSpitObject = onSpitObject, +// .onPlay = onPlay, +// .onPlaySpitType = onPlaySpitType, +// .onPlayRemoteObjectStatus = onPlayRemoteObjectStatus, +// .onCheckSpitTypeVeto = onCheckSpitTypeVeto, +// .onCheckSpitObjectVeto = onCheckSpitObjectVeto, +// }); + +// for (auto& obj : ProvidedObj) { +// SType* ptrSPITType1Fix = spitClient.createTypeFixed(obj.iD, obj.groupName, obj.name, 0, 0); +// spitClient.setActionType(ptrSPITType1Fix->getID(), true); // should be executed whenever a SPITObject with this SPITType is played +// } +// spitClient.clientProjectLoaded(); +// spitClient.startServerAutoDetect(); + +// spitProjectFinish.take(); +// string serverProjectID = spitClient.getServerProjectID(); +// string serverProjectName = spitClient.getClientProjectName(); +// int serverFramesPerSecond = spitClient.getServerFramesPerSecond(); // set when the server reports his project, see eventProjectReport +// ESP_LOGI(TAG, "eventProjectReport ID: %s Name: %s FramesPerSecond: %d", serverProjectID.data(), serverProjectName.data(), serverFramesPerSecond); + +// for (auto&& obj : ExpectedObjects) { +// if (obj.second == "") { +// ESP_LOGE(TAG, "No matching Remote ID for %s", obj.first.c_str()); +// } +// } + +// while (true) { +// auto res = responseQueue.dequeue(); +// if (res->type == ResponseType::StartMoving) { +// spitClient.sendRemoteObject(ExpectedObjects[expected.changingPos]); +// } else if (res->type == ResponseType::StepAt) { +// spitClient.sendRemoteObject(ExpectedObjects[expected.inPosX[res->pos - 1]]); +// } +// } +// } + +// void MqttThread() { +// ESP_LOGI(TAG, "Starting MQTT task"); + +// auto& store = NvsApi::getInstance(); +// auto broker = store.getValue(ConfigKeys.mqttBroker); +// if (broker != "") { +// ESP_LOGI(TAG, "Initializing MQTT"); +// auto prefix = store.getValue(ConfigKeys.mqttPrefix); +// auto& client = MqttClient::init(broker, prefix); +// if (!client.isRunning()) { +// client.addSubscription(MqttTopics.moveTo, [](string d) { startShuffle(d); }); +// client.start(); +// } + +// while (true) { +// auto res = responseQueue.dequeue(); +// if (res->type == ResponseType::StartMoving) { +// client.publish(MqttTopics.showDone, ""); +// } else if (res->type == ResponseType::StepAt) { +// client.publish(MqttTopics.inPosition, to_string(res->pos)); +// } +// } +// } +// } + +void app_main(void) { + initialize_ethernet(); + NvsApi::init(); + static fos::Task lightingTask(LightingTask, "Lights", 0x8000uL); + + etherActive.take(); + ESP_LOGI(TAG, "Start net services"); + + // static int ipVersion = AF_INET; + // static fos::TaskT telnetSocket(telnetSocketTask, ipVersion, "tcpServer", 4096); + // static fos::Task spit(SpitClientApp, "SpitClient"); + // // static fos::Task mqtt(MqttThread, "MqttEndpoint"); + // // static Task otaUpdate(SystemFunctions::OtaTaskEntry, "otaTask", 8192); + + auto& store = NvsApi::getInstance(); + auto logger = store.getValue(ConfigKeys.syslogDest); + if (logger != "") { + Syslog::setupTask(IpAdr{logger, 0}); + } + + ESP_LOGI(TAG, "Configuration"); + ESP_LOGI(TAG, "%s: %s", ConfigKeys.syslogDest, store.getValue(ConfigKeys.syslogDest).c_str()); + ESP_LOGI(TAG, "%s: %s", ConfigKeys.mqttBroker, store.getValue(ConfigKeys.mqttBroker).c_str()); + ESP_LOGI(TAG, "%s: %s", ConfigKeys.mqttPrefix, store.getValue(ConfigKeys.mqttPrefix).c_str()); + ESP_LOGI(TAG, "%s: %s", ConfigKeys.spitServer, store.getValue(ConfigKeys.spitServer).c_str()); + + while (true) { + fos::Task::delayMs(5000); + Syslog::logv(Syslog::Severity::Notice, "Min heap size: %u / FreeHeap %lu", + heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT), + esp_get_free_heap_size()); + } +} diff --git a/makefile b/makefile new file mode 100644 index 0000000..8f13d29 --- /dev/null +++ b/makefile @@ -0,0 +1,20 @@ + +.PHONY: start test clean + +start: + ./startCode + +# testConfig: buildTestOnTarget + +test: + cmake -DTgtEnv:STRING=Target -S . -B buildTestOnTarget + cmake --build buildTestOnTarget --parallel + idf.py -B buildTestOnTarget -p /dev/ttyACM0 flash monitor + +uTest: + cmake -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_C_COMPILER:FILEPATH=/usr/bin/gcc -DCMAKE_CXX_COMPILER:FILEPATH=/usr/bin/g++ -DTgtEnv:STRING=Host -S . -B buildUTest + cmake --build buildUTest --parallel + ctest --test-dir buildUTest + +clean: + rm -rf build buildTestOnTarget diff --git a/managed_components/frozen-eye__esp_telnet/.component_hash b/managed_components/frozen-eye__esp_telnet/.component_hash new file mode 100644 index 0000000..51bed66 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/.component_hash @@ -0,0 +1 @@ +e00a16d6372fa31df46b32e330847d6e09af2ec184d8c7cac4d183b3daac02e5 \ No newline at end of file diff --git a/managed_components/frozen-eye__esp_telnet/.gitignore b/managed_components/frozen-eye__esp_telnet/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/managed_components/frozen-eye__esp_telnet/AUTHORS b/managed_components/frozen-eye__esp_telnet/AUTHORS new file mode 100644 index 0000000..329745c --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/AUTHORS @@ -0,0 +1,12 @@ +Original Telnet Server code is based on the version created and written by the following people: + +- Sean Middleditch +- Jack Kelly +- Katherine Flavel +- Daniel Loffgren (https://github.com/RyuKojiro) + +and taken from here: [https://github.com/seanmiddleditch/libtelnet](https://github.com/seanmiddleditch/libtelnet) + +Midificatons are performed by: + +- FrozenEye (https://github.com/frozen-eye) diff --git a/managed_components/frozen-eye__esp_telnet/CHECKSUMS.json b/managed_components/frozen-eye__esp_telnet/CHECKSUMS.json new file mode 100644 index 0000000..6f2b39b --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/CHECKSUMS.json @@ -0,0 +1 @@ +{"version": "1.0", "algorithm": "sha256", "created_at": "2025-05-21T14:59:16.171166+00:00", "files": [{"path": "CMakeLists.txt", "size": 303, "hash": "70671f714f52ac79607119703f1ddab4a0b325f25e8981afe7c93baf7af7cd34"}, {"path": "LICENSE", "size": 1063, "hash": "068129b238141ba0ebcb2158402d73408c798f4d8c5a42f4dafaa41a002b9ea2"}, {"path": "AUTHORS", "size": 452, "hash": "277d45436ae96f30f6ee4b2dcde2661e679f2ded4f790cbee0c726d5f13c8dba"}, {"path": "idf_component.yml", "size": 179, "hash": "39f2a2bd0c96ab90610389356a2cf39842364514e2a69c1241e6ecd84b269d72"}, {"path": "Kconfig", "size": 1114, "hash": "ccc9fc290f957c953bdb3bbc8d1079efe7c5fdb82f10d6c7a2dc9973b4046bd7"}, {"path": ".gitignore", "size": 0, "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"path": "src/libtelnet.h", "size": 21611, "hash": "0798f3706642fdd4520923914ddfa59fb9ded670b7716877c80fa852153b8521"}, {"path": "src/server.c", "size": 9226, "hash": "bf90b0df126da4a16d79c626724ce715fdfb8015dd7ea0030b8a6cbf072c947f"}, {"path": "src/libtelnet.c", "size": 44236, "hash": "de15620edc5492aaf6c273ce4c9181cb99490a2153e56db7d7c3f96f3ea7e2b4"}, {"path": "include/telnet/server.h", "size": 1493, "hash": "ebe6d17a86a1c56c97e263bd4d468cffc5d95a3ca810e79388b2a9598f89f8ba"}]} \ No newline at end of file diff --git a/managed_components/frozen-eye__esp_telnet/CMakeLists.txt b/managed_components/frozen-eye__esp_telnet/CMakeLists.txt new file mode 100644 index 0000000..db10799 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.10) + +idf_component_register( + INCLUDE_DIRS + include + src + . + SRCS + src/libtelnet.c + src/server.c + REQUIRES + PRIV_REQUIRES +) + +# target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror) +# target_compile_features(${COMPONENT_LIB} PRIVATE cxx_std_17) diff --git a/managed_components/frozen-eye__esp_telnet/Kconfig b/managed_components/frozen-eye__esp_telnet/Kconfig new file mode 100644 index 0000000..bcad548 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/Kconfig @@ -0,0 +1,41 @@ +menu "Telnet Server" + + config TELNET_SERVER_DEFAULT_PORT + int "Default Telnet Server Port" + default 23 + help + Default Telnet Server Port Number + + config TELNET_SERVER_MAX_CONNECTIONS + int "Maximum Telnet Server Connections" + default 5 + help + Maximum Number of Telnet Server Connections + + config TELNET_SERVER_STACK_SIZE + int "Telnet Server Stack Size" + default 4096 + help + Telnet Server Stack Size + + config TELNET_SERVER_TASK_PRIORITY + int "Telnet Server Task Priority" + range 1 25 + default 24 + help + Telnet Server Task Priority. In case of high events rate, this parameter could be changed up to (configMAX_PRIORITIES-1). + + config TELNET_SERVER_TASK_CORE + int "Telnet Server Task Core" + range 0 1 + default 0 + help + Telnet Server Task Core + + config TELNET_SERVER_REDIRECT_LOGS + int "Redirect Logs to Telnet Server" + range 0 1 + default 0 + help + Redirect Logs to Telnet Server +endmenu diff --git a/managed_components/frozen-eye__esp_telnet/LICENSE b/managed_components/frozen-eye__esp_telnet/LICENSE new file mode 100644 index 0000000..3390e92 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/LICENSE @@ -0,0 +1,21 @@ +Copyright 2024 Frozen Eye + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/managed_components/frozen-eye__esp_telnet/idf_component.yml b/managed_components/frozen-eye__esp_telnet/idf_component.yml new file mode 100644 index 0000000..95eb8e5 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/idf_component.yml @@ -0,0 +1,6 @@ +dependencies: + idf: + version: '>=4.0' +description: esp_telnet is a small implementation of the telnet server. +url: https://github.com/frozen-eye/esp_telnet.git +version: 0.0.2 diff --git a/managed_components/frozen-eye__esp_telnet/include/telnet/server.h b/managed_components/frozen-eye__esp_telnet/include/telnet/server.h new file mode 100644 index 0000000..43715a1 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/include/telnet/server.h @@ -0,0 +1,55 @@ +#pragma once + +#include "sdkconfig.h" + +#include + +static const telnet_telopt_t default_telopts[] = { + {TELNET_TELOPT_COMPRESS2, TELNET_WILL, TELNET_DO}, {TELNET_TELOPT_ZMP, TELNET_WILL, TELNET_DO}, + {TELNET_TELOPT_MSSP, TELNET_WILL, TELNET_DONT}, {TELNET_TELOPT_NEW_ENVIRON, TELNET_WILL, TELNET_DONT}, + {TELNET_TELOPT_TTYPE, TELNET_WILL, TELNET_DONT}, {-1, 0, 0}}; + +struct user_t { + char* name; + int sock; + telnet_t* telnet; + char linebuf[255]; + int linepos; +}; + +#ifdef __cplusplus +extern "C" { +#endif + +struct telnet_server_config { + int port; + int stack_size; + int task_priority; + int task_core; + int redirect_logs; + int max_connections; + const telnet_telopt_t *telnet_opts; +}; + +/** + * @brief Telnet Server Default Configuration + * + */ +#define TELNET_SERVER_DEFAULT_CONFIG \ +{ \ + .port = CONFIG_TELNET_SERVER_DEFAULT_PORT, \ + .stack_size = CONFIG_TELNET_SERVER_STACK_SIZE, \ + .task_priority = CONFIG_TELNET_SERVER_TASK_PRIORITY, \ + .task_core = CONFIG_TELNET_SERVER_TASK_CORE, \ + .redirect_logs = CONFIG_TELNET_SERVER_REDIRECT_LOGS, \ + .max_connections = CONFIG_TELNET_SERVER_MAX_CONNECTIONS, \ + .telnet_opts = default_telopts, \ +} + +typedef struct telnet_server_config telnet_server_config_t; + +esp_err_t telnet_server_create(telnet_server_config_t* config); + +#ifdef __cplusplus +} +#endif diff --git a/managed_components/frozen-eye__esp_telnet/src/libtelnet.c b/managed_components/frozen-eye__esp_telnet/src/libtelnet.c new file mode 100644 index 0000000..3d09e53 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/src/libtelnet.c @@ -0,0 +1,1668 @@ +/* + * libtelnet - TELNET protocol handling library + * + * Sean Middleditch + * sean@sourcemud.org + * + * The author or authors of this code dedicate any and all copyright interest + * in this code to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and successors. We + * intend this dedication to be an overt act of relinquishment in perpetuity of + * all present and future rights to this code under copyright law. + */ + +#include +#include +#include +#include +#include +#include + +/* Win32 compatibility */ +#if defined(_WIN32) +# define vsnprintf _vsnprintf +# define __func__ __FUNCTION__ +# define ZLIB_WINAPI 1 +# if defined(_MSC_VER) +/* va_copy() is directly supported starting in Visual Studio 2013 + * https://msdn.microsoft.com/en-us/library/kb57fad8(v=vs.110).aspx + * https://msdn.microsoft.com/en-us/library/kb57fad8(v=vs.120).aspx + */ +# if _MSC_VER <= 1700 +# define va_copy(dest, src) (dest = src) +# endif +# endif +#endif + +#if defined(HAVE_ZLIB) +# include +#endif + +#include "libtelnet.h" + +/* inlinable functions */ +#if defined(__GNUC__) || __STDC_VERSION__ >= 199901L +# define INLINE __inline__ +#else +# define INLINE +#endif + +/* helper for Q-method option tracking */ +#define Q_US(q) ((q).state & 0x0F) +#define Q_HIM(q) (((q).state & 0xF0) >> 4) +#define Q_MAKE(us,him) ((us) | ((him) << 4)) + +/* helper for the negotiation routines */ +#define NEGOTIATE_EVENT(telnet,cmd,opt) \ + ev.type = (cmd); \ + ev.neg.telopt = (opt); \ + (telnet)->eh((telnet), &ev, (telnet)->ud); + +/* telnet state codes */ +enum telnet_state_t { + TELNET_STATE_DATA = 0, + TELNET_STATE_EOL, + TELNET_STATE_IAC, + TELNET_STATE_WILL, + TELNET_STATE_WONT, + TELNET_STATE_DO, + TELNET_STATE_DONT, + TELNET_STATE_SB, + TELNET_STATE_SB_DATA, + TELNET_STATE_SB_DATA_IAC +}; +typedef enum telnet_state_t telnet_state_t; + +/* telnet state tracker */ +struct telnet_t { + /* user data */ + void *ud; + /* telopt support table */ + const telnet_telopt_t *telopts; + /* event handler */ + telnet_event_handler_t eh; +#if defined(HAVE_ZLIB) + /* zlib (mccp2) compression */ + z_stream *z; +#endif + /* RFC1143 option negotiation states */ + struct telnet_rfc1143_t *q; + /* sub-request buffer */ + char *buffer; + /* current size of the buffer */ + size_t buffer_size; + /* current buffer write position (also length of buffer data) */ + size_t buffer_pos; + /* current state */ + enum telnet_state_t state; + /* option flags */ + unsigned char flags; + /* current subnegotiation telopt */ + unsigned char sb_telopt; + /* length of RFC1143 queue */ + unsigned int q_size; + /* number of entries in RFC1143 queue */ + unsigned int q_cnt; +}; + +/* RFC1143 option negotiation state */ +typedef struct telnet_rfc1143_t { + unsigned char telopt; + unsigned char state; +} telnet_rfc1143_t; + +/* RFC1143 state names */ +#define Q_NO 0 +#define Q_YES 1 +#define Q_WANTNO 2 +#define Q_WANTYES 3 +#define Q_WANTNO_OP 4 +#define Q_WANTYES_OP 5 + +/* telnet NVT EOL sequences */ +static const char CRLF[] = { '\r', '\n' }; +static const char CRNUL[] = { '\r', '\0' }; + +/* buffer sizes */ +static const size_t _buffer_sizes[] = { 0, 512, 2048, 8192, 16384, }; +static const size_t _buffer_sizes_count = sizeof(_buffer_sizes) / + sizeof(_buffer_sizes[0]); + +/* RFC1143 option negotiation state table allocation quantum */ +#define Q_BUFFER_GROWTH_QUANTUM 4 + +/* error generation function */ +static telnet_error_t _error(telnet_t *telnet, unsigned line, + const char* func, telnet_error_t err, int fatal, const char *fmt, + ...) { + telnet_event_t ev; + char buffer[512]; + va_list va; + + /* format informational text */ + va_start(va, fmt); + vsnprintf(buffer, sizeof(buffer), fmt, va); + va_end(va); + + /* send error event to the user */ + ev.type = fatal ? TELNET_EV_ERROR : TELNET_EV_WARNING; + ev.error.file = __FILE__; + ev.error.func = func; + ev.error.line = line; + ev.error.msg = buffer; + telnet->eh(telnet, &ev, telnet->ud); + + return err; +} + +#if defined(HAVE_ZLIB) +/* initialize the zlib box for a telnet box; if deflate is non-zero, it + * initializes zlib for delating (compression), otherwise for inflating + * (decompression). returns TELNET_EOK on success, something else on + * failure. + */ +telnet_error_t _init_zlib(telnet_t *telnet, int deflate, int err_fatal) { + z_stream *z; + int rs; + + /* if compression is already enabled, fail loudly */ + if (telnet->z != 0) + return _error(telnet, __LINE__, __func__, TELNET_EBADVAL, + err_fatal, "cannot initialize compression twice"); + + /* allocate zstream box */ + if ((z= (z_stream *)calloc(1, sizeof(z_stream))) == 0) + return _error(telnet, __LINE__, __func__, TELNET_ENOMEM, err_fatal, + "malloc() failed: %s", strerror(errno)); + + /* initialize */ + if (deflate) { + if ((rs = deflateInit(z, Z_DEFAULT_COMPRESSION)) != Z_OK) { + free(z); + return _error(telnet, __LINE__, __func__, TELNET_ECOMPRESS, + err_fatal, "deflateInit() failed: %s", zError(rs)); + } + telnet->flags |= TELNET_PFLAG_DEFLATE; + } else { + if ((rs = inflateInit(z)) != Z_OK) { + free(z); + return _error(telnet, __LINE__, __func__, TELNET_ECOMPRESS, + err_fatal, "inflateInit() failed: %s", zError(rs)); + } + telnet->flags &= ~TELNET_PFLAG_DEFLATE; + } + + telnet->z = z; + + return TELNET_EOK; +} +#endif /* defined(HAVE_ZLIB) */ + +/* push bytes out, compressing them first if need be */ +static void _send(telnet_t *telnet, const char *buffer, + size_t size) { + telnet_event_t ev; + +#if defined(HAVE_ZLIB) + /* if we have a deflate (compression) zlib box, use it */ + if (telnet->z != 0 && telnet->flags & TELNET_PFLAG_DEFLATE) { + char deflate_buffer[1024]; + int rs; + + /* initialize z state */ + telnet->z->next_in = (unsigned char *)buffer; + telnet->z->avail_in = (unsigned int)size; + telnet->z->next_out = (unsigned char *)deflate_buffer; + telnet->z->avail_out = sizeof(deflate_buffer); + + /* deflate until buffer exhausted and all output is produced */ + while (telnet->z->avail_in > 0 || telnet->z->avail_out == 0) { + /* compress */ + if ((rs = deflate(telnet->z, Z_SYNC_FLUSH)) != Z_OK) { + _error(telnet, __LINE__, __func__, TELNET_ECOMPRESS, 1, + "deflate() failed: %s", zError(rs)); + deflateEnd(telnet->z); + free(telnet->z); + telnet->z = 0; + break; + } + + /* send event */ + ev.type = TELNET_EV_SEND; + ev.data.buffer = deflate_buffer; + ev.data.size = sizeof(deflate_buffer) - telnet->z->avail_out; + telnet->eh(telnet, &ev, telnet->ud); + + /* prepare output buffer for next run */ + telnet->z->next_out = (unsigned char *)deflate_buffer; + telnet->z->avail_out = sizeof(deflate_buffer); + } + + /* do not continue with remaining code */ + return; + } +#endif /* defined(HAVE_ZLIB) */ + + ev.type = TELNET_EV_SEND; + ev.data.buffer = buffer; + ev.data.size = size; + telnet->eh(telnet, &ev, telnet->ud); +} + +/* to send bags of unsigned chars */ +#define _sendu(t, d, s) _send((t), (const char*)(d), (s)) + +/* check if we support a particular telopt; if us is non-zero, we + * check if we (local) supports it, otherwise we check if he (remote) + * supports it. return non-zero if supported, zero if not supported. + */ +static INLINE int _check_telopt(telnet_t *telnet, unsigned char telopt, + int us) { + int i; + + /* if we have no telopts table, we obviously don't support it */ + if (telnet->telopts == 0) + return 0; + + /* loop until found or end marker (us and him both 0) */ + for (i = 0; telnet->telopts[i].telopt != -1; ++i) { + if (telnet->telopts[i].telopt == telopt) { + if (us && telnet->telopts[i].us == TELNET_WILL) + return 1; + else if (!us && telnet->telopts[i].him == TELNET_DO) + return 1; + else + return 0; + } + } + + /* not found, so not supported */ + return 0; +} + +/* retrieve RFC1143 option state */ +static INLINE telnet_rfc1143_t _get_rfc1143(telnet_t *telnet, + unsigned char telopt) { + telnet_rfc1143_t empty; + unsigned int i; + + /* search for entry */ + for (i = 0; i != telnet->q_cnt; ++i) { + if (telnet->q[i].telopt == telopt) { + return telnet->q[i]; + } + } + + /* not found, return empty value */ + empty.telopt = telopt; + empty.state = 0; + return empty; +} + +/* save RFC1143 option state */ +static INLINE void _set_rfc1143(telnet_t *telnet, unsigned char telopt, + char us, char him) { + telnet_rfc1143_t *qtmp; + unsigned int i; + + /* search for entry */ + for (i = 0; i != telnet->q_cnt; ++i) { + if (telnet->q[i].telopt == telopt) { + telnet->q[i].state = Q_MAKE(us,him); + if (telopt != TELNET_TELOPT_BINARY) + return; + telnet->flags &= ~(TELNET_FLAG_TRANSMIT_BINARY | + TELNET_FLAG_RECEIVE_BINARY); + if (us == Q_YES) + telnet->flags |= TELNET_FLAG_TRANSMIT_BINARY; + if (him == Q_YES) + telnet->flags |= TELNET_FLAG_RECEIVE_BINARY; + return; + } + } + + /* we're going to need to track state for it, so grow the queue + * by 4 (four) elements and put the telopt into it; bail on allocation + * error. we go by four because it seems like a reasonable guess as + * to the number of enabled options for most simple code, and it + * allows for an acceptable number of reallocations for complex code. + */ + + /* Did we reach the end of the table? */ + if (telnet->q_cnt >= telnet->q_size) { + /* Expand the size */ + if ((qtmp = (telnet_rfc1143_t *)realloc(telnet->q, + sizeof(telnet_rfc1143_t) * + (telnet->q_size + Q_BUFFER_GROWTH_QUANTUM))) == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "realloc() failed: %s", strerror(errno)); + return; + } + memset(&qtmp[telnet->q_size], 0, sizeof(telnet_rfc1143_t) * + Q_BUFFER_GROWTH_QUANTUM); + telnet->q = qtmp; + telnet->q_size += Q_BUFFER_GROWTH_QUANTUM; + } + /* Add entry to end of table */ + telnet->q[telnet->q_cnt].telopt = telopt; + telnet->q[telnet->q_cnt].state = Q_MAKE(us, him); + ++telnet->q_cnt; +} + +/* send negotiation bytes */ +static INLINE void _send_negotiate(telnet_t *telnet, unsigned char cmd, + unsigned char telopt) { + unsigned char bytes[3]; + bytes[0] = TELNET_IAC; + bytes[1] = cmd; + bytes[2] = telopt; + _sendu(telnet, bytes, 3); +} + +/* negotiation handling magic for RFC1143 */ +static void _negotiate(telnet_t *telnet, unsigned char telopt) { + telnet_event_t ev; + telnet_rfc1143_t q; + + /* in PROXY mode, just pass it thru and do nothing */ + if (telnet->flags & TELNET_FLAG_PROXY) { + switch ((int)telnet->state) { + case TELNET_STATE_WILL: + NEGOTIATE_EVENT(telnet, TELNET_EV_WILL, telopt); + break; + case TELNET_STATE_WONT: + NEGOTIATE_EVENT(telnet, TELNET_EV_WONT, telopt); + break; + case TELNET_STATE_DO: + NEGOTIATE_EVENT(telnet, TELNET_EV_DO, telopt); + break; + case TELNET_STATE_DONT: + NEGOTIATE_EVENT(telnet, TELNET_EV_DONT, telopt); + break; + } + return; + } + + /* lookup the current state of the option */ + q = _get_rfc1143(telnet, telopt); + + /* start processing... */ + switch ((int)telnet->state) { + /* request to enable option on remote end or confirm DO */ + case TELNET_STATE_WILL: + switch (Q_HIM(q)) { + case Q_NO: + if (_check_telopt(telnet, telopt, 0)) { + _set_rfc1143(telnet, telopt, Q_US(q), Q_YES); + _send_negotiate(telnet, TELNET_DO, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_WILL, telopt); + } else + _send_negotiate(telnet, TELNET_DONT, telopt); + break; + case Q_WANTNO: + _set_rfc1143(telnet, telopt, Q_US(q), Q_NO); + NEGOTIATE_EVENT(telnet, TELNET_EV_WONT, telopt); + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "DONT answered by WILL"); + break; + case Q_WANTNO_OP: + _set_rfc1143(telnet, telopt, Q_US(q), Q_YES); + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "DONT answered by WILL"); + break; + case Q_WANTYES: + _set_rfc1143(telnet, telopt, Q_US(q), Q_YES); + NEGOTIATE_EVENT(telnet, TELNET_EV_WILL, telopt); + break; + case Q_WANTYES_OP: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTNO); + _send_negotiate(telnet, TELNET_DONT, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_WILL, telopt); + break; + } + break; + + /* request to disable option on remote end, confirm DONT, reject DO */ + case TELNET_STATE_WONT: + switch (Q_HIM(q)) { + case Q_YES: + _set_rfc1143(telnet, telopt, Q_US(q), Q_NO); + _send_negotiate(telnet, TELNET_DONT, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_WONT, telopt); + break; + case Q_WANTNO: + _set_rfc1143(telnet, telopt, Q_US(q), Q_NO); + NEGOTIATE_EVENT(telnet, TELNET_EV_WONT, telopt); + break; + case Q_WANTNO_OP: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTYES); + _send_negotiate(telnet, TELNET_DO, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_WONT, telopt); + break; + case Q_WANTYES: + case Q_WANTYES_OP: + _set_rfc1143(telnet, telopt, Q_US(q), Q_NO); + break; + } + break; + + /* request to enable option on local end or confirm WILL */ + case TELNET_STATE_DO: + switch (Q_US(q)) { + case Q_NO: + if (_check_telopt(telnet, telopt, 1)) { + _set_rfc1143(telnet, telopt, Q_YES, Q_HIM(q)); + _send_negotiate(telnet, TELNET_WILL, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_DO, telopt); + } else + _send_negotiate(telnet, TELNET_WONT, telopt); + break; + case Q_WANTNO: + _set_rfc1143(telnet, telopt, Q_NO, Q_HIM(q)); + NEGOTIATE_EVENT(telnet, TELNET_EV_DONT, telopt); + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "WONT answered by DO"); + break; + case Q_WANTNO_OP: + _set_rfc1143(telnet, telopt, Q_YES, Q_HIM(q)); + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "WONT answered by DO"); + break; + case Q_WANTYES: + _set_rfc1143(telnet, telopt, Q_YES, Q_HIM(q)); + NEGOTIATE_EVENT(telnet, TELNET_EV_DO, telopt); + break; + case Q_WANTYES_OP: + _set_rfc1143(telnet, telopt, Q_WANTNO, Q_HIM(q)); + _send_negotiate(telnet, TELNET_WONT, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_DO, telopt); + break; + } + break; + + /* request to disable option on local end, confirm WONT, reject WILL */ + case TELNET_STATE_DONT: + switch (Q_US(q)) { + case Q_YES: + _set_rfc1143(telnet, telopt, Q_NO, Q_HIM(q)); + _send_negotiate(telnet, TELNET_WONT, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_DONT, telopt); + break; + case Q_WANTNO: + _set_rfc1143(telnet, telopt, Q_NO, Q_HIM(q)); + NEGOTIATE_EVENT(telnet, TELNET_EV_DONT, telopt); + break; + case Q_WANTNO_OP: + _set_rfc1143(telnet, telopt, Q_WANTYES, Q_HIM(q)); + _send_negotiate(telnet, TELNET_WILL, telopt); + NEGOTIATE_EVENT(telnet, TELNET_EV_DONT, telopt); + break; + case Q_WANTYES: + case Q_WANTYES_OP: + _set_rfc1143(telnet, telopt, Q_NO, Q_HIM(q)); + break; + } + break; + } +} + +/* process an ENVIRON/NEW-ENVIRON subnegotiation buffer + * + * the algorithm and approach used here is kind of a hack, + * but it reduces the number of memory allocations we have + * to make. + * + * we copy the bytes back into the buffer, starting at the very + * beginning, which makes it easy to handle the ENVIRON ESC + * escape mechanism as well as ensure the variable name and + * value strings are NUL-terminated, all while fitting inside + * of the original buffer. + */ +static int _environ_telnet(telnet_t *telnet, unsigned char type, + char* buffer, size_t size) { + telnet_event_t ev; + struct telnet_environ_t *values = 0; + char *c, *last, *out; + size_t index, count; + + /* if we have no data, just pass it through */ + if (size == 0) { + return 0; + } + + /* first byte must be a valid command */ + if ((unsigned)buffer[0] != TELNET_ENVIRON_SEND && + (unsigned)buffer[0] != TELNET_ENVIRON_IS && + (unsigned)buffer[0] != TELNET_ENVIRON_INFO) { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "telopt %d subneg has invalid command", type); + return 0; + } + + /* store ENVIRON command */ + ev.environ.cmd = buffer[0]; + + /* if we have no arguments, send an event with no data end return */ + if (size == 1) { + /* no list of variables given */ + ev.environ.values = 0; + ev.environ.size = 0; + + /* invoke event with our arguments */ + ev.type = TELNET_EV_ENVIRON; + telnet->eh(telnet, &ev, telnet->ud); + + return 0; + } + + /* very second byte must be VAR or USERVAR, if present */ + if ((unsigned)buffer[1] != TELNET_ENVIRON_VAR && + (unsigned)buffer[1] != TELNET_ENVIRON_USERVAR) { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "telopt %d subneg missing variable type", type); + return 0; + } + + /* ensure last byte is not an escape byte (makes parsing later easier) */ + if ((unsigned)buffer[size - 1] == TELNET_ENVIRON_ESC) { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "telopt %d subneg ends with ESC", type); + return 0; + } + + /* count arguments; each valid entry starts with VAR or USERVAR */ + count = 0; + for (c = buffer + 1; c < buffer + size; ++c) { + if (*c == TELNET_ENVIRON_VAR || *c == TELNET_ENVIRON_USERVAR) { + ++count; + } else if (*c == TELNET_ENVIRON_ESC) { + /* skip the next byte */ + ++c; + } + } + + /* allocate argument array, bail on error */ + if ((values = (struct telnet_environ_t *)calloc(count, + sizeof(struct telnet_environ_t))) == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "calloc() failed: %s", strerror(errno)); + return 0; + } + + /* parse argument array strings */ + out = buffer; + c = buffer + 1; + for (index = 0; index != count; ++index) { + /* remember the variable type (will be VAR or USERVAR) */ + values[index].type = *c++; + + /* scan until we find an end-marker, and buffer up unescaped + * bytes into our buffer */ + last = out; + while (c < buffer + size) { + /* stop at the next variable or at the value */ + if ((unsigned)*c == TELNET_ENVIRON_VAR || + (unsigned)*c == TELNET_ENVIRON_VALUE || + (unsigned)*c == TELNET_ENVIRON_USERVAR) { + break; + } + + /* buffer next byte (taking into account ESC) */ + if (*c == TELNET_ENVIRON_ESC) { + ++c; + } + + *out++ = *c++; + } + *out++ = '\0'; + + /* store the variable name we have just received */ + values[index].var = last; + values[index].value = ""; + + /* if we got a value, find the next end marker and + * store the value; otherwise, store empty string */ + if (c < buffer + size && *c == TELNET_ENVIRON_VALUE) { + ++c; + last = out; + while (c < buffer + size) { + /* stop when we find the start of the next variable */ + if ((unsigned)*c == TELNET_ENVIRON_VAR || + (unsigned)*c == TELNET_ENVIRON_USERVAR) { + break; + } + + /* buffer next byte (taking into account ESC) */ + if (*c == TELNET_ENVIRON_ESC) { + ++c; + } + + *out++ = *c++; + } + *out++ = '\0'; + + /* store the variable value */ + values[index].value = last; + } + } + + /* pass values array and count to event */ + ev.environ.values = values; + ev.environ.size = count; + + /* invoke event with our arguments */ + ev.type = TELNET_EV_ENVIRON; + telnet->eh(telnet, &ev, telnet->ud); + + /* clean up */ + free(values); + return 0; +} + +/* process an MSSP subnegotiation buffer */ +static int _mssp_telnet(telnet_t *telnet, char* buffer, size_t size) { + telnet_event_t ev; + struct telnet_environ_t *values; + char *var = 0; + char *c, *last, *out; + size_t i, count; + unsigned char next_type; + + /* if we have no data, just pass it through */ + if (size == 0) { + return 0; + } + + /* first byte must be a VAR */ + if ((unsigned)buffer[0] != TELNET_MSSP_VAR) { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "MSSP subnegotiation has invalid data"); + return 0; + } + + /* count the arguments, any part that starts with VALUE */ + for (count = 0, i = 0; i != size; ++i) { + if ((unsigned)buffer[i] == TELNET_MSSP_VAL) { + ++count; + } + } + + /* allocate argument array, bail on error */ + if ((values = (struct telnet_environ_t *)calloc(count, + sizeof(struct telnet_environ_t))) == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "calloc() failed: %s", strerror(errno)); + return 0; + } + + ev.mssp.values = values; + ev.mssp.size = count; + + /* allocate strings in argument array */ + out = last = buffer; + next_type = buffer[0]; + for (i = 0, c = buffer + 1; c < buffer + size;) { + /* search for end marker */ + while (c < buffer + size && (unsigned)*c != TELNET_MSSP_VAR && + (unsigned)*c != TELNET_MSSP_VAL) { + *out++ = *c++; + } + *out++ = '\0'; + + /* if it's a variable name, just store the name for now */ + if (next_type == TELNET_MSSP_VAR) { + var = last; + } else if (next_type == TELNET_MSSP_VAL && var != 0) { + values[i].var = var; + values[i].value = last; + ++i; + } else { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "invalid MSSP subnegotiation data"); + free(values); + return 0; + } + + /* remember our next type and increment c for next loop run */ + last = out; + next_type = *c++; + } + + /* invoke event with our arguments */ + ev.type = TELNET_EV_MSSP; + telnet->eh(telnet, &ev, telnet->ud); + + /* clean up */ + free(values); + + return 0; +} + +/* parse ZMP command subnegotiation buffers */ +static int _zmp_telnet(telnet_t *telnet, const char* buffer, size_t size) { + telnet_event_t ev; + char **argv; + const char *c; + size_t i, argc; + + /* make sure this is a valid ZMP buffer */ + if (size == 0 || buffer[size - 1] != 0) { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "incomplete ZMP frame"); + return 0; + } + + /* count arguments */ + for (argc = 0, c = buffer; c != buffer + size; ++argc) + c += strlen(c) + 1; + + /* allocate argument array, bail on error */ + if ((argv = (char **)calloc(argc, sizeof(char *))) == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "calloc() failed: %s", strerror(errno)); + return 0; + } + + /* populate argument array */ + for (i = 0, c = buffer; i != argc; ++i) { + argv[i] = (char *)c; + c += strlen(c) + 1; + } + + /* invoke event with our arguments */ + ev.type = TELNET_EV_ZMP; + ev.zmp.argv = (const char**)argv; + ev.zmp.argc = argc; + telnet->eh(telnet, &ev, telnet->ud); + + /* clean up */ + free(argv); + return 0; +} + +/* parse TERMINAL-TYPE command subnegotiation buffers */ +static int _ttype_telnet(telnet_t *telnet, const char* buffer, size_t size) { + telnet_event_t ev; + + /* make sure request is not empty */ + if (size == 0) { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "incomplete TERMINAL-TYPE request"); + return 0; + } + + /* make sure request has valid command type */ + if (buffer[0] != TELNET_TTYPE_IS && + buffer[0] != TELNET_TTYPE_SEND) { + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "TERMINAL-TYPE request has invalid type"); + return 0; + } + + /* send proper event */ + if (buffer[0] == TELNET_TTYPE_IS) { + char *name; + + /* allocate space for name */ + if ((name = (char *)malloc(size)) == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "malloc() failed: %s", strerror(errno)); + return 0; + } + memcpy(name, buffer + 1, size - 1); + name[size - 1] = '\0'; + + ev.type = TELNET_EV_TTYPE; + ev.ttype.cmd = TELNET_TTYPE_IS; + ev.ttype.name = name; + telnet->eh(telnet, &ev, telnet->ud); + + /* clean up */ + free(name); + } else { + ev.type = TELNET_EV_TTYPE; + ev.ttype.cmd = TELNET_TTYPE_SEND; + ev.ttype.name = 0; + telnet->eh(telnet, &ev, telnet->ud); + } + + return 0; +} + +/* process a subnegotiation buffer; return non-zero if the current buffer + * must be aborted and reprocessed due to COMPRESS2 being activated + */ +static int _subnegotiate(telnet_t *telnet) { + telnet_event_t ev; + + /* standard subnegotiation event */ + ev.type = TELNET_EV_SUBNEGOTIATION; + ev.sub.telopt = telnet->sb_telopt; + ev.sub.buffer = telnet->buffer; + ev.sub.size = telnet->buffer_pos; + telnet->eh(telnet, &ev, telnet->ud); + + switch (telnet->sb_telopt) { +#if defined(HAVE_ZLIB) + /* received COMPRESS2 begin marker, setup our zlib box and + * start handling the compressed stream if it's not already. + */ + case TELNET_TELOPT_COMPRESS2: + if (_init_zlib(telnet, 0, 1) != TELNET_EOK) + return 0; + + /* notify app that compression was enabled */ + ev.type = TELNET_EV_COMPRESS; + ev.compress.state = 1; + telnet->eh(telnet, &ev, telnet->ud); + return 1; +#endif /* defined(HAVE_ZLIB) */ + + /* specially handled subnegotiation telopt types */ + case TELNET_TELOPT_ZMP: + return _zmp_telnet(telnet, telnet->buffer, telnet->buffer_pos); + case TELNET_TELOPT_TTYPE: + return _ttype_telnet(telnet, telnet->buffer, telnet->buffer_pos); + case TELNET_TELOPT_ENVIRON: + case TELNET_TELOPT_NEW_ENVIRON: + return _environ_telnet(telnet, telnet->sb_telopt, telnet->buffer, + telnet->buffer_pos); + case TELNET_TELOPT_MSSP: + return _mssp_telnet(telnet, telnet->buffer, telnet->buffer_pos); + default: + return 0; + } +} + +/* initialize a telnet state tracker */ +telnet_t *telnet_init(const telnet_telopt_t *telopts, + telnet_event_handler_t eh, unsigned char flags, void *user_data) { + /* allocate structure */ + struct telnet_t *telnet = (telnet_t*)calloc(1, sizeof(telnet_t)); + if (telnet == 0) + return 0; + + /* initialize data */ + telnet->ud = user_data; + telnet->telopts = telopts; + telnet->eh = eh; + telnet->flags = flags; + + return telnet; +} + +/* free up any memory allocated by a state tracker */ +void telnet_free(telnet_t *telnet) { + /* free sub-request buffer */ + if (telnet->buffer != 0) { + free(telnet->buffer); + telnet->buffer = 0; + telnet->buffer_size = 0; + telnet->buffer_pos = 0; + } + +#if defined(HAVE_ZLIB) + /* free zlib box */ + if (telnet->z != 0) { + if (telnet->flags & TELNET_PFLAG_DEFLATE) + deflateEnd(telnet->z); + else + inflateEnd(telnet->z); + free(telnet->z); + telnet->z = 0; + } +#endif /* defined(HAVE_ZLIB) */ + + /* free RFC1143 queue */ + if (telnet->q) { + free(telnet->q); + telnet->q = NULL; + telnet->q_size = 0; + telnet->q_cnt = 0; + } + + /* free the telnet structure itself */ + free(telnet); +} + +/* push a byte into the telnet buffer */ +static telnet_error_t _buffer_byte(telnet_t *telnet, + unsigned char byte) { + char *new_buffer; + size_t i; + + /* check if we're out of room */ + if (telnet->buffer_pos == telnet->buffer_size) { + /* find the next buffer size */ + for (i = 0; i != _buffer_sizes_count; ++i) { + if (_buffer_sizes[i] == telnet->buffer_size) { + break; + } + } + + /* overflow -- can't grow any more */ + if (i >= _buffer_sizes_count - 1) { + _error(telnet, __LINE__, __func__, TELNET_EOVERFLOW, 0, + "subnegotiation buffer size limit reached"); + return TELNET_EOVERFLOW; + } + + /* (re)allocate buffer */ + new_buffer = (char *)realloc(telnet->buffer, _buffer_sizes[i + 1]); + if (new_buffer == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "realloc() failed"); + return TELNET_ENOMEM; + } + + telnet->buffer = new_buffer; + telnet->buffer_size = _buffer_sizes[i + 1]; + } + + /* push the byte, all set */ + telnet->buffer[telnet->buffer_pos++] = byte; + return TELNET_EOK; +} + +static void _process(telnet_t *telnet, const char *buffer, size_t size) { + telnet_event_t ev; + unsigned char byte; + size_t i, start; + for (i = start = 0; i != size; ++i) { + byte = buffer[i]; + switch (telnet->state) { + /* regular data */ + case TELNET_STATE_DATA: + /* on an IAC byte, pass through all pending bytes and + * switch states */ + if (byte == TELNET_IAC) { + if (i != start) { + ev.type = TELNET_EV_DATA; + ev.data.buffer = buffer + start; + ev.data.size = i - start; + telnet->eh(telnet, &ev, telnet->ud); + } + telnet->state = TELNET_STATE_IAC; + } else if (byte == '\r' && + (telnet->flags & TELNET_FLAG_NVT_EOL) && + !(telnet->flags & TELNET_FLAG_RECEIVE_BINARY)) { + if (i != start) { + ev.type = TELNET_EV_DATA; + ev.data.buffer = buffer + start; + ev.data.size = i - start; + telnet->eh(telnet, &ev, telnet->ud); + } + telnet->state = TELNET_STATE_EOL; + } + break; + + /* NVT EOL to be translated */ + case TELNET_STATE_EOL: + if (byte != '\n') { + byte = '\r'; + ev.type = TELNET_EV_DATA; + ev.data.buffer = (char*)&byte; + ev.data.size = 1; + telnet->eh(telnet, &ev, telnet->ud); + byte = buffer[i]; + } + /* any byte following '\r' other than '\n' or '\0' is invalid, + * so pass both \r and the byte */ + start = i; + if (byte == '\0') + ++start; + /* state update */ + telnet->state = TELNET_STATE_DATA; + break; + + /* IAC command */ + case TELNET_STATE_IAC: + switch (byte) { + /* subnegotiation */ + case TELNET_SB: + telnet->state = TELNET_STATE_SB; + break; + /* negotiation commands */ + case TELNET_WILL: + telnet->state = TELNET_STATE_WILL; + break; + case TELNET_WONT: + telnet->state = TELNET_STATE_WONT; + break; + case TELNET_DO: + telnet->state = TELNET_STATE_DO; + break; + case TELNET_DONT: + telnet->state = TELNET_STATE_DONT; + break; + /* IAC escaping */ + case TELNET_IAC: + /* event */ + ev.type = TELNET_EV_DATA; + ev.data.buffer = (char*)&byte; + ev.data.size = 1; + telnet->eh(telnet, &ev, telnet->ud); + + /* state update */ + start = i + 1; + telnet->state = TELNET_STATE_DATA; + break; + /* some other command */ + default: + /* event */ + ev.type = TELNET_EV_IAC; + ev.iac.cmd = byte; + telnet->eh(telnet, &ev, telnet->ud); + + /* state update */ + start = i + 1; + telnet->state = TELNET_STATE_DATA; + } + break; + + /* negotiation commands */ + case TELNET_STATE_WILL: + case TELNET_STATE_WONT: + case TELNET_STATE_DO: + case TELNET_STATE_DONT: + _negotiate(telnet, byte); + start = i + 1; + telnet->state = TELNET_STATE_DATA; + break; + + /* subnegotiation -- determine subnegotiation telopt */ + case TELNET_STATE_SB: + telnet->sb_telopt = byte; + telnet->buffer_pos = 0; + telnet->state = TELNET_STATE_SB_DATA; + break; + + /* subnegotiation -- buffer bytes until end request */ + case TELNET_STATE_SB_DATA: + /* IAC command in subnegotiation -- either IAC SE or IAC IAC */ + if (byte == TELNET_IAC) { + telnet->state = TELNET_STATE_SB_DATA_IAC; + } else if (telnet->sb_telopt == TELNET_TELOPT_COMPRESS && byte == TELNET_WILL) { + /* In 1998 MCCP used TELOPT 85 and the protocol defined an invalid + * subnegotiation sequence (IAC SB 85 WILL SE) to start compression. + * Subsequently MCCP version 2 was created in 2000 using TELOPT 86 + * and a valid subnegotiation (IAC SB 86 IAC SE). libtelnet for now + * just captures and discards MCCPv1 sequences. + */ + start = i + 2; + telnet->state = TELNET_STATE_DATA; + /* buffer the byte, or bail if we can't */ + } else if (_buffer_byte(telnet, byte) != TELNET_EOK) { + start = i + 1; + telnet->state = TELNET_STATE_DATA; + } + break; + + /* IAC escaping inside a subnegotiation */ + case TELNET_STATE_SB_DATA_IAC: + switch (byte) { + /* end subnegotiation */ + case TELNET_SE: + /* return to default state */ + start = i + 1; + telnet->state = TELNET_STATE_DATA; + + /* process subnegotiation */ + if (_subnegotiate(telnet) != 0) { + /* any remaining bytes in the buffer are compressed. + * we have to re-invoke telnet_recv to get those + * bytes inflated and abort trying to process the + * remaining compressed bytes in the current _process + * buffer argument + */ + telnet_recv(telnet, &buffer[start], size - start); + return; + } + break; + /* escaped IAC byte */ + case TELNET_IAC: + /* push IAC into buffer */ + if (_buffer_byte(telnet, TELNET_IAC) != + TELNET_EOK) { + start = i + 1; + telnet->state = TELNET_STATE_DATA; + } else { + telnet->state = TELNET_STATE_SB_DATA; + } + break; + /* something else -- protocol error. attempt to process + * content in subnegotiation buffer, then evaluate the + * given command as an IAC code. + */ + default: + _error(telnet, __LINE__, __func__, TELNET_EPROTOCOL, 0, + "unexpected byte after IAC inside SB: %d", + byte); + + /* enter IAC state */ + start = i + 1; + telnet->state = TELNET_STATE_IAC; + + /* process subnegotiation; see comment in + * TELNET_STATE_SB_DATA_IAC about invoking telnet_recv() + */ + if (_subnegotiate(telnet) != 0) { + telnet_recv(telnet, &buffer[start], size - start); + return; + } else { + /* recursive call to get the current input byte processed + * as a regular IAC command. we could use a goto, but + * that would be gross. + */ + _process(telnet, (char *)&byte, 1); + } + break; + } + break; + } + } + + /* pass through any remaining bytes */ + if (telnet->state == TELNET_STATE_DATA && i != start) { + ev.type = TELNET_EV_DATA; + ev.data.buffer = buffer + start; + ev.data.size = i - start; + telnet->eh(telnet, &ev, telnet->ud); + } +} + +/* push a bytes into the state tracker */ +void telnet_recv(telnet_t *telnet, const char *buffer, + size_t size) { +#if defined(HAVE_ZLIB) + /* if we have an inflate (decompression) zlib stream, use it */ + if (telnet->z != 0 && !(telnet->flags & TELNET_PFLAG_DEFLATE)) { + char inflate_buffer[1024]; + int rs; + + /* initialize zlib state */ + telnet->z->next_in = (unsigned char*)buffer; + telnet->z->avail_in = (unsigned int)size; + telnet->z->next_out = (unsigned char *)inflate_buffer; + telnet->z->avail_out = sizeof(inflate_buffer); + + /* inflate until buffer exhausted and all output is produced */ + while (telnet->z->avail_in > 0 || telnet->z->avail_out == 0) { + /* reset output buffer */ + + /* decompress */ + rs = inflate(telnet->z, Z_SYNC_FLUSH); + + /* process the decompressed bytes on success */ + if (rs == Z_OK || rs == Z_STREAM_END) + _process(telnet, inflate_buffer, sizeof(inflate_buffer) - + telnet->z->avail_out); + else + _error(telnet, __LINE__, __func__, TELNET_ECOMPRESS, 1, + "inflate() failed: %s", zError(rs)); + + /* prepare output buffer for next run */ + telnet->z->next_out = (unsigned char *)inflate_buffer; + telnet->z->avail_out = sizeof(inflate_buffer); + + /* on error (or on end of stream) disable further inflation */ + if (rs != Z_OK) { + telnet_event_t ev; + + /* disable compression */ + inflateEnd(telnet->z); + free(telnet->z); + telnet->z = 0; + + /* send event */ + ev.type = TELNET_EV_COMPRESS; + ev.compress.state = 0; + telnet->eh(telnet, &ev, telnet->ud); + + break; + } + } + + /* COMPRESS2 is not negotiated, just process */ + } else +#endif /* defined(HAVE_ZLIB) */ + _process(telnet, buffer, size); +} + +/* send an iac command */ +void telnet_iac(telnet_t *telnet, unsigned char cmd) { + unsigned char bytes[2]; + bytes[0] = TELNET_IAC; + bytes[1] = cmd; + _sendu(telnet, bytes, 2); +} + +/* send negotiation */ +void telnet_negotiate(telnet_t *telnet, unsigned char cmd, + unsigned char telopt) { + telnet_rfc1143_t q; + + /* if we're in proxy mode, just send it now */ + if (telnet->flags & TELNET_FLAG_PROXY) { + unsigned char bytes[3]; + bytes[0] = TELNET_IAC; + bytes[1] = cmd; + bytes[2] = telopt; + _sendu(telnet, bytes, 3); + return; + } + + /* get current option states */ + q = _get_rfc1143(telnet, telopt); + + switch (cmd) { + /* advertise willingess to support an option */ + case TELNET_WILL: + switch (Q_US(q)) { + case Q_NO: + _set_rfc1143(telnet, telopt, Q_WANTYES, Q_HIM(q)); + _send_negotiate(telnet, TELNET_WILL, telopt); + break; + case Q_WANTNO: + _set_rfc1143(telnet, telopt, Q_WANTNO_OP, Q_HIM(q)); + break; + case Q_WANTYES_OP: + _set_rfc1143(telnet, telopt, Q_WANTYES, Q_HIM(q)); + break; + } + break; + + /* force turn-off of locally enabled option */ + case TELNET_WONT: + switch (Q_US(q)) { + case Q_YES: + _set_rfc1143(telnet, telopt, Q_WANTNO, Q_HIM(q)); + _send_negotiate(telnet, TELNET_WONT, telopt); + break; + case Q_WANTYES: + _set_rfc1143(telnet, telopt, Q_WANTYES_OP, Q_HIM(q)); + break; + case Q_WANTNO_OP: + _set_rfc1143(telnet, telopt, Q_WANTNO, Q_HIM(q)); + break; + } + break; + + /* ask remote end to enable an option */ + case TELNET_DO: + switch (Q_HIM(q)) { + case Q_NO: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTYES); + _send_negotiate(telnet, TELNET_DO, telopt); + break; + case Q_WANTNO: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTNO_OP); + break; + case Q_WANTYES_OP: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTYES); + break; + } + break; + + /* demand remote end disable an option */ + case TELNET_DONT: + switch (Q_HIM(q)) { + case Q_YES: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTNO); + _send_negotiate(telnet, TELNET_DONT, telopt); + break; + case Q_WANTYES: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTYES_OP); + break; + case Q_WANTNO_OP: + _set_rfc1143(telnet, telopt, Q_US(q), Q_WANTNO); + break; + } + break; + } +} + +/* send non-command data (escapes IAC bytes) */ +void telnet_send(telnet_t *telnet, const char *buffer, + size_t size) { + size_t i, l; + + for (l = i = 0; i != size; ++i) { + /* dump prior portion of text, send escaped bytes */ + if (buffer[i] == (char)TELNET_IAC) { + /* dump prior text if any */ + if (i != l) { + _send(telnet, buffer + l, i - l); + } + l = i + 1; + + /* send escape */ + telnet_iac(telnet, TELNET_IAC); + } + } + + /* send whatever portion of buffer is left */ + if (i != l) { + _send(telnet, buffer + l, i - l); + } +} + +/* send non-command text (escapes IAC bytes and does NVT translation) */ +void telnet_send_text(telnet_t *telnet, const char *buffer, + size_t size) { + size_t i, l; + + for (l = i = 0; i != size; ++i) { + /* dump prior portion of text, send escaped bytes */ + if (buffer[i] == (char)TELNET_IAC) { + /* dump prior text if any */ + if (i != l) { + _send(telnet, buffer + l, i - l); + } + l = i + 1; + + /* send escape */ + telnet_iac(telnet, TELNET_IAC); + } + /* special characters if not in BINARY mode */ + else if (!(telnet->flags & TELNET_FLAG_TRANSMIT_BINARY) && + (buffer[i] == '\r' || buffer[i] == '\n')) { + /* dump prior portion of text */ + if (i != l) { + _send(telnet, buffer + l, i - l); + } + l = i + 1; + + /* automatic translation of \r -> CRNUL */ + if (buffer[i] == '\r') { + _send(telnet, CRNUL, 2); + } + /* automatic translation of \n -> CRLF */ + else { + _send(telnet, CRLF, 2); + } + } + } + + /* send whatever portion of buffer is left */ + if (i != l) { + _send(telnet, buffer + l, i - l); + } +} + +/* send subnegotiation header */ +void telnet_begin_sb(telnet_t *telnet, unsigned char telopt) { + unsigned char sb[3]; + sb[0] = TELNET_IAC; + sb[1] = TELNET_SB; + sb[2] = telopt; + _sendu(telnet, sb, 3); +} + + +/* send complete subnegotiation */ +void telnet_subnegotiation(telnet_t *telnet, unsigned char telopt, + const char *buffer, size_t size) { + unsigned char bytes[5]; + bytes[0] = TELNET_IAC; + bytes[1] = TELNET_SB; + bytes[2] = telopt; + bytes[3] = TELNET_IAC; + bytes[4] = TELNET_SE; + + _sendu(telnet, bytes, 3); + telnet_send(telnet, buffer, size); + _sendu(telnet, bytes + 3, 2); + +#if defined(HAVE_ZLIB) + /* if we're a proxy and we just sent the COMPRESS2 marker, we must + * make sure all further data is compressed if not already. + */ + if (telnet->flags & TELNET_FLAG_PROXY && + telopt == TELNET_TELOPT_COMPRESS2) { + telnet_event_t ev; + + if (_init_zlib(telnet, 1, 1) != TELNET_EOK) + return; + + /* notify app that compression was enabled */ + ev.type = TELNET_EV_COMPRESS; + ev.compress.state = 1; + telnet->eh(telnet, &ev, telnet->ud); + } +#endif /* defined(HAVE_ZLIB) */ +} + +void telnet_begin_compress2(telnet_t *telnet) { +#if defined(HAVE_ZLIB) + static const unsigned char compress2[] = { TELNET_IAC, TELNET_SB, + TELNET_TELOPT_COMPRESS2, TELNET_IAC, TELNET_SE }; + + telnet_event_t ev; + + /* attempt to create output stream first, bail if we can't */ + if (_init_zlib(telnet, 1, 0) != TELNET_EOK) + return; + + /* send compression marker. we send directly to the event handler + * instead of passing through _send because _send would result in + * the compress marker itself being compressed. + */ + ev.type = TELNET_EV_SEND; + ev.data.buffer = (const char*)compress2; + ev.data.size = sizeof(compress2); + telnet->eh(telnet, &ev, telnet->ud); + + /* notify app that compression was successfully enabled */ + ev.type = TELNET_EV_COMPRESS; + ev.compress.state = 1; + telnet->eh(telnet, &ev, telnet->ud); +#else + (void)telnet; +#endif /* defined(HAVE_ZLIB) */ +} + +/* send formatted data with \r and \n translation in addition to IAC IAC */ +int telnet_vprintf(telnet_t *telnet, const char *fmt, va_list va) { + va_list va_temp; + char buffer[1024]; + char *output = buffer; + unsigned int rs, i, l; + + /* format */ + va_copy(va_temp, va); + rs = vsnprintf(buffer, sizeof(buffer), fmt, va_temp); + va_end(va_temp); + + if (rs >= sizeof(buffer)) { + output = (char*)malloc(rs + 1); + if (output == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "malloc() failed: %s", strerror(errno)); + return -1; + } + + va_copy(va_temp, va); + rs = vsnprintf(output, rs + 1, fmt, va_temp); + va_end(va_temp); + } + + /* send */ + for (l = i = 0; i != rs; ++i) { + /* special characters */ + if (output[i] == (char)TELNET_IAC || output[i] == '\r' || + output[i] == '\n') { + /* dump prior portion of text */ + if (i != l) + _send(telnet, output + l, i - l); + l = i + 1; + + /* IAC -> IAC IAC */ + if (output[i] == (char)TELNET_IAC) + telnet_iac(telnet, TELNET_IAC); + /* automatic translation of \r -> CRNUL */ + else if (output[i] == '\r') + _send(telnet, CRNUL, 2); + /* automatic translation of \n -> CRLF */ + else if (output[i] == '\n') + _send(telnet, CRLF, 2); + } + } + + /* send whatever portion of output is left */ + if (i != l) { + _send(telnet, output + l, i - l); + } + + /* free allocated memory, if any */ + if (output != buffer) { + free(output); + } + + return rs; +} + +/* see telnet_vprintf */ +int telnet_printf(telnet_t *telnet, const char *fmt, ...) { + va_list va; + int rs; + + va_start(va, fmt); + rs = telnet_vprintf(telnet, fmt, va); + va_end(va); + + return rs; +} + +/* send formatted data through telnet_send */ +int telnet_raw_vprintf(telnet_t *telnet, const char *fmt, va_list va) { + va_list va_temp; + char buffer[1024]; + char *output = buffer; + unsigned int rs; + + /* format; allocate more space if necessary */ + va_copy(va_temp, va); + rs = vsnprintf(buffer, sizeof(buffer), fmt, va_temp); + va_end(va_temp); + + if (rs >= sizeof(buffer)) { + output = (char*)malloc(rs + 1); + if (output == 0) { + _error(telnet, __LINE__, __func__, TELNET_ENOMEM, 0, + "malloc() failed: %s", strerror(errno)); + return -1; + } + + va_copy(va_temp, va); + rs = vsnprintf(output, rs + 1, fmt, va_temp); + va_end(va_temp); + } + + /* send out the formatted data */ + telnet_send(telnet, output, rs); + + /* release allocated memory, if any */ + if (output != buffer) { + free(output); + } + + return rs; +} + +/* see telnet_raw_vprintf */ +int telnet_raw_printf(telnet_t *telnet, const char *fmt, ...) { + va_list va; + int rs; + + va_start(va, fmt); + rs = telnet_raw_vprintf(telnet, fmt, va); + va_end(va); + + return rs; +} + +/* begin NEW-ENVIRON subnegotation */ +void telnet_begin_newenviron(telnet_t *telnet, unsigned char cmd) { + telnet_begin_sb(telnet, TELNET_TELOPT_NEW_ENVIRON); + telnet_send(telnet, (const char *)&cmd, 1); +} + +/* send a NEW-ENVIRON value */ +void telnet_newenviron_value(telnet_t *telnet, unsigned char type, + const char *string) { + telnet_send(telnet, (const char*)&type, 1); + + if (string != 0) { + telnet_send(telnet, string, strlen(string)); + } +} + +/* send TERMINAL-TYPE SEND command */ +void telnet_ttype_send(telnet_t *telnet) { + static const unsigned char SEND[] = { TELNET_IAC, TELNET_SB, + TELNET_TELOPT_TTYPE, TELNET_TTYPE_SEND, TELNET_IAC, TELNET_SE }; + _sendu(telnet, SEND, sizeof(SEND)); +} + +/* send TERMINAL-TYPE IS command */ +void telnet_ttype_is(telnet_t *telnet, const char* ttype) { + static const unsigned char IS[] = { TELNET_IAC, TELNET_SB, + TELNET_TELOPT_TTYPE, TELNET_TTYPE_IS }; + if (!ttype) { + ttype = "NVT"; + } + _sendu(telnet, IS, sizeof(IS)); + _send(telnet, ttype, strlen(ttype)); + telnet_finish_sb(telnet); +} + +/* send ZMP data */ +void telnet_send_zmp(telnet_t *telnet, size_t argc, const char **argv) { + size_t i; + + /* ZMP header */ + telnet_begin_zmp(telnet, argv[0]); + + /* send out each argument, including trailing NUL byte */ + for (i = 1; i != argc; ++i) + telnet_zmp_arg(telnet, argv[i]); + + /* ZMP footer */ + telnet_finish_zmp(telnet); +} + +/* send ZMP data using varargs */ +void telnet_send_vzmpv(telnet_t *telnet, va_list va) { + const char* arg; + + /* ZMP header */ + telnet_begin_sb(telnet, TELNET_TELOPT_ZMP); + + /* send out each argument, including trailing NUL byte */ + while ((arg = va_arg(va, const char *)) != 0) + telnet_zmp_arg(telnet, arg); + + /* ZMP footer */ + telnet_finish_zmp(telnet); +} + +/* see telnet_send_vzmpv */ +void telnet_send_zmpv(telnet_t *telnet, ...) { + va_list va; + + va_start(va, telnet); + telnet_send_vzmpv(telnet, va); + va_end(va); +} + +/* begin a ZMP command */ +void telnet_begin_zmp(telnet_t *telnet, const char *cmd) { + telnet_begin_sb(telnet, TELNET_TELOPT_ZMP); + telnet_zmp_arg(telnet, cmd); +} + +/* send a ZMP argument */ +void telnet_zmp_arg(telnet_t *telnet, const char* arg) { + telnet_send(telnet, arg, strlen(arg) + 1); +} diff --git a/managed_components/frozen-eye__esp_telnet/src/libtelnet.h b/managed_components/frozen-eye__esp_telnet/src/libtelnet.h new file mode 100644 index 0000000..e3e6048 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/src/libtelnet.h @@ -0,0 +1,691 @@ +/*! + * \brief libtelnet - TELNET protocol handling library + * + * SUMMARY: + * + * libtelnet is a library for handling the TELNET protocol. It includes + * routines for parsing incoming data from a remote peer as well as formatting + * data to send to the remote peer. + * + * libtelnet uses a callback-oriented API, allowing application-specific + * handling of various events. The callback system is also used for buffering + * outgoing protocol data, allowing the application to maintain control over + * the actual socket connection. + * + * Features supported include the full TELNET protocol, Q-method option + * negotiation, ZMP, MCCP2, MSSP, and NEW-ENVIRON. + * + * CONFORMS TO: + * + * RFC854 - http://www.faqs.org/rfcs/rfc854.html + * RFC855 - http://www.faqs.org/rfcs/rfc855.html + * RFC1091 - http://www.faqs.org/rfcs/rfc1091.html + * RFC1143 - http://www.faqs.org/rfcs/rfc1143.html + * RFC1408 - http://www.faqs.org/rfcs/rfc1408.html + * RFC1572 - http://www.faqs.org/rfcs/rfc1572.html + * + * LICENSE: + * + * The author or authors of this code dedicate any and all copyright interest + * in this code to the public domain. We make this dedication for the benefit + * of the public at large and to the detriment of our heirs and successors. We + * intend this dedication to be an overt act of relinquishment in perpetuity of + * all present and future rights to this code under copyright law. + * + * \file libtelnet.h + * + * \version 0.23 + * + * \author Sean Middleditch + */ + +#if !defined(LIBTELNET_INCLUDE) +#define LIBTELNET_INCLUDE 1 + +/* standard C headers necessary for the libtelnet API */ +#include +#include + +/* C++ support */ +#if defined(__cplusplus) +extern "C" { +#endif + +/* printf type checking feature in GCC and some other compilers */ +#if __GNUC__ +# define TELNET_GNU_PRINTF(f,a) __attribute__((format(printf, f, a))) /*!< internal helper */ +# define TELNET_GNU_SENTINEL __attribute__((sentinel)) /*!< internal helper */ +#else +# define TELNET_GNU_PRINTF(f,a) /*!< internal helper */ +# define TELNET_GNU_SENTINEL /*!< internal helper */ +#endif + +/* Disable environ macro for Visual C++ 2015. */ +#undef environ + +/*! Telnet state tracker object type. */ +typedef struct telnet_t telnet_t; + +/*! Telnet event object type. */ +typedef union telnet_event_t telnet_event_t; + +/*! Telnet option table element type. */ +typedef struct telnet_telopt_t telnet_telopt_t; + +/*! \name Telnet commands */ +/*@{*/ +/*! Telnet commands and special values. */ +#define TELNET_IAC 255 +#define TELNET_DONT 254 +#define TELNET_DO 253 +#define TELNET_WONT 252 +#define TELNET_WILL 251 +#define TELNET_SB 250 +#define TELNET_GA 249 +#define TELNET_EL 248 +#define TELNET_EC 247 +#define TELNET_AYT 246 +#define TELNET_AO 245 +#define TELNET_IP 244 +#define TELNET_BREAK 243 +#define TELNET_DM 242 +#define TELNET_NOP 241 +#define TELNET_SE 240 +#define TELNET_EOR 239 +#define TELNET_ABORT 238 +#define TELNET_SUSP 237 +#define TELNET_EOF 236 +/*@}*/ + +/*! \name Telnet option values. */ +/*@{*/ +/*! Telnet options. */ +#define TELNET_TELOPT_BINARY 0 +#define TELNET_TELOPT_ECHO 1 +#define TELNET_TELOPT_RCP 2 +#define TELNET_TELOPT_SGA 3 +#define TELNET_TELOPT_NAMS 4 +#define TELNET_TELOPT_STATUS 5 +#define TELNET_TELOPT_TM 6 +#define TELNET_TELOPT_RCTE 7 +#define TELNET_TELOPT_NAOL 8 +#define TELNET_TELOPT_NAOP 9 +#define TELNET_TELOPT_NAOCRD 10 +#define TELNET_TELOPT_NAOHTS 11 +#define TELNET_TELOPT_NAOHTD 12 +#define TELNET_TELOPT_NAOFFD 13 +#define TELNET_TELOPT_NAOVTS 14 +#define TELNET_TELOPT_NAOVTD 15 +#define TELNET_TELOPT_NAOLFD 16 +#define TELNET_TELOPT_XASCII 17 +#define TELNET_TELOPT_LOGOUT 18 +#define TELNET_TELOPT_BM 19 +#define TELNET_TELOPT_DET 20 +#define TELNET_TELOPT_SUPDUP 21 +#define TELNET_TELOPT_SUPDUPOUTPUT 22 +#define TELNET_TELOPT_SNDLOC 23 +#define TELNET_TELOPT_TTYPE 24 +#define TELNET_TELOPT_EOR 25 +#define TELNET_TELOPT_TUID 26 +#define TELNET_TELOPT_OUTMRK 27 +#define TELNET_TELOPT_TTYLOC 28 +#define TELNET_TELOPT_3270REGIME 29 +#define TELNET_TELOPT_X3PAD 30 +#define TELNET_TELOPT_NAWS 31 +#define TELNET_TELOPT_TSPEED 32 +#define TELNET_TELOPT_LFLOW 33 +#define TELNET_TELOPT_LINEMODE 34 +#define TELNET_TELOPT_XDISPLOC 35 +#define TELNET_TELOPT_ENVIRON 36 +#define TELNET_TELOPT_AUTHENTICATION 37 +#define TELNET_TELOPT_ENCRYPT 38 +#define TELNET_TELOPT_NEW_ENVIRON 39 +#define TELNET_TELOPT_MSSP 70 +#define TELNET_TELOPT_COMPRESS 85 +#define TELNET_TELOPT_COMPRESS2 86 +#define TELNET_TELOPT_ZMP 93 +#define TELNET_TELOPT_EXOPL 255 + +#define TELNET_TELOPT_MCCP2 86 +/*@}*/ + +/*! \name Protocol codes for TERMINAL-TYPE commands. */ +/*@{*/ +/*! TERMINAL-TYPE codes. */ +#define TELNET_TTYPE_IS 0 +#define TELNET_TTYPE_SEND 1 +/*@}*/ + +/*! \name Protocol codes for NEW-ENVIRON/ENVIRON commands. */ +/*@{*/ +/*! NEW-ENVIRON/ENVIRON codes. */ +#define TELNET_ENVIRON_IS 0 +#define TELNET_ENVIRON_SEND 1 +#define TELNET_ENVIRON_INFO 2 +#define TELNET_ENVIRON_VAR 0 +#define TELNET_ENVIRON_VALUE 1 +#define TELNET_ENVIRON_ESC 2 +#define TELNET_ENVIRON_USERVAR 3 +/*@}*/ + +/*! \name Protocol codes for MSSP commands. */ +/*@{*/ +/*! MSSP codes. */ +#define TELNET_MSSP_VAR 1 +#define TELNET_MSSP_VAL 2 +/*@}*/ + +/*! \name Telnet state tracker flags. */ +/*@{*/ +/*! Control behavior of telnet state tracker. */ +#define TELNET_FLAG_PROXY (1<<0) +#define TELNET_FLAG_NVT_EOL (1<<1) + +/* Internal-only bits in option flags */ +#define TELNET_FLAG_TRANSMIT_BINARY (1<<5) +#define TELNET_FLAG_RECEIVE_BINARY (1<<6) +#define TELNET_PFLAG_DEFLATE (1<<7) +/*@}*/ + +/*! + * error codes + */ +enum telnet_error_t { + TELNET_EOK = 0, /*!< no error */ + TELNET_EBADVAL, /*!< invalid parameter, or API misuse */ + TELNET_ENOMEM, /*!< memory allocation failure */ + TELNET_EOVERFLOW, /*!< data exceeds buffer size */ + TELNET_EPROTOCOL, /*!< invalid sequence of special bytes */ + TELNET_ECOMPRESS /*!< error handling compressed streams */ +}; +typedef enum telnet_error_t telnet_error_t; /*!< Error code type. */ + +/*! + * event codes + */ +enum telnet_event_type_t { + TELNET_EV_DATA = 0, /*!< raw text data has been received */ + TELNET_EV_SEND, /*!< data needs to be sent to the peer */ + TELNET_EV_IAC, /*!< generic IAC code received */ + TELNET_EV_WILL, /*!< WILL option negotiation received */ + TELNET_EV_WONT, /*!< WONT option neogitation received */ + TELNET_EV_DO, /*!< DO option negotiation received */ + TELNET_EV_DONT, /*!< DONT option negotiation received */ + TELNET_EV_SUBNEGOTIATION, /*!< sub-negotiation data received */ + TELNET_EV_COMPRESS, /*!< compression has been enabled */ + TELNET_EV_ZMP, /*!< ZMP command has been received */ + TELNET_EV_TTYPE, /*!< TTYPE command has been received */ + TELNET_EV_ENVIRON, /*!< ENVIRON command has been received */ + TELNET_EV_MSSP, /*!< MSSP command has been received */ + TELNET_EV_WARNING, /*!< recoverable error has occured */ + TELNET_EV_ERROR /*!< non-recoverable error has occured */ +}; +typedef enum telnet_event_type_t telnet_event_type_t; /*!< Telnet event type. */ + +/*! + * environ/MSSP command information + */ +struct telnet_environ_t { + unsigned char type; /*!< either TELNET_ENVIRON_VAR or TELNET_ENVIRON_USERVAR */ + char *var; /*!< name of the variable being set */ + char *value; /*!< value of variable being set; empty string if no value */ +}; + +/*! + * event information + */ +union telnet_event_t { + /*! + * \brief Event type + * + * The type field will determine which of the other event structure fields + * have been filled in. For instance, if the event type is TELNET_EV_ZMP, + * then the zmp event field (and ONLY the zmp event field) will be filled + * in. + */ + enum telnet_event_type_t type; + + /*! + * data event: for DATA and SEND events + */ + struct data_t { + enum telnet_event_type_t _type; /*!< alias for type */ + const char *buffer; /*!< byte buffer */ + size_t size; /*!< number of bytes in buffer */ + } data; /*!< DATA and SEND */ + + /*! + * WARNING and ERROR events + */ + struct error_t { + enum telnet_event_type_t _type; /*!< alias for type */ + const char *file; /*!< file the error occured in */ + const char *func; /*!< function the error occured in */ + const char *msg; /*!< error message string */ + int line; /*!< line of file error occured on */ + telnet_error_t errcode; /*!< error code */ + } error; /*!< WARNING and ERROR */ + + /*! + * command event: for IAC + */ + struct iac_t { + enum telnet_event_type_t _type; /*!< alias for type */ + unsigned char cmd; /*!< telnet command received */ + } iac; /*!< IAC */ + + /*! + * negotiation event: WILL, WONT, DO, DONT + */ + struct negotiate_t { + enum telnet_event_type_t _type; /*!< alias for type */ + unsigned char telopt; /*!< option being negotiated */ + } neg; /*!< WILL, WONT, DO, DONT */ + + /*! + * subnegotiation event + */ + struct subnegotiate_t { + enum telnet_event_type_t _type; /*!< alias for type */ + const char *buffer; /*!< data of sub-negotiation */ + size_t size; /*!< number of bytes in buffer */ + unsigned char telopt; /*!< option code for negotiation */ + } sub; /*!< SB */ + + /*! + * ZMP event + */ + struct zmp_t { + enum telnet_event_type_t _type; /*!< alias for type */ + const char **argv; /*!< array of argument string */ + size_t argc; /*!< number of elements in argv */ + } zmp; /*!< ZMP */ + + /*! + * TTYPE event + */ + struct ttype_t { + enum telnet_event_type_t _type; /*!< alias for type */ + unsigned char cmd; /*!< TELNET_TTYPE_IS or TELNET_TTYPE_SEND */ + const char* name; /*!< terminal type name (IS only) */ + } ttype; /*!< TTYPE */ + + /*! + * COMPRESS event + */ + struct compress_t { + enum telnet_event_type_t _type; /*!< alias for type */ + unsigned char state; /*!< 1 if compression is enabled, + 0 if disabled */ + } compress; /*!< COMPRESS */ + + /*! + * ENVIRON/NEW-ENVIRON event + */ + struct environ_t { + enum telnet_event_type_t _type; /*!< alias for type */ + const struct telnet_environ_t *values; /*!< array of variable values */ + size_t size; /*!< number of elements in values */ + unsigned char cmd; /*!< SEND, IS, or INFO */ + } environ; /*!< ENVIRON, NEW-ENVIRON */ + + /*! + * MSSP event + */ + struct mssp_t { + enum telnet_event_type_t _type; /*!< alias for type */ + const struct telnet_environ_t *values; /*!< array of variable values */ + size_t size; /*!< number of elements in values */ + } mssp; /*!< MSSP */ +}; + +/*! + * \brief event handler + * + * This is the type of function that must be passed to + * telnet_init() when creating a new telnet object. The + * function will be invoked once for every event generated + * by the libtelnet protocol parser. + * + * \param telnet The telnet object that generated the event + * \param event Event structure with details about the event + * \param user_data User-supplied pointer + */ +typedef void (*telnet_event_handler_t)(telnet_t *telnet, + telnet_event_t *event, void *user_data); + +/*! + * telopt support table element; use telopt of -1 for end marker + */ +struct telnet_telopt_t { + short telopt; /*!< one of the TELOPT codes or -1 */ + unsigned char us; /*!< TELNET_WILL or TELNET_WONT */ + unsigned char him; /*!< TELNET_DO or TELNET_DONT */ +}; + +/*! + * state tracker -- private data structure + */ +struct telnet_t; + +/*! + * \brief Initialize a telnet state tracker. + * + * This function initializes a new state tracker, which is used for all + * other libtelnet functions. Each connection must have its own + * telnet state tracker object. + * + * \param telopts Table of TELNET options the application supports. + * \param eh Event handler function called for every event. + * \param flags 0 or TELNET_FLAG_PROXY. + * \param user_data Optional data pointer that will be passsed to eh. + * \return Telnet state tracker object. + */ +extern telnet_t* telnet_init(const telnet_telopt_t *telopts, + telnet_event_handler_t eh, unsigned char flags, void *user_data); + +/*! + * \brief Free up any memory allocated by a state tracker. + * + * This function must be called when a telnet state tracker is no + * longer needed (such as after the connection has been closed) to + * release any memory resources used by the state tracker. + * + * \param telnet Telnet state tracker object. + */ +extern void telnet_free(telnet_t *telnet); + +/*! + * \brief Push a byte buffer into the state tracker. + * + * Passes one or more bytes to the telnet state tracker for + * protocol parsing. The byte buffer is most often going to be + * the buffer that recv() was called for while handling the + * connection. + * + * \param telnet Telnet state tracker object. + * \param buffer Pointer to byte buffer. + * \param size Number of bytes pointed to by buffer. + */ +extern void telnet_recv(telnet_t *telnet, const char *buffer, + size_t size); + +/*! + * \brief Send a telnet command. + * + * \param telnet Telnet state tracker object. + * \param cmd Command to send. + */ +extern void telnet_iac(telnet_t *telnet, unsigned char cmd); + +/*! + * \brief Send negotiation command. + * + * Internally, libtelnet uses RFC1143 option negotiation rules. + * The negotiation commands sent with this function may be ignored + * if they are determined to be redundant. + * + * \param telnet Telnet state tracker object. + * \param cmd TELNET_WILL, TELNET_WONT, TELNET_DO, or TELNET_DONT. + * \param opt One of the TELNET_TELOPT_* values. + */ +extern void telnet_negotiate(telnet_t *telnet, unsigned char cmd, + unsigned char opt); + +/*! + * Send non-command data (escapes IAC bytes). + * + * \param telnet Telnet state tracker object. + * \param buffer Buffer of bytes to send. + * \param size Number of bytes to send. + */ +extern void telnet_send(telnet_t *telnet, + const char *buffer, size_t size); + +/*! + * Send non-command text (escapes IAC bytes and translates + * \\r -> CR-NUL and \\n -> CR-LF unless in BINARY mode. + * + * \param telnet Telnet state tracker object. + * \param buffer Buffer of bytes to send. + * \param size Number of bytes to send. + */ +extern void telnet_send_text(telnet_t *telnet, + const char *buffer, size_t size); + +/*! + * \brief Begin a sub-negotiation command. + * + * Sends IAC SB followed by the telopt code. All following data sent + * will be part of the sub-negotiation, until telnet_finish_sb() is + * called. + * + * \param telnet Telnet state tracker object. + * \param telopt One of the TELNET_TELOPT_* values. + */ +extern void telnet_begin_sb(telnet_t *telnet, + unsigned char telopt); + +/*! + * \brief Finish a sub-negotiation command. + * + * This must be called after a call to telnet_begin_sb() to finish a + * sub-negotiation command. + * + * \param telnet Telnet state tracker object. + */ +#define telnet_finish_sb(telnet) telnet_iac((telnet), TELNET_SE) + +/*! + * \brief Shortcut for sending a complete subnegotiation buffer. + * + * Equivalent to: + * telnet_begin_sb(telnet, telopt); + * telnet_send(telnet, buffer, size); + * telnet_finish_sb(telnet); + * + * \param telnet Telnet state tracker format. + * \param telopt One of the TELNET_TELOPT_* values. + * \param buffer Byte buffer for sub-negotiation data. + * \param size Number of bytes to use for sub-negotiation data. + */ +extern void telnet_subnegotiation(telnet_t *telnet, unsigned char telopt, + const char *buffer, size_t size); + +/*! + * \brief Begin sending compressed data. + * + * This function will begein sending data using the COMPRESS2 option, + * which enables the use of zlib to compress data sent to the client. + * The client must offer support for COMPRESS2 with option negotiation, + * and zlib support must be compiled into libtelnet. + * + * Only the server may call this command. + * + * \param telnet Telnet state tracker object. + */ +extern void telnet_begin_compress2(telnet_t *telnet); + +/*! + * \brief Send formatted data. + * + * This function is a wrapper around telnet_send(). It allows using + * printf-style formatting. + * + * Additionally, this function will translate \\r to the CR NUL construct and + * \\n with CR LF, as well as automatically escaping IAC bytes like + * telnet_send(). + * + * \param telnet Telnet state tracker object. + * \param fmt Format string. + * \return Number of bytes sent. + */ +extern int telnet_printf(telnet_t *telnet, const char *fmt, ...) + TELNET_GNU_PRINTF(2, 3); + +/*! + * \brief Send formatted data. + * + * See telnet_printf(). + */ +extern int telnet_vprintf(telnet_t *telnet, const char *fmt, va_list va); + +/*! + * \brief Send formatted data (no newline escaping). + * + * This behaves identically to telnet_printf(), except that the \\r and \\n + * characters are not translated. The IAC byte is still escaped as normal + * with telnet_send(). + * + * \param telnet Telnet state tracker object. + * \param fmt Format string. + * \return Number of bytes sent. + */ +extern int telnet_raw_printf(telnet_t *telnet, const char *fmt, ...) + TELNET_GNU_PRINTF(2, 3); + +/*! + * \brief Send formatted data (no newline escaping). + * + * See telnet_raw_printf(). + */ +extern int telnet_raw_vprintf(telnet_t *telnet, const char *fmt, va_list va); + +/*! + * \brief Begin a new set of NEW-ENVIRON values to request or send. + * + * This function will begin the sub-negotiation block for sending or + * requesting NEW-ENVIRON values. + * + * The telnet_finish_newenviron() macro must be called after this + * function to terminate the NEW-ENVIRON command. + * + * \param telnet Telnet state tracker object. + * \param type One of TELNET_ENVIRON_SEND, TELNET_ENVIRON_IS, or + * TELNET_ENVIRON_INFO. + */ +extern void telnet_begin_newenviron(telnet_t *telnet, unsigned char type); + +/*! + * \brief Send a NEW-ENVIRON variable name or value. + * + * This can only be called between calls to telnet_begin_newenviron() and + * telnet_finish_newenviron(). + * + * \param telnet Telnet state tracker object. + * \param type One of TELNET_ENVIRON_VAR, TELNET_ENVIRON_USERVAR, or + * TELNET_ENVIRON_VALUE. + * \param string Variable name or value. + */ +extern void telnet_newenviron_value(telnet_t* telnet, unsigned char type, + const char *string); + +/*! + * \brief Finish a NEW-ENVIRON command. + * + * This must be called after a call to telnet_begin_newenviron() to finish a + * NEW-ENVIRON variable list. + * + * \param telnet Telnet state tracker object. + */ +#define telnet_finish_newenviron(telnet) telnet_finish_sb((telnet)) + +/*! + * \brief Send the TERMINAL-TYPE SEND command. + * + * Sends the sequence IAC TERMINAL-TYPE SEND. + * + * \param telnet Telnet state tracker object. + */ +extern void telnet_ttype_send(telnet_t *telnet); + +/*! + * \brief Send the TERMINAL-TYPE IS command. + * + * Sends the sequence IAC TERMINAL-TYPE IS "string". + * + * According to the RFC, the recipient of a TERMINAL-TYPE SEND shall + * send the next possible terminal-type the client supports. Upon sending + * the type, the client should switch modes to begin acting as the terminal + * type is just sent. + * + * The server may continue sending TERMINAL-TYPE IS until it receives a + * terminal type is understands. To indicate to the server that it has + * reached the end of the available optoins, the client must send the last + * terminal type a second time. When the server receives the same terminal + * type twice in a row, it knows it has seen all available terminal types. + * + * After the last terminal type is sent, if the client receives another + * TERMINAL-TYPE SEND command, it must begin enumerating the available + * terminal types from the very beginning. This allows the server to + * scan the available types for a preferred terminal type and, if none + * is found, to then ask the client to switch to an acceptable + * alternative. + * + * Note that if the client only supports a single terminal type, then + * simply sending that one type in response to every SEND will satisfy + * the behavior requirements. + * + * \param telnet Telnet state tracker object. + * \param ttype Name of the terminal-type being sent. + */ +extern void telnet_ttype_is(telnet_t *telnet, const char* ttype); + +/*! + * \brief Send a ZMP command. + * + * \param telnet Telnet state tracker object. + * \param argc Number of ZMP commands being sent. + * \param argv Array of argument strings. + */ +extern void telnet_send_zmp(telnet_t *telnet, size_t argc, const char **argv); + +/*! + * \brief Send a ZMP command. + * + * Arguments are listed out in var-args style. After the last argument, a + * NULL pointer must be passed in as a sentinel value. + * + * \param telnet Telnet state tracker object. + */ +extern void telnet_send_zmpv(telnet_t *telnet, ...) TELNET_GNU_SENTINEL; + +/*! + * \brief Send a ZMP command. + * + * See telnet_send_zmpv(). + */ +extern void telnet_send_vzmpv(telnet_t *telnet, va_list va); + +/*! + * \brief Begin sending a ZMP command + * + * \param telnet Telnet state tracker object. + * \param cmd The first argument (command name) for the ZMP command. + */ +extern void telnet_begin_zmp(telnet_t *telnet, const char *cmd); + +/*! + * \brief Send a ZMP command argument. + * + * \param telnet Telnet state tracker object. + * \param arg Telnet argument string. + */ +extern void telnet_zmp_arg(telnet_t *telnet, const char *arg); + +/*! + * \brief Finish a ZMP command. + * + * This must be called after a call to telnet_begin_zmp() to finish a + * ZMP argument list. + * + * \param telnet Telnet state tracker object. + */ +#define telnet_finish_zmp(telnet) telnet_finish_sb((telnet)) + +/* C++ support */ +#if defined(__cplusplus) +} /* extern "C" */ +#endif + +#endif /* !defined(LIBTELNET_INCLUDE) */ diff --git a/managed_components/frozen-eye__esp_telnet/src/server.c b/managed_components/frozen-eye__esp_telnet/src/server.c new file mode 100644 index 0000000..7780f71 --- /dev/null +++ b/managed_components/frozen-eye__esp_telnet/src/server.c @@ -0,0 +1,336 @@ +#include "freertos/FreeRTOS.h" +#include "sdkconfig.h" + +#define LINEBUFFER_SIZE 256 +#define AWAIT_TIMEOUT 10 + +#include +#include +#include +#include +#include + +static const char* TAG = "telnet"; +static struct user_t users[CONFIG_TELNET_SERVER_MAX_CONNECTIONS + 1]; + +static void linebuffer_push(char* buffer, size_t size, int* linepos, char ch, + void (*cb)(const char* line, size_t overflow, void* ud), void* ud) +{ + /* CRLF -- line terminator */ + if (ch == '\n' && *linepos > 0 && buffer[*linepos - 1] == '\r') { + /* NUL terminate (replaces \r in buffer), notify app, clear */ + buffer[*linepos - 1] = 0; + cb(buffer, 0, ud); + *linepos = 0; + + /* CRNUL -- just a CR */ + } + else if (ch == 0 && *linepos > 0 && buffer[*linepos - 1] == '\r') { + /* do nothing, the CR is already in the buffer */ + + /* anything else (including technically invalid CR followed by + * anything besides LF or NUL -- just buffer if we have room + * \r + */ + } + else if (*linepos != (int)size) { + buffer[(*linepos)++] = ch; + + /* buffer overflow */ + } + else { + /* terminate (NOTE: eats a byte), notify app, clear buffer */ + buffer[size - 1] = 0; + cb(buffer, size - 1, ud); + *linepos = 0; + } +} + +static void _message(const char* from, const char* msg) +{ + int i; + for (i = 0; i != CONFIG_TELNET_SERVER_MAX_CONNECTIONS; ++i) { + if (users[i].sock != -1 && users[i].name != 0 && strcmp(users[i].name, from) != 0) { + telnet_printf(users[i].telnet, "%s: \"%s\"\n", from, msg); + } + } +} + +static void _broadcast(const char* from, const char* msg) +{ + int i; + for (i = 0; i != CONFIG_TELNET_SERVER_MAX_CONNECTIONS; ++i) { + if (users[i].sock != -1) { + telnet_printf(users[i].telnet, "%s: \"%s\"\n", from, msg); + } + } +} + +static void _send(int sock, const char* buffer, size_t size) +{ + int rs; + + /* ignore on invalid socket */ + if (sock == -1) + return; + + /* send data */ + while (size > 0) { + if ((rs = send(sock, buffer, (int)size, 0)) == -1) { + if (errno != EINTR && errno != ECONNRESET) { + ESP_LOGW(TAG, "send() failed: %s", strerror(errno)); + return; + } + else { + return; + } + } + else if (rs == 0) { + ESP_LOGE(TAG, "send() unexpectedly returned 0"); + return; + } + + /* update pointer and size to see if we've got more to send */ + buffer += rs; + size -= rs; + } +} + +/* process input line */ +static void _online(const char* line, size_t overflow, void* ud) +{ + struct user_t* user = (struct user_t*)ud; + int i; + + (void)overflow; + + /* if the user has no name, this is his "login" */ + if (user->name == 0) { + /* must not be empty, must be at least 32 chars */ + if (strlen(line) == 0 || strlen(line) > 32) { + telnet_printf(user->telnet, "Invalid name. Enter name: "); + return; + } + + /* must not already exist */ + for (i = 0; i != CONFIG_TELNET_SERVER_MAX_CONNECTIONS; ++i) { + if (users[i].name != 0 && strcmp(users[i].name, line) == 0) { + telnet_printf(user->telnet, "Name already in use. Enter name: "); + return; + } + } + + /* keep name */ + user->name = strdup(line); + telnet_printf(user->telnet, "Welcome, %s!\n", line); + return; + } + + /* execute a command, need to send to the system */ + // _message(user->name, line); +} + +static void _input(struct user_t* user, const char* buffer, size_t size) +{ + unsigned int i; + for (i = 0; user->sock != -1 && i != size; ++i) { + linebuffer_push(user->linebuf, sizeof(user->linebuf), &user->linepos, (char)buffer[i], _online, user); + } +} + +static void _event_handler(telnet_t* telnet, telnet_event_t* ev, void* user_data) +{ + struct user_t* user = (struct user_t*)user_data; + + switch (ev->type) { + /* data received */ + case TELNET_EV_DATA: + _input(user, ev->data.buffer, ev->data.size); + // telnet_negotiate(telnet, TELNET_WONT, TELNET_TELOPT_ECHO); + // telnet_negotiate(telnet, TELNET_WILL, TELNET_TELOPT_ECHO); + break; + /* data must be sent */ + case TELNET_EV_SEND: _send(user->sock, ev->data.buffer, ev->data.size); break; + /* enable compress2 if accepted by client */ + case TELNET_EV_DO: + if (ev->neg.telopt == TELNET_TELOPT_COMPRESS2) + telnet_begin_compress2(telnet); + break; + /* error */ + case TELNET_EV_ERROR: + close(user->sock); + user->sock = -1; + if (user->name != 0) { + _message(user->name, "** HAS HAD AN ERROR **"); + free(user->name); + user->name = 0; + } + telnet_free(user->telnet); + break; + default: + /* ignore */ + break; + } +} + +void telnet_task(void* arg) +{ + static char buffer[512]; + static struct sockaddr_in addr; + static telnet_server_config_t config; + + // save the configuration + memcpy(&config, arg, sizeof(telnet_server_config_t)); + + int listen_sock; + int client_sock; + int rs; + int i; + socklen_t addrlen; + struct pollfd pfd[config.max_connections + 1]; + + /* initialize data structures */ + memset(&pfd, 0, sizeof(pfd)); + memset(users, 0, sizeof(users)); + for (i = 0; i != config.max_connections; ++i) { + users[i].sock = -1; + } + + /* create listening socket */ + if ((listen_sock = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + ESP_LOGE(TAG, "socket() failed: %s", strerror(errno)); + return; + } + + /* reuse address option */ + rs = 1; + setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&rs, sizeof(rs)); + + /* bind to listening addr/port */ + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(config.port); + if (bind(listen_sock, (struct sockaddr*)&addr, sizeof(addr)) == -1) { + ESP_LOGE(TAG, "bind() failed: %s", strerror(errno)); + close(listen_sock); + return; + } + + /* listen for clients */ + if (listen(listen_sock, 3) == -1) { + ESP_LOGE(TAG, "listen() failed: %s", strerror(errno)); + close(listen_sock); + return; + } + + ESP_LOGI(TAG, "Telnet server listening on port %d", config.port); + + /* initialize listening descriptors */ + pfd[config.max_connections].fd = listen_sock; + pfd[config.max_connections].events = POLLIN; + + /* loop for ever */ + while (true) { + /* prepare for poll */ + for (i = 0; i != config.max_connections; ++i) { + if (users[i].sock != -1) { + pfd[i].fd = users[i].sock; + pfd[i].events = POLLIN; + } + else { + pfd[i].fd = -1; + pfd[i].events = 0; + } + } + + /* poll */ + rs = poll(pfd, config.max_connections + 1, AWAIT_TIMEOUT); + if (rs == -1 && errno != EINTR) { + ESP_LOGE(TAG, "poll() failed: %s", strerror(errno)); + close(listen_sock); + return; + } + + /* new connection */ + if (pfd[config.max_connections].revents & (POLLIN | POLLRDNORM | POLLRDBAND | POLLPRI | POLLERR | POLLHUP)) { + /* acept the sock */ + ESP_LOGW(TAG, "New connection"); + addrlen = sizeof(addr); + if ((client_sock = accept(listen_sock, (struct sockaddr*)&addr, &addrlen)) == -1) { + ESP_LOGE(TAG, "accept() failed: %s", strerror(errno)); + return; + } + + ESP_LOGV(TAG, "Connection received"); + + /* find a free user */ + for (i = 0; i != config.max_connections; ++i) { + if (users[i].sock == -1) { + break; + } + } + + if (i == config.max_connections) { + ESP_LOGV(TAG, " rejected (too many users)"); + _send(client_sock, "Too many users.\n", 16); + close(client_sock); + } + + /* init, welcome */ + users[i].sock = client_sock; + users[i].telnet = telnet_init(config.telnet_opts, _event_handler, 0, &users[i]); + telnet_negotiate(users[i].telnet, TELNET_WILL, TELNET_TELOPT_COMPRESS2); + telnet_printf(users[i].telnet, "Enter name: "); + + // telnet_negotiate(users[i].telnet, TELNET_WILL, TELNET_TELOPT_ECHO); + } + + /* read from client */ + for (i = 0; i != config.max_connections; ++i) { + /* skip users that aren't actually connected */ + if (users[i].sock == -1) { + continue; + } + + if (pfd[i].revents & (POLLIN | POLLERR | POLLHUP)) { + if ((rs = recv(users[i].sock, buffer, sizeof(buffer), 0)) > 0) { + telnet_recv(users[i].telnet, buffer, rs); + } + else if (rs == 0) { + ESP_LOGV(TAG, "Connection closed."); + close(users[i].sock); + users[i].sock = -1; + if (users[i].name != 0) { + // _message(users[i].name, "** HAS DISCONNECTED **"); + free(users[i].name); + users[i].name = 0; + } + telnet_free(users[i].telnet); + } + else if (errno != EINTR) { + ESP_LOGE(TAG, "recv(client) failed: %s", strerror(errno)); + return; + } + } + } + } +} + +static TaskHandle_t xHandle = NULL; + +esp_err_t telnet_server_create(telnet_server_config_t* config) +{ + if (config == NULL) { + return ESP_ERR_INVALID_ARG; + } + + BaseType_t xReturned = xTaskCreate(telnet_task, "telnet_task", config->stack_size, config, config->task_priority, &xHandle); + if (xReturned == pdPASS) { + ESP_LOGV(TAG, "Telnet task created successfully."); + return ESP_OK; + } + + ESP_LOGE(TAG, "Failed to create telnet task."); + return ESP_FAIL; +} diff --git a/sdkconfig b/sdkconfig new file mode 100644 index 0000000..2677bb0 --- /dev/null +++ b/sdkconfig @@ -0,0 +1,2255 @@ +# +# Automatically generated file. DO NOT EDIT. +# Espressif IoT Development Framework (ESP-IDF) 5.4.3 Project Configuration +# +CONFIG_SOC_CAPS_ECO_VER_MAX=301 +CONFIG_SOC_ADC_SUPPORTED=y +CONFIG_SOC_DAC_SUPPORTED=y +CONFIG_SOC_UART_SUPPORTED=y +CONFIG_SOC_MCPWM_SUPPORTED=y +CONFIG_SOC_GPTIMER_SUPPORTED=y +CONFIG_SOC_SDMMC_HOST_SUPPORTED=y +CONFIG_SOC_BT_SUPPORTED=y +CONFIG_SOC_PCNT_SUPPORTED=y +CONFIG_SOC_PHY_SUPPORTED=y +CONFIG_SOC_WIFI_SUPPORTED=y +CONFIG_SOC_SDIO_SLAVE_SUPPORTED=y +CONFIG_SOC_TWAI_SUPPORTED=y +CONFIG_SOC_EFUSE_SUPPORTED=y +CONFIG_SOC_EMAC_SUPPORTED=y +CONFIG_SOC_ULP_SUPPORTED=y +CONFIG_SOC_CCOMP_TIMER_SUPPORTED=y +CONFIG_SOC_RTC_FAST_MEM_SUPPORTED=y +CONFIG_SOC_RTC_SLOW_MEM_SUPPORTED=y +CONFIG_SOC_RTC_MEM_SUPPORTED=y +CONFIG_SOC_I2S_SUPPORTED=y +CONFIG_SOC_RMT_SUPPORTED=y +CONFIG_SOC_SDM_SUPPORTED=y +CONFIG_SOC_GPSPI_SUPPORTED=y +CONFIG_SOC_LEDC_SUPPORTED=y +CONFIG_SOC_I2C_SUPPORTED=y +CONFIG_SOC_SUPPORT_COEXISTENCE=y +CONFIG_SOC_AES_SUPPORTED=y +CONFIG_SOC_MPI_SUPPORTED=y +CONFIG_SOC_SHA_SUPPORTED=y +CONFIG_SOC_FLASH_ENC_SUPPORTED=y +CONFIG_SOC_SECURE_BOOT_SUPPORTED=y +CONFIG_SOC_TOUCH_SENSOR_SUPPORTED=y +CONFIG_SOC_BOD_SUPPORTED=y +CONFIG_SOC_ULP_FSM_SUPPORTED=y +CONFIG_SOC_CLK_TREE_SUPPORTED=y +CONFIG_SOC_MPU_SUPPORTED=y +CONFIG_SOC_WDT_SUPPORTED=y +CONFIG_SOC_SPI_FLASH_SUPPORTED=y +CONFIG_SOC_RNG_SUPPORTED=y +CONFIG_SOC_LIGHT_SLEEP_SUPPORTED=y +CONFIG_SOC_DEEP_SLEEP_SUPPORTED=y +CONFIG_SOC_LP_PERIPH_SHARE_INTERRUPT=y +CONFIG_SOC_PM_SUPPORTED=y +CONFIG_SOC_DPORT_WORKAROUND_DIS_INTERRUPT_LVL=5 +CONFIG_SOC_XTAL_SUPPORT_26M=y +CONFIG_SOC_XTAL_SUPPORT_40M=y +CONFIG_SOC_XTAL_SUPPORT_AUTO_DETECT=y +CONFIG_SOC_ADC_RTC_CTRL_SUPPORTED=y +CONFIG_SOC_ADC_DIG_CTRL_SUPPORTED=y +CONFIG_SOC_ADC_DMA_SUPPORTED=y +CONFIG_SOC_ADC_PERIPH_NUM=2 +CONFIG_SOC_ADC_MAX_CHANNEL_NUM=10 +CONFIG_SOC_ADC_ATTEN_NUM=4 +CONFIG_SOC_ADC_DIGI_CONTROLLER_NUM=2 +CONFIG_SOC_ADC_PATT_LEN_MAX=16 +CONFIG_SOC_ADC_DIGI_MIN_BITWIDTH=9 +CONFIG_SOC_ADC_DIGI_MAX_BITWIDTH=12 +CONFIG_SOC_ADC_DIGI_RESULT_BYTES=2 +CONFIG_SOC_ADC_DIGI_DATA_BYTES_PER_CONV=4 +CONFIG_SOC_ADC_DIGI_MONITOR_NUM=0 +CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_HIGH=2 +CONFIG_SOC_ADC_SAMPLE_FREQ_THRES_LOW=20 +CONFIG_SOC_ADC_RTC_MIN_BITWIDTH=9 +CONFIG_SOC_ADC_RTC_MAX_BITWIDTH=12 +CONFIG_SOC_ADC_SHARED_POWER=y +CONFIG_SOC_BROWNOUT_RESET_SUPPORTED=y +CONFIG_SOC_SHARED_IDCACHE_SUPPORTED=y +CONFIG_SOC_IDCACHE_PER_CORE=y +CONFIG_SOC_CPU_CORES_NUM=2 +CONFIG_SOC_CPU_INTR_NUM=32 +CONFIG_SOC_CPU_HAS_FPU=y +CONFIG_SOC_HP_CPU_HAS_MULTIPLE_CORES=y +CONFIG_SOC_CPU_BREAKPOINTS_NUM=2 +CONFIG_SOC_CPU_WATCHPOINTS_NUM=2 +CONFIG_SOC_CPU_WATCHPOINT_MAX_REGION_SIZE=0x40 +CONFIG_SOC_DAC_CHAN_NUM=2 +CONFIG_SOC_DAC_RESOLUTION=8 +CONFIG_SOC_DAC_DMA_16BIT_ALIGN=y +CONFIG_SOC_GPIO_PORT=1 +CONFIG_SOC_GPIO_PIN_COUNT=40 +CONFIG_SOC_GPIO_VALID_GPIO_MASK=0xFFFFFFFFFF +CONFIG_SOC_GPIO_IN_RANGE_MAX=39 +CONFIG_SOC_GPIO_OUT_RANGE_MAX=33 +CONFIG_SOC_GPIO_VALID_DIGITAL_IO_PAD_MASK=0xEF0FEA +CONFIG_SOC_GPIO_CLOCKOUT_BY_IO_MUX=y +CONFIG_SOC_GPIO_CLOCKOUT_CHANNEL_NUM=3 +CONFIG_SOC_GPIO_SUPPORT_HOLD_IO_IN_DSLP=y +CONFIG_SOC_I2C_NUM=2 +CONFIG_SOC_HP_I2C_NUM=2 +CONFIG_SOC_I2C_FIFO_LEN=32 +CONFIG_SOC_I2C_CMD_REG_NUM=16 +CONFIG_SOC_I2C_SUPPORT_SLAVE=y +CONFIG_SOC_I2C_SUPPORT_APB=y +CONFIG_SOC_I2C_SUPPORT_10BIT_ADDR=y +CONFIG_SOC_I2C_STOP_INDEPENDENT=y +CONFIG_SOC_I2S_NUM=2 +CONFIG_SOC_I2S_HW_VERSION_1=y +CONFIG_SOC_I2S_SUPPORTS_APLL=y +CONFIG_SOC_I2S_SUPPORTS_PLL_F160M=y +CONFIG_SOC_I2S_SUPPORTS_PDM=y +CONFIG_SOC_I2S_SUPPORTS_PDM_TX=y +CONFIG_SOC_I2S_PDM_MAX_TX_LINES=1 +CONFIG_SOC_I2S_SUPPORTS_PDM_RX=y +CONFIG_SOC_I2S_PDM_MAX_RX_LINES=1 +CONFIG_SOC_I2S_SUPPORTS_ADC_DAC=y +CONFIG_SOC_I2S_SUPPORTS_ADC=y +CONFIG_SOC_I2S_SUPPORTS_DAC=y +CONFIG_SOC_I2S_SUPPORTS_LCD_CAMERA=y +CONFIG_SOC_I2S_MAX_DATA_WIDTH=24 +CONFIG_SOC_I2S_TRANS_SIZE_ALIGN_WORD=y +CONFIG_SOC_I2S_LCD_I80_VARIANT=y +CONFIG_SOC_LCD_I80_SUPPORTED=y +CONFIG_SOC_LCD_I80_BUSES=2 +CONFIG_SOC_LCD_I80_BUS_WIDTH=24 +CONFIG_SOC_LEDC_HAS_TIMER_SPECIFIC_MUX=y +CONFIG_SOC_LEDC_SUPPORT_APB_CLOCK=y +CONFIG_SOC_LEDC_SUPPORT_REF_TICK=y +CONFIG_SOC_LEDC_SUPPORT_HS_MODE=y +CONFIG_SOC_LEDC_TIMER_NUM=4 +CONFIG_SOC_LEDC_CHANNEL_NUM=8 +CONFIG_SOC_LEDC_TIMER_BIT_WIDTH=20 +CONFIG_SOC_MCPWM_GROUPS=2 +CONFIG_SOC_MCPWM_TIMERS_PER_GROUP=3 +CONFIG_SOC_MCPWM_OPERATORS_PER_GROUP=3 +CONFIG_SOC_MCPWM_COMPARATORS_PER_OPERATOR=2 +CONFIG_SOC_MCPWM_GENERATORS_PER_OPERATOR=2 +CONFIG_SOC_MCPWM_TRIGGERS_PER_OPERATOR=2 +CONFIG_SOC_MCPWM_GPIO_FAULTS_PER_GROUP=3 +CONFIG_SOC_MCPWM_CAPTURE_TIMERS_PER_GROUP=y +CONFIG_SOC_MCPWM_CAPTURE_CHANNELS_PER_TIMER=3 +CONFIG_SOC_MCPWM_GPIO_SYNCHROS_PER_GROUP=3 +CONFIG_SOC_MMU_PERIPH_NUM=2 +CONFIG_SOC_MMU_LINEAR_ADDRESS_REGION_NUM=3 +CONFIG_SOC_MPU_MIN_REGION_SIZE=0x20000000 +CONFIG_SOC_MPU_REGIONS_MAX_NUM=8 +CONFIG_SOC_PCNT_GROUPS=1 +CONFIG_SOC_PCNT_UNITS_PER_GROUP=8 +CONFIG_SOC_PCNT_CHANNELS_PER_UNIT=2 +CONFIG_SOC_PCNT_THRES_POINT_PER_UNIT=2 +CONFIG_SOC_RMT_GROUPS=1 +CONFIG_SOC_RMT_TX_CANDIDATES_PER_GROUP=8 +CONFIG_SOC_RMT_RX_CANDIDATES_PER_GROUP=8 +CONFIG_SOC_RMT_CHANNELS_PER_GROUP=8 +CONFIG_SOC_RMT_MEM_WORDS_PER_CHANNEL=64 +CONFIG_SOC_RMT_SUPPORT_REF_TICK=y +CONFIG_SOC_RMT_SUPPORT_APB=y +CONFIG_SOC_RMT_CHANNEL_CLK_INDEPENDENT=y +CONFIG_SOC_RTCIO_PIN_COUNT=18 +CONFIG_SOC_RTCIO_INPUT_OUTPUT_SUPPORTED=y +CONFIG_SOC_RTCIO_HOLD_SUPPORTED=y +CONFIG_SOC_RTCIO_WAKE_SUPPORTED=y +CONFIG_SOC_SDM_GROUPS=1 +CONFIG_SOC_SDM_CHANNELS_PER_GROUP=8 +CONFIG_SOC_SDM_CLK_SUPPORT_APB=y +CONFIG_SOC_SPI_HD_BOTH_INOUT_SUPPORTED=y +CONFIG_SOC_SPI_AS_CS_SUPPORTED=y +CONFIG_SOC_SPI_PERIPH_NUM=3 +CONFIG_SOC_SPI_DMA_CHAN_NUM=2 +CONFIG_SOC_SPI_MAX_CS_NUM=3 +CONFIG_SOC_SPI_SUPPORT_CLK_APB=y +CONFIG_SOC_SPI_MAXIMUM_BUFFER_SIZE=64 +CONFIG_SOC_SPI_MAX_PRE_DIVIDER=8192 +CONFIG_SOC_MEMSPI_SRC_FREQ_80M_SUPPORTED=y +CONFIG_SOC_MEMSPI_SRC_FREQ_40M_SUPPORTED=y +CONFIG_SOC_MEMSPI_SRC_FREQ_26M_SUPPORTED=y +CONFIG_SOC_MEMSPI_SRC_FREQ_20M_SUPPORTED=y +CONFIG_SOC_TIMER_GROUPS=2 +CONFIG_SOC_TIMER_GROUP_TIMERS_PER_GROUP=2 +CONFIG_SOC_TIMER_GROUP_COUNTER_BIT_WIDTH=64 +CONFIG_SOC_TIMER_GROUP_TOTAL_TIMERS=4 +CONFIG_SOC_TIMER_GROUP_SUPPORT_APB=y +CONFIG_SOC_LP_TIMER_BIT_WIDTH_LO=32 +CONFIG_SOC_LP_TIMER_BIT_WIDTH_HI=16 +CONFIG_SOC_TOUCH_SENSOR_VERSION=1 +CONFIG_SOC_TOUCH_SENSOR_NUM=10 +CONFIG_SOC_TOUCH_SAMPLE_CFG_NUM=1 +CONFIG_SOC_TWAI_CONTROLLER_NUM=1 +CONFIG_SOC_TWAI_BRP_MIN=2 +CONFIG_SOC_TWAI_CLK_SUPPORT_APB=y +CONFIG_SOC_TWAI_SUPPORT_MULTI_ADDRESS_LAYOUT=y +CONFIG_SOC_UART_NUM=3 +CONFIG_SOC_UART_HP_NUM=3 +CONFIG_SOC_UART_SUPPORT_APB_CLK=y +CONFIG_SOC_UART_SUPPORT_REF_TICK=y +CONFIG_SOC_UART_FIFO_LEN=128 +CONFIG_SOC_UART_BITRATE_MAX=5000000 +CONFIG_SOC_SPIRAM_SUPPORTED=y +CONFIG_SOC_SPI_MEM_SUPPORT_CONFIG_GPIO_BY_EFUSE=y +CONFIG_SOC_SHA_SUPPORT_PARALLEL_ENG=y +CONFIG_SOC_SHA_ENDIANNESS_BE=y +CONFIG_SOC_SHA_SUPPORT_SHA1=y +CONFIG_SOC_SHA_SUPPORT_SHA256=y +CONFIG_SOC_SHA_SUPPORT_SHA384=y +CONFIG_SOC_SHA_SUPPORT_SHA512=y +CONFIG_SOC_MPI_MEM_BLOCKS_NUM=4 +CONFIG_SOC_MPI_OPERATIONS_NUM=1 +CONFIG_SOC_RSA_MAX_BIT_LEN=4096 +CONFIG_SOC_AES_SUPPORT_AES_128=y +CONFIG_SOC_AES_SUPPORT_AES_192=y +CONFIG_SOC_AES_SUPPORT_AES_256=y +CONFIG_SOC_SECURE_BOOT_V1=y +CONFIG_SOC_EFUSE_SECURE_BOOT_KEY_DIGESTS=1 +CONFIG_SOC_FLASH_ENCRYPTED_XTS_AES_BLOCK_MAX=32 +CONFIG_SOC_PHY_DIG_REGS_MEM_SIZE=21 +CONFIG_SOC_PM_SUPPORT_EXT0_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_EXT1_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_EXT_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_TOUCH_SENSOR_WAKEUP=y +CONFIG_SOC_PM_SUPPORT_RTC_PERIPH_PD=y +CONFIG_SOC_PM_SUPPORT_RTC_FAST_MEM_PD=y +CONFIG_SOC_PM_SUPPORT_RTC_SLOW_MEM_PD=y +CONFIG_SOC_PM_SUPPORT_RC_FAST_PD=y +CONFIG_SOC_PM_SUPPORT_VDDSDIO_PD=y +CONFIG_SOC_PM_SUPPORT_MODEM_PD=y +CONFIG_SOC_CONFIGURABLE_VDDSDIO_SUPPORTED=y +CONFIG_SOC_PM_MODEM_PD_BY_SW=y +CONFIG_SOC_CLK_APLL_SUPPORTED=y +CONFIG_SOC_CLK_RC_FAST_D256_SUPPORTED=y +CONFIG_SOC_RTC_SLOW_CLK_SUPPORT_RC_FAST_D256=y +CONFIG_SOC_CLK_RC_FAST_SUPPORT_CALIBRATION=y +CONFIG_SOC_CLK_XTAL32K_SUPPORTED=y +CONFIG_SOC_SDMMC_USE_IOMUX=y +CONFIG_SOC_SDMMC_NUM_SLOTS=2 +CONFIG_SOC_WIFI_WAPI_SUPPORT=y +CONFIG_SOC_WIFI_CSI_SUPPORT=y +CONFIG_SOC_WIFI_MESH_SUPPORT=y +CONFIG_SOC_WIFI_SUPPORT_VARIABLE_BEACON_WINDOW=y +CONFIG_SOC_WIFI_NAN_SUPPORT=y +CONFIG_SOC_BLE_SUPPORTED=y +CONFIG_SOC_BLE_MESH_SUPPORTED=y +CONFIG_SOC_BT_CLASSIC_SUPPORTED=y +CONFIG_SOC_BLUFI_SUPPORTED=y +CONFIG_SOC_BT_H2C_ENC_KEY_CTRL_ENH_VSC_SUPPORTED=y +CONFIG_SOC_BLE_MULTI_CONN_OPTIMIZATION=y +CONFIG_SOC_ULP_HAS_ADC=y +CONFIG_SOC_PHY_COMBO_MODULE=y +CONFIG_SOC_EMAC_RMII_CLK_OUT_INTERNAL_LOOPBACK=y +CONFIG_IDF_CMAKE=y +CONFIG_IDF_TOOLCHAIN="gcc" +CONFIG_IDF_TOOLCHAIN_GCC=y +CONFIG_IDF_TARGET_ARCH_XTENSA=y +CONFIG_IDF_TARGET_ARCH="xtensa" +CONFIG_IDF_TARGET="esp32" +CONFIG_IDF_INIT_VERSION="5.4.3" +CONFIG_IDF_TARGET_ESP32=y +CONFIG_IDF_FIRMWARE_CHIP_ID=0x0000 + +# +# Build type +# +CONFIG_APP_BUILD_TYPE_APP_2NDBOOT=y +# CONFIG_APP_BUILD_TYPE_RAM is not set +CONFIG_APP_BUILD_GENERATE_BINARIES=y +CONFIG_APP_BUILD_BOOTLOADER=y +CONFIG_APP_BUILD_USE_FLASH_SECTIONS=y +# CONFIG_APP_REPRODUCIBLE_BUILD is not set +# CONFIG_APP_NO_BLOBS is not set +# CONFIG_APP_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +# CONFIG_APP_COMPATIBLE_PRE_V3_1_BOOTLOADERS is not set +# end of Build type + +# +# Bootloader config +# + +# +# Bootloader manager +# +CONFIG_BOOTLOADER_COMPILE_TIME_DATE=y +CONFIG_BOOTLOADER_PROJECT_VER=1 +# end of Bootloader manager + +CONFIG_BOOTLOADER_OFFSET_IN_FLASH=0x1000 +CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_DEBUG is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_PERF is not set +# CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_NONE is not set + +# +# Log +# +# CONFIG_BOOTLOADER_LOG_LEVEL_NONE is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_ERROR is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_WARN is not set +CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y +# CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG is not set +# CONFIG_BOOTLOADER_LOG_LEVEL_VERBOSE is not set +CONFIG_BOOTLOADER_LOG_LEVEL=3 + +# +# Format +# +# CONFIG_BOOTLOADER_LOG_COLORS is not set +CONFIG_BOOTLOADER_LOG_TIMESTAMP_SOURCE_CPU_TICKS=y +# end of Format +# end of Log + +# +# Serial Flash Configurations +# +# CONFIG_BOOTLOADER_FLASH_DC_AWARE is not set +CONFIG_BOOTLOADER_FLASH_XMC_SUPPORT=y +# end of Serial Flash Configurations + +# CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_8V is not set +CONFIG_BOOTLOADER_VDDSDIO_BOOST_1_9V=y +# CONFIG_BOOTLOADER_FACTORY_RESET is not set +# CONFIG_BOOTLOADER_APP_TEST is not set +CONFIG_BOOTLOADER_REGION_PROTECTION_ENABLE=y +CONFIG_BOOTLOADER_WDT_ENABLE=y +# CONFIG_BOOTLOADER_WDT_DISABLE_IN_USER_CODE is not set +CONFIG_BOOTLOADER_WDT_TIME_MS=9000 +# CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ON_POWER_ON is not set +# CONFIG_BOOTLOADER_SKIP_VALIDATE_ALWAYS is not set +CONFIG_BOOTLOADER_RESERVE_RTC_SIZE=0 +# CONFIG_BOOTLOADER_CUSTOM_RESERVE_RTC is not set +# end of Bootloader config + +# +# Security features +# +CONFIG_SECURE_BOOT_V1_SUPPORTED=y +# CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT is not set +# CONFIG_SECURE_BOOT is not set +# CONFIG_SECURE_FLASH_ENC_ENABLED is not set +# end of Security features + +# +# Application manager +# +CONFIG_APP_COMPILE_TIME_DATE=y +# CONFIG_APP_EXCLUDE_PROJECT_VER_VAR is not set +# CONFIG_APP_EXCLUDE_PROJECT_NAME_VAR is not set +# CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=9 +# end of Application manager + +CONFIG_ESP_ROM_HAS_CRC_LE=y +CONFIG_ESP_ROM_HAS_CRC_BE=y +CONFIG_ESP_ROM_HAS_MZ_CRC32=y +CONFIG_ESP_ROM_HAS_JPEG_DECODE=y +CONFIG_ESP_ROM_HAS_UART_BUF_SWITCH=y +CONFIG_ESP_ROM_NEEDS_SWSETUP_WORKAROUND=y +CONFIG_ESP_ROM_HAS_NEWLIB=y +CONFIG_ESP_ROM_HAS_NEWLIB_NANO_FORMAT=y +CONFIG_ESP_ROM_HAS_NEWLIB_32BIT_TIME=y +CONFIG_ESP_ROM_HAS_SW_FLOAT=y +CONFIG_ESP_ROM_USB_OTG_NUM=-1 +CONFIG_ESP_ROM_USB_SERIAL_DEVICE_NUM=-1 +CONFIG_ESP_ROM_SUPPORT_DEEP_SLEEP_WAKEUP_STUB=y +CONFIG_ESP_ROM_HAS_OUTPUT_PUTC_FUNC=y + +# +# Serial flasher config +# +# CONFIG_ESPTOOLPY_NO_STUB is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QIO is not set +# CONFIG_ESPTOOLPY_FLASHMODE_QOUT is not set +CONFIG_ESPTOOLPY_FLASHMODE_DIO=y +# CONFIG_ESPTOOLPY_FLASHMODE_DOUT is not set +CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y +CONFIG_ESPTOOLPY_FLASHMODE="dio" +# CONFIG_ESPTOOLPY_FLASHFREQ_80M is not set +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# CONFIG_ESPTOOLPY_FLASHFREQ_26M is not set +# CONFIG_ESPTOOLPY_FLASHFREQ_20M is not set +CONFIG_ESPTOOLPY_FLASHFREQ="40m" +# CONFIG_ESPTOOLPY_FLASHSIZE_1MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y +# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_8MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_16MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_32MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_64MB is not set +# CONFIG_ESPTOOLPY_FLASHSIZE_128MB is not set +CONFIG_ESPTOOLPY_FLASHSIZE="2MB" +# CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE is not set +CONFIG_ESPTOOLPY_BEFORE_RESET=y +# CONFIG_ESPTOOLPY_BEFORE_NORESET is not set +CONFIG_ESPTOOLPY_BEFORE="default_reset" +CONFIG_ESPTOOLPY_AFTER_RESET=y +# CONFIG_ESPTOOLPY_AFTER_NORESET is not set +CONFIG_ESPTOOLPY_AFTER="hard_reset" +CONFIG_ESPTOOLPY_MONITOR_BAUD=115200 +# end of Serial flasher config + +# +# Partition Table +# +CONFIG_PARTITION_TABLE_SINGLE_APP=y +# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set +# CONFIG_PARTITION_TABLE_TWO_OTA is not set +# CONFIG_PARTITION_TABLE_TWO_OTA_LARGE is not set +# CONFIG_PARTITION_TABLE_CUSTOM is not set +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions_singleapp.csv" +CONFIG_PARTITION_TABLE_OFFSET=0x8000 +CONFIG_PARTITION_TABLE_MD5=y +# end of Partition Table + +# +# Example Configuration +# +CONFIG_DEVICE_WIFI_SSID="eth2ap" +CONFIG_DEVICE_WIFI_PASSWORD="12345678" +CONFIG_DEVICE_WIFI_CHANNEL=1 +CONFIG_DEVICE_MAX_STA_CONN=4 +# end of Example Configuration + +# +# Example Ethernet Configuration +# +CONFIG_ENV_GPIO_RANGE_MIN=0 +CONFIG_ENV_GPIO_RANGE_MAX=39 +CONFIG_ENV_GPIO_IN_RANGE_MAX=39 +CONFIG_ENV_GPIO_OUT_RANGE_MAX=33 +CONFIG_USE_INTERNAL_ETHERNET=y +# CONFIG_ETH_PHY_GENERIC is not set +# CONFIG_ETH_PHY_IP101 is not set +# CONFIG_ETH_PHY_RTL8201 is not set +CONFIG_ETH_PHY_LAN87XX=y +# CONFIG_ETH_PHY_DP83848 is not set +# CONFIG_ETH_PHY_KSZ80XX is not set +CONFIG_ETH_MDC_GPIO=23 +CONFIG_ETH_MDIO_GPIO=33 +CONFIG_ETH_PHY_RST_GPIO=5 +CONFIG_ETH_PHY_ADDR=1 +# CONFIG_USE_SPI_ETHERNET is not set +# end of Example Ethernet Configuration + +# +# Compiler options +# +CONFIG_COMPILER_OPTIMIZATION_DEBUG=y +# CONFIG_COMPILER_OPTIMIZATION_SIZE is not set +# CONFIG_COMPILER_OPTIMIZATION_PERF is not set +# CONFIG_COMPILER_OPTIMIZATION_NONE is not set +CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE=y +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE is not set +CONFIG_COMPILER_ASSERT_NDEBUG_EVALUATE=y +CONFIG_COMPILER_FLOAT_LIB_FROM_GCCLIB=y +CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=2 +# CONFIG_COMPILER_OPTIMIZATION_CHECKS_SILENT is not set +CONFIG_COMPILER_HIDE_PATHS_MACROS=y +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_COMPILER_CXX_EXCEPTIONS_EMG_POOL_SIZE=0 +# CONFIG_COMPILER_CXX_RTTI is not set +CONFIG_COMPILER_STACK_CHECK_MODE_NONE=y +# CONFIG_COMPILER_STACK_CHECK_MODE_NORM is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_STRONG is not set +# CONFIG_COMPILER_STACK_CHECK_MODE_ALL is not set +# CONFIG_COMPILER_NO_MERGE_CONSTANTS is not set +# CONFIG_COMPILER_WARN_WRITE_STRINGS is not set +CONFIG_COMPILER_DISABLE_DEFAULT_ERRORS=y +# CONFIG_COMPILER_DISABLE_GCC12_WARNINGS is not set +# CONFIG_COMPILER_DISABLE_GCC13_WARNINGS is not set +# CONFIG_COMPILER_DISABLE_GCC14_WARNINGS is not set +# CONFIG_COMPILER_DUMP_RTL_FILES is not set +CONFIG_COMPILER_RT_LIB_GCCLIB=y +CONFIG_COMPILER_RT_LIB_NAME="gcc" +CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y +# CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE is not set +# CONFIG_COMPILER_STATIC_ANALYZER is not set +# end of Compiler options + +# +# Component config +# + +# +# Application Level Tracing +# +# CONFIG_APPTRACE_DEST_JTAG is not set +CONFIG_APPTRACE_DEST_NONE=y +# CONFIG_APPTRACE_DEST_UART1 is not set +# CONFIG_APPTRACE_DEST_UART2 is not set +CONFIG_APPTRACE_DEST_UART_NONE=y +CONFIG_APPTRACE_UART_TASK_PRIO=1 +CONFIG_APPTRACE_LOCK_ENABLE=y +# end of Application Level Tracing + +# +# Bluetooth +# +# CONFIG_BT_ENABLED is not set + +# +# Common Options +# + +# +# BLE Log +# +# CONFIG_BLE_LOG_ENABLED is not set +# end of BLE Log + +# CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set +# CONFIG_BT_BLE_LOG_UHCI_OUT_ENABLED is not set +# end of Common Options +# end of Bluetooth + +# +# Console Library +# +# CONFIG_CONSOLE_SORTED_HELP is not set +# end of Console Library + +# +# Driver Configurations +# + +# +# TWAI Configuration +# +# CONFIG_TWAI_ISR_IN_IRAM is not set +CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC=y +CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST=y +CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID=y +CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT=y +CONFIG_TWAI_ERRATA_FIX_LISTEN_ONLY_DOM=y +# end of TWAI Configuration + +# +# Legacy ADC Driver Configuration +# +CONFIG_ADC_DISABLE_DAC=y +# CONFIG_ADC_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_ADC_SKIP_LEGACY_CONFLICT_CHECK is not set + +# +# Legacy ADC Calibration Configuration +# +CONFIG_ADC_CAL_EFUSE_TP_ENABLE=y +CONFIG_ADC_CAL_EFUSE_VREF_ENABLE=y +CONFIG_ADC_CAL_LUT_ENABLE=y +# CONFIG_ADC_CALI_SUPPRESS_DEPRECATE_WARN is not set +# end of Legacy ADC Calibration Configuration +# end of Legacy ADC Driver Configuration + +# +# Legacy DAC Driver Configurations +# +# CONFIG_DAC_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_DAC_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy DAC Driver Configurations + +# +# Legacy MCPWM Driver Configurations +# +# CONFIG_MCPWM_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_MCPWM_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy MCPWM Driver Configurations + +# +# Legacy Timer Group Driver Configurations +# +# CONFIG_GPTIMER_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_GPTIMER_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy Timer Group Driver Configurations + +# +# Legacy RMT Driver Configurations +# +# CONFIG_RMT_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_RMT_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy RMT Driver Configurations + +# +# Legacy I2S Driver Configurations +# +# CONFIG_I2S_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_I2S_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy I2S Driver Configurations + +# +# Legacy I2C Driver Configurations +# +# CONFIG_I2C_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy I2C Driver Configurations + +# +# Legacy PCNT Driver Configurations +# +# CONFIG_PCNT_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_PCNT_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy PCNT Driver Configurations + +# +# Legacy SDM Driver Configurations +# +# CONFIG_SDM_SUPPRESS_DEPRECATE_WARN is not set +# CONFIG_SDM_SKIP_LEGACY_CONFLICT_CHECK is not set +# end of Legacy SDM Driver Configurations +# end of Driver Configurations + +# +# eFuse Bit Manager +# +# CONFIG_EFUSE_CUSTOM_TABLE is not set +# CONFIG_EFUSE_VIRTUAL is not set +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_NONE is not set +CONFIG_EFUSE_CODE_SCHEME_COMPAT_3_4=y +# CONFIG_EFUSE_CODE_SCHEME_COMPAT_REPEAT is not set +CONFIG_EFUSE_MAX_BLK_LEN=192 +# end of eFuse Bit Manager + +# +# ESP-TLS +# +CONFIG_ESP_TLS_USING_MBEDTLS=y +# CONFIG_ESP_TLS_USE_SECURE_ELEMENT is not set +# CONFIG_ESP_TLS_CLIENT_SESSION_TICKETS is not set +# CONFIG_ESP_TLS_SERVER_SESSION_TICKETS is not set +# CONFIG_ESP_TLS_SERVER_CERT_SELECT_HOOK is not set +# CONFIG_ESP_TLS_SERVER_MIN_AUTH_MODE_OPTIONAL is not set +# CONFIG_ESP_TLS_PSK_VERIFICATION is not set +# CONFIG_ESP_TLS_INSECURE is not set +CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y +# end of ESP-TLS + +# +# ADC and ADC Calibration +# +# CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM is not set +# CONFIG_ADC_CONTINUOUS_ISR_IRAM_SAFE is not set + +# +# ADC Calibration Configurations +# +CONFIG_ADC_CALI_EFUSE_TP_ENABLE=y +CONFIG_ADC_CALI_EFUSE_VREF_ENABLE=y +CONFIG_ADC_CALI_LUT_ENABLE=y +# end of ADC Calibration Configurations + +CONFIG_ADC_DISABLE_DAC_OUTPUT=y +# CONFIG_ADC_ENABLE_DEBUG_LOG is not set +# end of ADC and ADC Calibration + +# +# Wireless Coexistence +# +CONFIG_ESP_COEX_ENABLED=y +# CONFIG_ESP_COEX_GPIO_DEBUG is not set +# end of Wireless Coexistence + +# +# Common ESP-related +# +CONFIG_ESP_ERR_TO_NAME_LOOKUP=y +# end of Common ESP-related + +# +# ESP-Driver:DAC Configurations +# +# CONFIG_DAC_CTRL_FUNC_IN_IRAM is not set +# CONFIG_DAC_ISR_IRAM_SAFE is not set +# CONFIG_DAC_ENABLE_DEBUG_LOG is not set +CONFIG_DAC_DMA_AUTO_16BIT_ALIGN=y +# end of ESP-Driver:DAC Configurations + +# +# ESP-Driver:GPIO Configurations +# +# CONFIG_GPIO_ESP32_SUPPORT_SWITCH_SLP_PULL is not set +# CONFIG_GPIO_CTRL_FUNC_IN_IRAM is not set +# end of ESP-Driver:GPIO Configurations + +# +# ESP-Driver:GPTimer Configurations +# +CONFIG_GPTIMER_ISR_HANDLER_IN_IRAM=y +# CONFIG_GPTIMER_CTRL_FUNC_IN_IRAM is not set +# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set +CONFIG_GPTIMER_OBJ_CACHE_SAFE=y +# CONFIG_GPTIMER_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:GPTimer Configurations + +# +# ESP-Driver:I2C Configurations +# +# CONFIG_I2C_ISR_IRAM_SAFE is not set +# CONFIG_I2C_ENABLE_DEBUG_LOG is not set +# CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2 is not set +# end of ESP-Driver:I2C Configurations + +# +# ESP-Driver:I2S Configurations +# +# CONFIG_I2S_ISR_IRAM_SAFE is not set +# CONFIG_I2S_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:I2S Configurations + +# +# ESP-Driver:LEDC Configurations +# +# CONFIG_LEDC_CTRL_FUNC_IN_IRAM is not set +# end of ESP-Driver:LEDC Configurations + +# +# ESP-Driver:MCPWM Configurations +# +# CONFIG_MCPWM_ISR_IRAM_SAFE is not set +# CONFIG_MCPWM_CTRL_FUNC_IN_IRAM is not set +# CONFIG_MCPWM_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:MCPWM Configurations + +# +# ESP-Driver:PCNT Configurations +# +# CONFIG_PCNT_CTRL_FUNC_IN_IRAM is not set +# CONFIG_PCNT_ISR_IRAM_SAFE is not set +# CONFIG_PCNT_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:PCNT Configurations + +# +# ESP-Driver:RMT Configurations +# +# CONFIG_RMT_ISR_IRAM_SAFE is not set +# CONFIG_RMT_RECV_FUNC_IN_IRAM is not set +# CONFIG_RMT_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:RMT Configurations + +# +# ESP-Driver:Sigma Delta Modulator Configurations +# +# CONFIG_SDM_CTRL_FUNC_IN_IRAM is not set +# CONFIG_SDM_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:Sigma Delta Modulator Configurations + +# +# ESP-Driver:SPI Configurations +# +# CONFIG_SPI_MASTER_IN_IRAM is not set +CONFIG_SPI_MASTER_ISR_IN_IRAM=y +# CONFIG_SPI_SLAVE_IN_IRAM is not set +CONFIG_SPI_SLAVE_ISR_IN_IRAM=y +# end of ESP-Driver:SPI Configurations + +# +# ESP-Driver:Touch Sensor Configurations +# +# CONFIG_TOUCH_CTRL_FUNC_IN_IRAM is not set +# CONFIG_TOUCH_ISR_IRAM_SAFE is not set +# CONFIG_TOUCH_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:Touch Sensor Configurations + +# +# ESP-Driver:UART Configurations +# +# CONFIG_UART_ISR_IN_IRAM is not set +# end of ESP-Driver:UART Configurations + +# +# ESP-Driver:UHCI Configurations +# +# CONFIG_UHCI_ISR_HANDLER_IN_IRAM is not set +# CONFIG_UHCI_ISR_CACHE_SAFE is not set +# CONFIG_UHCI_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:UHCI Configurations + +# +# Ethernet +# +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y +CONFIG_ETH_PHY_INTERFACE_RMII=y +CONFIG_ETH_RMII_CLK_INPUT=y +# CONFIG_ETH_RMII_CLK_OUTPUT is not set +CONFIG_ETH_RMII_CLK_IN_GPIO=0 +CONFIG_ETH_DMA_BUFFER_SIZE=512 +CONFIG_ETH_DMA_RX_BUFFER_NUM=10 +CONFIG_ETH_DMA_TX_BUFFER_NUM=10 +# CONFIG_ETH_IRAM_OPTIMIZATION is not set +CONFIG_ETH_USE_SPI_ETHERNET=y +# CONFIG_ETH_SPI_ETHERNET_DM9051 is not set +# CONFIG_ETH_SPI_ETHERNET_W5500 is not set +# CONFIG_ETH_SPI_ETHERNET_KSZ8851SNL is not set +# CONFIG_ETH_USE_OPENETH is not set +# CONFIG_ETH_TRANSMIT_MUTEX is not set +# end of Ethernet + +# +# Event Loop Library +# +# CONFIG_ESP_EVENT_LOOP_PROFILING is not set +CONFIG_ESP_EVENT_POST_FROM_ISR=y +CONFIG_ESP_EVENT_POST_FROM_IRAM_ISR=y +# end of Event Loop Library + +# +# GDB Stub +# +CONFIG_ESP_GDBSTUB_ENABLED=y +# CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME is not set +CONFIG_ESP_GDBSTUB_SUPPORT_TASKS=y +CONFIG_ESP_GDBSTUB_MAX_TASKS=32 +# end of GDB Stub + +# +# ESP HID +# +CONFIG_ESPHID_TASK_SIZE_BT=2048 +CONFIG_ESPHID_TASK_SIZE_BLE=4096 +# end of ESP HID + +# +# ESP HTTP client +# +CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y +# CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH is not set +# CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH is not set +# CONFIG_ESP_HTTP_CLIENT_ENABLE_CUSTOM_TRANSPORT is not set +CONFIG_ESP_HTTP_CLIENT_EVENT_POST_TIMEOUT=2000 +# end of ESP HTTP client + +# +# HTTP Server +# +CONFIG_HTTPD_MAX_REQ_HDR_LEN=512 +CONFIG_HTTPD_MAX_URI_LEN=512 +CONFIG_HTTPD_ERR_RESP_NO_DELAY=y +CONFIG_HTTPD_PURGE_BUF_LEN=32 +# CONFIG_HTTPD_LOG_PURGE_DATA is not set +# CONFIG_HTTPD_WS_SUPPORT is not set +# CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set +CONFIG_HTTPD_SERVER_EVENT_POST_TIMEOUT=2000 +# end of HTTP Server + +# +# ESP HTTPS OTA +# +# CONFIG_ESP_HTTPS_OTA_DECRYPT_CB is not set +# CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP is not set +CONFIG_ESP_HTTPS_OTA_EVENT_POST_TIMEOUT=2000 +# end of ESP HTTPS OTA + +# +# ESP HTTPS server +# +# CONFIG_ESP_HTTPS_SERVER_ENABLE is not set +CONFIG_ESP_HTTPS_SERVER_EVENT_POST_TIMEOUT=2000 +# end of ESP HTTPS server + +# +# Hardware Settings +# + +# +# Chip revision +# +CONFIG_ESP32_REV_MIN_0=y +# CONFIG_ESP32_REV_MIN_1 is not set +# CONFIG_ESP32_REV_MIN_1_1 is not set +# CONFIG_ESP32_REV_MIN_2 is not set +# CONFIG_ESP32_REV_MIN_3 is not set +# CONFIG_ESP32_REV_MIN_3_1 is not set +CONFIG_ESP32_REV_MIN=0 +CONFIG_ESP32_REV_MIN_FULL=0 +CONFIG_ESP_REV_MIN_FULL=0 + +# +# Maximum Supported ESP32 Revision (Rev v3.99) +# +CONFIG_ESP32_REV_MAX_FULL=399 +CONFIG_ESP_REV_MAX_FULL=399 +CONFIG_ESP_EFUSE_BLOCK_REV_MIN_FULL=0 +CONFIG_ESP_EFUSE_BLOCK_REV_MAX_FULL=99 + +# +# Maximum Supported ESP32 eFuse Block Revision (eFuse Block Rev v0.99) +# +# end of Chip revision + +# +# MAC Config +# +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_STA=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_WIFI_AP=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_BT=y +CONFIG_ESP_MAC_ADDR_UNIVERSE_ETH=y +CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES_FOUR=y +CONFIG_ESP_MAC_UNIVERSAL_MAC_ADDRESSES=4 +# CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_TWO is not set +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES_FOUR=y +CONFIG_ESP32_UNIVERSAL_MAC_ADDRESSES=4 +# CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR is not set +# CONFIG_ESP_MAC_USE_CUSTOM_MAC_AS_BASE_MAC is not set +# end of MAC Config + +# +# Sleep Config +# +# CONFIG_ESP_SLEEP_POWER_DOWN_FLASH is not set +CONFIG_ESP_SLEEP_FLASH_LEAKAGE_WORKAROUND=y +# CONFIG_ESP_SLEEP_MSPI_NEED_ALL_IO_PU is not set +CONFIG_ESP_SLEEP_RTC_BUS_ISO_WORKAROUND=y +# CONFIG_ESP_SLEEP_GPIO_RESET_WORKAROUND is not set +CONFIG_ESP_SLEEP_WAIT_FLASH_READY_EXTRA_DELAY=2000 +# CONFIG_ESP_SLEEP_CACHE_SAFE_ASSERTION is not set +# CONFIG_ESP_SLEEP_DEBUG is not set +CONFIG_ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS=y +# end of Sleep Config + +# +# RTC Clock Config +# +CONFIG_RTC_CLK_SRC_INT_RC=y +# CONFIG_RTC_CLK_SRC_EXT_CRYS is not set +# CONFIG_RTC_CLK_SRC_EXT_OSC is not set +# CONFIG_RTC_CLK_SRC_INT_8MD256 is not set +CONFIG_RTC_CLK_CAL_CYCLES=1024 +# end of RTC Clock Config + +# +# Peripheral Control +# +# CONFIG_PERIPH_CTRL_FUNC_IN_IRAM is not set +# end of Peripheral Control + +# +# Main XTAL Config +# +# CONFIG_XTAL_FREQ_26 is not set +# CONFIG_XTAL_FREQ_32 is not set +CONFIG_XTAL_FREQ_40=y +# CONFIG_XTAL_FREQ_AUTO is not set +CONFIG_XTAL_FREQ=40 +# end of Main XTAL Config + +CONFIG_ESP_SPI_BUS_LOCK_ISR_FUNCS_IN_IRAM=y +# end of Hardware Settings + +# +# ESP-Driver:LCD Controller Configurations +# +# CONFIG_LCD_ENABLE_DEBUG_LOG is not set +# end of ESP-Driver:LCD Controller Configurations + +# +# ESP-MM: Memory Management Configurations +# +# end of ESP-MM: Memory Management Configurations + +# +# ESP NETIF Adapter +# +CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=120 +# CONFIG_ESP_NETIF_PROVIDE_CUSTOM_IMPLEMENTATION is not set +CONFIG_ESP_NETIF_TCPIP_LWIP=y +# CONFIG_ESP_NETIF_LOOPBACK is not set +CONFIG_ESP_NETIF_USES_TCPIP_WITH_BSD_API=y +CONFIG_ESP_NETIF_REPORT_DATA_TRAFFIC=y +# CONFIG_ESP_NETIF_RECEIVE_REPORT_ERRORS is not set +# CONFIG_ESP_NETIF_L2_TAP is not set +# CONFIG_ESP_NETIF_BRIDGE_EN is not set +# CONFIG_ESP_NETIF_SET_DNS_PER_DEFAULT_NETIF is not set +# end of ESP NETIF Adapter + +# +# Partition API Configuration +# +# end of Partition API Configuration + +# +# PHY +# +CONFIG_ESP_PHY_ENABLED=y +CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP_PHY_MAX_TX_POWER=20 +# CONFIG_ESP_PHY_REDUCE_TX_POWER is not set +# CONFIG_ESP_PHY_ENABLE_CERT_TEST is not set +CONFIG_ESP_PHY_RF_CAL_PARTIAL=y +# CONFIG_ESP_PHY_RF_CAL_NONE is not set +# CONFIG_ESP_PHY_RF_CAL_FULL is not set +CONFIG_ESP_PHY_CALIBRATION_MODE=0 +CONFIG_ESP_PHY_PLL_TRACK_PERIOD_MS=1000 +# CONFIG_ESP_PHY_PLL_TRACK_DEBUG is not set +# CONFIG_ESP_PHY_RECORD_USED_TIME is not set +CONFIG_ESP_PHY_IRAM_OPT=y +# CONFIG_ESP_PHY_DEBUG is not set +# end of PHY + +# +# Power Management +# +# CONFIG_PM_ENABLE is not set +# CONFIG_PM_SLP_IRAM_OPT is not set +# end of Power Management + +# +# ESP PSRAM +# +# CONFIG_SPIRAM is not set +# end of ESP PSRAM + +# +# ESP Ringbuf +# +# CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH is not set +# end of ESP Ringbuf + +# +# ESP Security Specific +# +# end of ESP Security Specific + +# +# ESP System Settings +# +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_80 is not set +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y +# CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240 is not set +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=160 + +# +# Memory +# +# CONFIG_ESP32_USE_FIXED_STATIC_RAM_SIZE is not set + +# +# Non-backward compatible options +# +# CONFIG_ESP_SYSTEM_ESP32_SRAM1_REGION_AS_IRAM is not set +# end of Non-backward compatible options +# end of Memory + +# +# Trace memory +# +# CONFIG_ESP32_TRAX is not set +CONFIG_ESP32_TRACEMEM_RESERVE_DRAM=0x0 +# end of Trace memory + +# CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT is not set +CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT=y +# CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP_SYSTEM_PANIC_GDBSTUB is not set +CONFIG_ESP_SYSTEM_PANIC_REBOOT_DELAY_SECONDS=0 + +# +# Memory protection +# +# end of Memory protection + +CONFIG_ESP_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_ESP_MAIN_TASK_STACK_SIZE=3584 +CONFIG_ESP_MAIN_TASK_AFFINITY_CPU0=y +# CONFIG_ESP_MAIN_TASK_AFFINITY_CPU1 is not set +# CONFIG_ESP_MAIN_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_ESP_MAIN_TASK_AFFINITY=0x0 +CONFIG_ESP_MINIMAL_SHARED_STACK_SIZE=2048 +CONFIG_ESP_CONSOLE_UART_DEFAULT=y +# CONFIG_ESP_CONSOLE_UART_CUSTOM is not set +# CONFIG_ESP_CONSOLE_NONE is not set +CONFIG_ESP_CONSOLE_UART=y +CONFIG_ESP_CONSOLE_UART_NUM=0 +CONFIG_ESP_CONSOLE_ROM_SERIAL_PORT_NUM=0 +CONFIG_ESP_CONSOLE_UART_BAUDRATE=115200 +CONFIG_ESP_INT_WDT=y +CONFIG_ESP_INT_WDT_TIMEOUT_MS=300 +CONFIG_ESP_INT_WDT_CHECK_CPU1=y +CONFIG_ESP_TASK_WDT_EN=y +CONFIG_ESP_TASK_WDT_INIT=y +# CONFIG_ESP_TASK_WDT_PANIC is not set +CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y +# CONFIG_ESP_PANIC_HANDLER_IRAM is not set +# CONFIG_ESP_DEBUG_STUBS_ENABLE is not set +CONFIG_ESP_DEBUG_OCDAWARE=y +# CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_5 is not set +CONFIG_ESP_SYSTEM_CHECK_INT_LEVEL_4=y + +# +# Brownout Detector +# +CONFIG_ESP_BROWNOUT_DET=y +CONFIG_ESP_BROWNOUT_DET_LVL_SEL_0=y +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_ESP_BROWNOUT_DET_LVL_SEL_7 is not set +CONFIG_ESP_BROWNOUT_DET_LVL=0 +# end of Brownout Detector + +# CONFIG_ESP32_DISABLE_BASIC_ROM_CONSOLE is not set +CONFIG_ESP_SYSTEM_BROWNOUT_INTR=y +# end of ESP System Settings + +# +# IPC (Inter-Processor Call) +# +CONFIG_ESP_IPC_TASK_STACK_SIZE=1024 +CONFIG_ESP_IPC_USES_CALLERS_PRIORITY=y +CONFIG_ESP_IPC_ISR_ENABLE=y +# end of IPC (Inter-Processor Call) + +# +# ESP Timer (High Resolution Timer) +# +# CONFIG_ESP_TIMER_PROFILING is not set +CONFIG_ESP_TIME_FUNCS_USE_RTC_TIMER=y +CONFIG_ESP_TIME_FUNCS_USE_ESP_TIMER=y +CONFIG_ESP_TIMER_TASK_STACK_SIZE=3584 +CONFIG_ESP_TIMER_INTERRUPT_LEVEL=1 +# CONFIG_ESP_TIMER_SHOW_EXPERIMENTAL is not set +CONFIG_ESP_TIMER_TASK_AFFINITY=0x0 +CONFIG_ESP_TIMER_TASK_AFFINITY_CPU0=y +CONFIG_ESP_TIMER_ISR_AFFINITY_CPU0=y +# CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD is not set +CONFIG_ESP_TIMER_IMPL_TG0_LAC=y +# end of ESP Timer (High Resolution Timer) + +# +# Wi-Fi +# +CONFIG_ESP_WIFI_ENABLED=y +CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10 +CONFIG_ESP_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +# CONFIG_ESP_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +CONFIG_ESP_WIFI_STATIC_RX_MGMT_BUFFER=y +# CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUFFER is not set +CONFIG_ESP_WIFI_DYNAMIC_RX_MGMT_BUF=0 +CONFIG_ESP_WIFI_RX_MGMT_BUF_NUM_DEF=5 +# CONFIG_ESP_WIFI_CSI_ENABLED is not set +CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP_WIFI_TX_BA_WIN=6 +CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP_WIFI_RX_BA_WIN=6 +CONFIG_ESP_WIFI_NVS_ENABLED=y +CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0=y +# CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1 is not set +CONFIG_ESP_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32 +CONFIG_ESP_WIFI_IRAM_OPT=y +# CONFIG_ESP_WIFI_EXTRA_IRAM_OPT is not set +CONFIG_ESP_WIFI_RX_IRAM_OPT=y +CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y +CONFIG_ESP_WIFI_ENABLE_SAE_PK=y +CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y +CONFIG_ESP_WIFI_ENABLE_WPA3_OWE_STA=y +# CONFIG_ESP_WIFI_SLP_IRAM_OPT is not set +CONFIG_ESP_WIFI_SLP_DEFAULT_MIN_ACTIVE_TIME=50 +CONFIG_ESP_WIFI_SLP_DEFAULT_MAX_ACTIVE_TIME=10 +CONFIG_ESP_WIFI_SLP_DEFAULT_WAIT_BROADCAST_DATA_TIME=15 +CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=y +# CONFIG_ESP_WIFI_GMAC_SUPPORT is not set +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y +# CONFIG_ESP_WIFI_SLP_BEACON_LOST_OPT is not set +CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=7 +# CONFIG_ESP_WIFI_NAN_ENABLE is not set +CONFIG_ESP_WIFI_MBEDTLS_CRYPTO=y +CONFIG_ESP_WIFI_MBEDTLS_TLS_CLIENT=y +# CONFIG_ESP_WIFI_WAPI_PSK is not set +# CONFIG_ESP_WIFI_11KV_SUPPORT is not set +# CONFIG_ESP_WIFI_MBO_SUPPORT is not set +# CONFIG_ESP_WIFI_DPP_SUPPORT is not set +# CONFIG_ESP_WIFI_11R_SUPPORT is not set +# CONFIG_ESP_WIFI_WPS_SOFTAP_REGISTRAR is not set + +# +# WPS Configuration Options +# +# CONFIG_ESP_WIFI_WPS_STRICT is not set +# CONFIG_ESP_WIFI_WPS_PASSPHRASE is not set +# end of WPS Configuration Options + +# CONFIG_ESP_WIFI_DEBUG_PRINT is not set +# CONFIG_ESP_WIFI_TESTING_OPTIONS is not set +CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT=y +# CONFIG_ESP_WIFI_ENT_FREE_DYNAMIC_BUFFER is not set +# end of Wi-Fi + +# +# Core dump +# +# CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH is not set +# CONFIG_ESP_COREDUMP_ENABLE_TO_UART is not set +CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y +# end of Core dump + +# +# FAT Filesystem support +# +CONFIG_FATFS_VOLUME_COUNT=2 +CONFIG_FATFS_LFN_NONE=y +# CONFIG_FATFS_LFN_HEAP is not set +# CONFIG_FATFS_LFN_STACK is not set +# CONFIG_FATFS_SECTOR_512 is not set +CONFIG_FATFS_SECTOR_4096=y +# CONFIG_FATFS_CODEPAGE_DYNAMIC is not set +CONFIG_FATFS_CODEPAGE_437=y +# CONFIG_FATFS_CODEPAGE_720 is not set +# CONFIG_FATFS_CODEPAGE_737 is not set +# CONFIG_FATFS_CODEPAGE_771 is not set +# CONFIG_FATFS_CODEPAGE_775 is not set +# CONFIG_FATFS_CODEPAGE_850 is not set +# CONFIG_FATFS_CODEPAGE_852 is not set +# CONFIG_FATFS_CODEPAGE_855 is not set +# CONFIG_FATFS_CODEPAGE_857 is not set +# CONFIG_FATFS_CODEPAGE_860 is not set +# CONFIG_FATFS_CODEPAGE_861 is not set +# CONFIG_FATFS_CODEPAGE_862 is not set +# CONFIG_FATFS_CODEPAGE_863 is not set +# CONFIG_FATFS_CODEPAGE_864 is not set +# CONFIG_FATFS_CODEPAGE_865 is not set +# CONFIG_FATFS_CODEPAGE_866 is not set +# CONFIG_FATFS_CODEPAGE_869 is not set +# CONFIG_FATFS_CODEPAGE_932 is not set +# CONFIG_FATFS_CODEPAGE_936 is not set +# CONFIG_FATFS_CODEPAGE_949 is not set +# CONFIG_FATFS_CODEPAGE_950 is not set +CONFIG_FATFS_CODEPAGE=437 +CONFIG_FATFS_FS_LOCK=0 +CONFIG_FATFS_TIMEOUT_MS=10000 +CONFIG_FATFS_PER_FILE_CACHE=y +# CONFIG_FATFS_USE_FASTSEEK is not set +CONFIG_FATFS_USE_STRFUNC_NONE=y +# CONFIG_FATFS_USE_STRFUNC_WITHOUT_CRLF_CONV is not set +# CONFIG_FATFS_USE_STRFUNC_WITH_CRLF_CONV is not set +CONFIG_FATFS_VFS_FSTAT_BLKSIZE=0 +# CONFIG_FATFS_IMMEDIATE_FSYNC is not set +# CONFIG_FATFS_USE_LABEL is not set +CONFIG_FATFS_LINK_LOCK=y +# CONFIG_FATFS_USE_DYN_BUFFERS is not set + +# +# File system free space calculation behavior +# +CONFIG_FATFS_DONT_TRUST_FREE_CLUSTER_CNT=0 +CONFIG_FATFS_DONT_TRUST_LAST_ALLOC=0 +# end of File system free space calculation behavior +# end of FAT Filesystem support + +# +# FreeRTOS +# + +# +# Kernel +# +# CONFIG_FREERTOS_SMP is not set +# CONFIG_FREERTOS_UNICORE is not set +CONFIG_FREERTOS_HZ=100 +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_NONE is not set +# CONFIG_FREERTOS_CHECK_STACKOVERFLOW_PTRVAL is not set +CONFIG_FREERTOS_CHECK_STACKOVERFLOW_CANARY=y +CONFIG_FREERTOS_THREAD_LOCAL_STORAGE_POINTERS=1 +CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=1536 +# CONFIG_FREERTOS_USE_IDLE_HOOK is not set +# CONFIG_FREERTOS_USE_TICK_HOOK is not set +CONFIG_FREERTOS_MAX_TASK_NAME_LEN=16 +# CONFIG_FREERTOS_ENABLE_BACKWARD_COMPATIBILITY is not set +CONFIG_FREERTOS_USE_TIMERS=y +CONFIG_FREERTOS_TIMER_SERVICE_TASK_NAME="Tmr Svc" +# CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU0 is not set +# CONFIG_FREERTOS_TIMER_TASK_AFFINITY_CPU1 is not set +CONFIG_FREERTOS_TIMER_TASK_NO_AFFINITY=y +CONFIG_FREERTOS_TIMER_SERVICE_TASK_CORE_AFFINITY=0x7FFFFFFF +CONFIG_FREERTOS_TIMER_TASK_PRIORITY=1 +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_FREERTOS_TIMER_QUEUE_LENGTH=10 +CONFIG_FREERTOS_QUEUE_REGISTRY_SIZE=0 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=1 +# CONFIG_FREERTOS_USE_TRACE_FACILITY is not set +# CONFIG_FREERTOS_USE_LIST_DATA_INTEGRITY_CHECK_BYTES is not set +# CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS is not set +# CONFIG_FREERTOS_USE_APPLICATION_TASK_TAG is not set +# end of Kernel + +# +# Port +# +CONFIG_FREERTOS_TASK_FUNCTION_WRAPPER=y +# CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK is not set +CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS=y +# CONFIG_FREERTOS_TASK_PRE_DELETION_HOOK is not set +# CONFIG_FREERTOS_ENABLE_STATIC_TASK_CLEAN_UP is not set +CONFIG_FREERTOS_CHECK_MUTEX_GIVEN_BY_OWNER=y +CONFIG_FREERTOS_ISR_STACKSIZE=1536 +CONFIG_FREERTOS_INTERRUPT_BACKTRACE=y +# CONFIG_FREERTOS_FPU_IN_ISR is not set +CONFIG_FREERTOS_TICK_SUPPORT_CORETIMER=y +CONFIG_FREERTOS_CORETIMER_0=y +# CONFIG_FREERTOS_CORETIMER_1 is not set +CONFIG_FREERTOS_SYSTICK_USES_CCOUNT=y +# CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH is not set +# CONFIG_FREERTOS_CHECK_PORT_CRITICAL_COMPLIANCE is not set +# end of Port + +# +# Extra +# +# end of Extra + +CONFIG_FREERTOS_PORT=y +CONFIG_FREERTOS_NO_AFFINITY=0x7FFFFFFF +CONFIG_FREERTOS_SUPPORT_STATIC_ALLOCATION=y +CONFIG_FREERTOS_DEBUG_OCDAWARE=y +CONFIG_FREERTOS_ENABLE_TASK_SNAPSHOT=y +CONFIG_FREERTOS_PLACE_SNAPSHOT_FUNS_INTO_FLASH=y +CONFIG_FREERTOS_NUMBER_OF_CORES=2 +# end of FreeRTOS + +# +# Hardware Abstraction Layer (HAL) and Low Level (LL) +# +CONFIG_HAL_ASSERTION_EQUALS_SYSTEM=y +# CONFIG_HAL_ASSERTION_DISABLE is not set +# CONFIG_HAL_ASSERTION_SILENT is not set +# CONFIG_HAL_ASSERTION_ENABLE is not set +CONFIG_HAL_DEFAULT_ASSERTION_LEVEL=2 +CONFIG_HAL_SPI_MASTER_FUNC_IN_IRAM=y +CONFIG_HAL_SPI_SLAVE_FUNC_IN_IRAM=y +# end of Hardware Abstraction Layer (HAL) and Low Level (LL) + +# +# Heap memory debugging +# +CONFIG_HEAP_POISONING_DISABLED=y +# CONFIG_HEAP_POISONING_LIGHT is not set +# CONFIG_HEAP_POISONING_COMPREHENSIVE is not set +CONFIG_HEAP_TRACING_OFF=y +# CONFIG_HEAP_TRACING_STANDALONE is not set +# CONFIG_HEAP_TRACING_TOHOST is not set +# CONFIG_HEAP_USE_HOOKS is not set +# CONFIG_HEAP_TASK_TRACKING is not set +# CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS is not set +# CONFIG_HEAP_PLACE_FUNCTION_INTO_FLASH is not set +# end of Heap memory debugging + +# +# Log +# + +# +# Log Level +# +# CONFIG_LOG_DEFAULT_LEVEL_NONE is not set +# CONFIG_LOG_DEFAULT_LEVEL_ERROR is not set +# CONFIG_LOG_DEFAULT_LEVEL_WARN is not set +CONFIG_LOG_DEFAULT_LEVEL_INFO=y +# CONFIG_LOG_DEFAULT_LEVEL_DEBUG is not set +# CONFIG_LOG_DEFAULT_LEVEL_VERBOSE is not set +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_MAXIMUM_EQUALS_DEFAULT=y +# CONFIG_LOG_MAXIMUM_LEVEL_DEBUG is not set +# CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE is not set +CONFIG_LOG_MAXIMUM_LEVEL=3 + +# +# Level Settings +# +# CONFIG_LOG_MASTER_LEVEL is not set +CONFIG_LOG_DYNAMIC_LEVEL_CONTROL=y +# CONFIG_LOG_TAG_LEVEL_IMPL_NONE is not set +# CONFIG_LOG_TAG_LEVEL_IMPL_LINKED_LIST is not set +CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_AND_LINKED_LIST=y +# CONFIG_LOG_TAG_LEVEL_CACHE_ARRAY is not set +CONFIG_LOG_TAG_LEVEL_CACHE_BINARY_MIN_HEAP=y +CONFIG_LOG_TAG_LEVEL_IMPL_CACHE_SIZE=31 +# end of Level Settings +# end of Log Level + +# +# Format +# +# CONFIG_LOG_COLORS is not set +CONFIG_LOG_TIMESTAMP_SOURCE_RTOS=y +# CONFIG_LOG_TIMESTAMP_SOURCE_SYSTEM is not set +# end of Format +# end of Log + +# +# LWIP +# +CONFIG_LWIP_ENABLE=y +CONFIG_LWIP_LOCAL_HOSTNAME="LeoLights" +# CONFIG_LWIP_NETIF_API is not set +CONFIG_LWIP_TCPIP_TASK_PRIO=18 +# CONFIG_LWIP_TCPIP_CORE_LOCKING is not set +# CONFIG_LWIP_CHECK_THREAD_SAFETY is not set +CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES=y +# CONFIG_LWIP_L2_TO_L3_COPY is not set +# CONFIG_LWIP_IRAM_OPTIMIZATION is not set +# CONFIG_LWIP_EXTRA_IRAM_OPTIMIZATION is not set +CONFIG_LWIP_TIMERS_ONDEMAND=y +CONFIG_LWIP_ND6=y +# CONFIG_LWIP_FORCE_ROUTER_FORWARDING is not set +CONFIG_LWIP_MAX_SOCKETS=10 +# CONFIG_LWIP_USE_ONLY_LWIP_SELECT is not set +# CONFIG_LWIP_SO_LINGER is not set +CONFIG_LWIP_SO_REUSE=y +CONFIG_LWIP_SO_REUSE_RXTOALL=y +# CONFIG_LWIP_SO_RCVBUF is not set +# CONFIG_LWIP_NETBUF_RECVINFO is not set +CONFIG_LWIP_IP_DEFAULT_TTL=64 +CONFIG_LWIP_IP4_FRAG=y +CONFIG_LWIP_IP6_FRAG=y +# CONFIG_LWIP_IP4_REASSEMBLY is not set +# CONFIG_LWIP_IP6_REASSEMBLY is not set +CONFIG_LWIP_IP_REASS_MAX_PBUFS=10 +# CONFIG_LWIP_IP_FORWARD is not set +# CONFIG_LWIP_STATS is not set +CONFIG_LWIP_ESP_GRATUITOUS_ARP=y +CONFIG_LWIP_GARP_TMR_INTERVAL=60 +CONFIG_LWIP_ESP_MLDV6_REPORT=y +CONFIG_LWIP_MLDV6_TMR_INTERVAL=40 +CONFIG_LWIP_TCPIP_RECVMBOX_SIZE=32 +CONFIG_LWIP_DHCP_DOES_ARP_CHECK=y +# CONFIG_LWIP_DHCP_DOES_ACD_CHECK is not set +# CONFIG_LWIP_DHCP_DOES_NOT_CHECK_OFFERED_IP is not set +# CONFIG_LWIP_DHCP_DISABLE_CLIENT_ID is not set +CONFIG_LWIP_DHCP_DISABLE_VENDOR_CLASS_ID=y +# CONFIG_LWIP_DHCP_RESTORE_LAST_IP is not set +CONFIG_LWIP_DHCP_OPTIONS_LEN=69 +CONFIG_LWIP_NUM_NETIF_CLIENT_DATA=0 +CONFIG_LWIP_DHCP_COARSE_TIMER_SECS=1 + +# +# DHCP server +# +CONFIG_LWIP_DHCPS=y +CONFIG_LWIP_DHCPS_LEASE_UNIT=60 +CONFIG_LWIP_DHCPS_MAX_STATION_NUM=8 +CONFIG_LWIP_DHCPS_STATIC_ENTRIES=y +CONFIG_LWIP_DHCPS_ADD_DNS=y +# end of DHCP server + +# CONFIG_LWIP_AUTOIP is not set +CONFIG_LWIP_IPV4=y +CONFIG_LWIP_IPV6=y +# CONFIG_LWIP_IPV6_AUTOCONFIG is not set +CONFIG_LWIP_IPV6_NUM_ADDRESSES=3 +# CONFIG_LWIP_IPV6_FORWARD is not set +# CONFIG_LWIP_NETIF_STATUS_CALLBACK is not set +CONFIG_LWIP_NETIF_LOOPBACK=y +CONFIG_LWIP_LOOPBACK_MAX_PBUFS=8 + +# +# TCP +# +CONFIG_LWIP_MAX_ACTIVE_TCP=16 +CONFIG_LWIP_MAX_LISTENING_TCP=16 +CONFIG_LWIP_TCP_HIGH_SPEED_RETRANSMISSION=y +CONFIG_LWIP_TCP_MAXRTX=12 +CONFIG_LWIP_TCP_SYNMAXRTX=12 +CONFIG_LWIP_TCP_MSS=1440 +CONFIG_LWIP_TCP_TMR_INTERVAL=250 +CONFIG_LWIP_TCP_MSL=60000 +CONFIG_LWIP_TCP_FIN_WAIT_TIMEOUT=20000 +CONFIG_LWIP_TCP_SND_BUF_DEFAULT=5760 +CONFIG_LWIP_TCP_WND_DEFAULT=5760 +CONFIG_LWIP_TCP_RECVMBOX_SIZE=6 +CONFIG_LWIP_TCP_ACCEPTMBOX_SIZE=6 +CONFIG_LWIP_TCP_QUEUE_OOSEQ=y +CONFIG_LWIP_TCP_OOSEQ_TIMEOUT=6 +CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 +# CONFIG_LWIP_TCP_SACK_OUT is not set +CONFIG_LWIP_TCP_OVERSIZE_MSS=y +# CONFIG_LWIP_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_LWIP_TCP_OVERSIZE_DISABLE is not set +CONFIG_LWIP_TCP_RTO_TIME=1500 +# end of TCP + +# +# UDP +# +CONFIG_LWIP_MAX_UDP_PCBS=16 +CONFIG_LWIP_UDP_RECVMBOX_SIZE=6 +# end of UDP + +# +# Checksums +# +# CONFIG_LWIP_CHECKSUM_CHECK_IP is not set +# CONFIG_LWIP_CHECKSUM_CHECK_UDP is not set +CONFIG_LWIP_CHECKSUM_CHECK_ICMP=y +# end of Checksums + +CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=3072 +CONFIG_LWIP_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU0 is not set +# CONFIG_LWIP_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_LWIP_TCPIP_TASK_AFFINITY=0x7FFFFFFF +CONFIG_LWIP_IPV6_MEMP_NUM_ND6_QUEUE=3 +CONFIG_LWIP_IPV6_ND6_NUM_NEIGHBORS=5 +CONFIG_LWIP_IPV6_ND6_NUM_PREFIXES=5 +CONFIG_LWIP_IPV6_ND6_NUM_ROUTERS=3 +CONFIG_LWIP_IPV6_ND6_NUM_DESTINATIONS=10 +# CONFIG_LWIP_IPV6_ND6_ROUTE_INFO_OPTION_SUPPORT is not set +# CONFIG_LWIP_PPP_SUPPORT is not set +# CONFIG_LWIP_SLIP_SUPPORT is not set + +# +# ICMP +# +CONFIG_LWIP_ICMP=y +# CONFIG_LWIP_MULTICAST_PING is not set +# CONFIG_LWIP_BROADCAST_PING is not set +# end of ICMP + +# +# LWIP RAW API +# +CONFIG_LWIP_MAX_RAW_PCBS=16 +# end of LWIP RAW API + +# +# SNTP +# +CONFIG_LWIP_SNTP_MAX_SERVERS=1 +# CONFIG_LWIP_DHCP_GET_NTP_SRV is not set +CONFIG_LWIP_SNTP_UPDATE_DELAY=3600000 +CONFIG_LWIP_SNTP_STARTUP_DELAY=y +CONFIG_LWIP_SNTP_MAXIMUM_STARTUP_DELAY=5000 +# end of SNTP + +# +# DNS +# +CONFIG_LWIP_DNS_MAX_HOST_IP=1 +CONFIG_LWIP_DNS_MAX_SERVERS=3 +# CONFIG_LWIP_FALLBACK_DNS_SERVER_SUPPORT is not set +# CONFIG_LWIP_DNS_SETSERVER_WITH_NETIF is not set +# end of DNS + +CONFIG_LWIP_BRIDGEIF_MAX_PORTS=7 +CONFIG_LWIP_ESP_LWIP_ASSERT=y + +# +# Hooks +# +# CONFIG_LWIP_HOOK_TCP_ISN_NONE is not set +CONFIG_LWIP_HOOK_TCP_ISN_DEFAULT=y +# CONFIG_LWIP_HOOK_TCP_ISN_CUSTOM is not set +CONFIG_LWIP_HOOK_IP6_ROUTE_NONE=y +# CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT is not set +# CONFIG_LWIP_HOOK_IP6_ROUTE_CUSTOM is not set +CONFIG_LWIP_HOOK_ND6_GET_GW_NONE=y +# CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT is not set +# CONFIG_LWIP_HOOK_ND6_GET_GW_CUSTOM is not set +CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_NONE=y +# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_DEFAULT is not set +# CONFIG_LWIP_HOOK_IP6_SELECT_SRC_ADDR_CUSTOM is not set +CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_NONE=y +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_DEFAULT is not set +# CONFIG_LWIP_HOOK_NETCONN_EXT_RESOLVE_CUSTOM is not set +CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_NONE=y +# CONFIG_LWIP_HOOK_DNS_EXT_RESOLVE_CUSTOM is not set +# CONFIG_LWIP_HOOK_IP6_INPUT_NONE is not set +CONFIG_LWIP_HOOK_IP6_INPUT_DEFAULT=y +# CONFIG_LWIP_HOOK_IP6_INPUT_CUSTOM is not set +# end of Hooks + +# CONFIG_LWIP_DEBUG is not set +# end of LWIP + +# +# mbedTLS +# +CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC=y +# CONFIG_MBEDTLS_DEFAULT_MEM_ALLOC is not set +# CONFIG_MBEDTLS_CUSTOM_MEM_ALLOC is not set +CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y +CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 +CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 +# CONFIG_MBEDTLS_DYNAMIC_BUFFER is not set +# CONFIG_MBEDTLS_DEBUG is not set + +# +# mbedTLS v3.x related +# +# CONFIG_MBEDTLS_SSL_PROTO_TLS1_3 is not set +# CONFIG_MBEDTLS_SSL_VARIABLE_BUFFER_LENGTH is not set +# CONFIG_MBEDTLS_X509_TRUSTED_CERT_CALLBACK is not set +# CONFIG_MBEDTLS_SSL_CONTEXT_SERIALIZATION is not set +CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE=y +# CONFIG_MBEDTLS_SSL_KEYING_MATERIAL_EXPORT is not set +CONFIG_MBEDTLS_PKCS7_C=y +# end of mbedTLS v3.x related + +# +# Certificate Bundle +# +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN is not set +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE is not set +# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE is not set +# CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEPRECATED_LIST is not set +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_MAX_CERTS=200 +# end of Certificate Bundle + +# CONFIG_MBEDTLS_ECP_RESTARTABLE is not set +CONFIG_MBEDTLS_CMAC_C=y +CONFIG_MBEDTLS_HARDWARE_AES=y +CONFIG_MBEDTLS_GCM_SUPPORT_NON_AES_CIPHER=y +CONFIG_MBEDTLS_HARDWARE_MPI=y +# CONFIG_MBEDTLS_LARGE_KEY_SOFTWARE_MPI is not set +CONFIG_MBEDTLS_HARDWARE_SHA=y +CONFIG_MBEDTLS_ROM_MD5=y +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_SIGN is not set +# CONFIG_MBEDTLS_ATCA_HW_ECDSA_VERIFY is not set +CONFIG_MBEDTLS_HAVE_TIME=y +# CONFIG_MBEDTLS_PLATFORM_TIME_ALT is not set +# CONFIG_MBEDTLS_HAVE_TIME_DATE is not set +CONFIG_MBEDTLS_ECDSA_DETERMINISTIC=y +CONFIG_MBEDTLS_SHA1_C=y +CONFIG_MBEDTLS_SHA512_C=y +# CONFIG_MBEDTLS_SHA3_C is not set +CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT=y +# CONFIG_MBEDTLS_TLS_SERVER_ONLY is not set +# CONFIG_MBEDTLS_TLS_CLIENT_ONLY is not set +# CONFIG_MBEDTLS_TLS_DISABLED is not set +CONFIG_MBEDTLS_TLS_SERVER=y +CONFIG_MBEDTLS_TLS_CLIENT=y +CONFIG_MBEDTLS_TLS_ENABLED=y + +# +# TLS Key Exchange Methods +# +# CONFIG_MBEDTLS_PSK_MODES is not set +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ELLIPTIC_CURVE=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_RSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA=y +CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA=y +# end of TLS Key Exchange Methods + +CONFIG_MBEDTLS_SSL_RENEGOTIATION=y +CONFIG_MBEDTLS_SSL_PROTO_TLS1_2=y +# CONFIG_MBEDTLS_SSL_PROTO_GMTSSL1_1 is not set +# CONFIG_MBEDTLS_SSL_PROTO_DTLS is not set +CONFIG_MBEDTLS_SSL_ALPN=y +CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS=y +CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS=y + +# +# Symmetric Ciphers +# +CONFIG_MBEDTLS_AES_C=y +# CONFIG_MBEDTLS_CAMELLIA_C is not set +# CONFIG_MBEDTLS_DES_C is not set +# CONFIG_MBEDTLS_BLOWFISH_C is not set +# CONFIG_MBEDTLS_XTEA_C is not set +CONFIG_MBEDTLS_CCM_C=y +CONFIG_MBEDTLS_GCM_C=y +# CONFIG_MBEDTLS_NIST_KW_C is not set +# end of Symmetric Ciphers + +# CONFIG_MBEDTLS_RIPEMD160_C is not set + +# +# Certificates +# +CONFIG_MBEDTLS_PEM_PARSE_C=y +CONFIG_MBEDTLS_PEM_WRITE_C=y +CONFIG_MBEDTLS_X509_CRL_PARSE_C=y +CONFIG_MBEDTLS_X509_CSR_PARSE_C=y +# end of Certificates + +CONFIG_MBEDTLS_ECP_C=y +CONFIG_MBEDTLS_PK_PARSE_EC_EXTENDED=y +CONFIG_MBEDTLS_PK_PARSE_EC_COMPRESSED=y +# CONFIG_MBEDTLS_DHM_C is not set +CONFIG_MBEDTLS_ECDH_C=y +CONFIG_MBEDTLS_ECDSA_C=y +# CONFIG_MBEDTLS_ECJPAKE_C is not set +CONFIG_MBEDTLS_ECP_DP_SECP192R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP521R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP192K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP224K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_SECP256K1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP256R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP384R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_BP512R1_ENABLED=y +CONFIG_MBEDTLS_ECP_DP_CURVE25519_ENABLED=y +CONFIG_MBEDTLS_ECP_NIST_OPTIM=y +# CONFIG_MBEDTLS_ECP_FIXED_POINT_OPTIM is not set +# CONFIG_MBEDTLS_POLY1305_C is not set +# CONFIG_MBEDTLS_CHACHA20_C is not set +# CONFIG_MBEDTLS_HKDF_C is not set +# CONFIG_MBEDTLS_THREADING_C is not set +CONFIG_MBEDTLS_ERROR_STRINGS=y +CONFIG_MBEDTLS_FS_IO=y +# CONFIG_MBEDTLS_ALLOW_WEAK_CERTIFICATE_VERIFICATION is not set +# end of mbedTLS + +# +# ESP-MQTT Configurations +# +CONFIG_MQTT_PROTOCOL_311=y +# CONFIG_MQTT_PROTOCOL_5 is not set +CONFIG_MQTT_TRANSPORT_SSL=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET=y +CONFIG_MQTT_TRANSPORT_WEBSOCKET_SECURE=y +# CONFIG_MQTT_MSG_ID_INCREMENTAL is not set +# CONFIG_MQTT_SKIP_PUBLISH_IF_DISCONNECTED is not set +# CONFIG_MQTT_REPORT_DELETED_MESSAGES is not set +# CONFIG_MQTT_USE_CUSTOM_CONFIG is not set +# CONFIG_MQTT_TASK_CORE_SELECTION_ENABLED is not set +# CONFIG_MQTT_CUSTOM_OUTBOX is not set +# end of ESP-MQTT Configurations + +# +# Newlib +# +CONFIG_NEWLIB_STDOUT_LINE_ENDING_CRLF=y +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_LF is not set +# CONFIG_NEWLIB_STDOUT_LINE_ENDING_CR is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_CRLF is not set +# CONFIG_NEWLIB_STDIN_LINE_ENDING_LF is not set +CONFIG_NEWLIB_STDIN_LINE_ENDING_CR=y +# CONFIG_NEWLIB_NANO_FORMAT is not set +CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC_HRT=y +# CONFIG_NEWLIB_TIME_SYSCALL_USE_RTC is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_HRT is not set +# CONFIG_NEWLIB_TIME_SYSCALL_USE_NONE is not set +# end of Newlib + +# +# NVS +# +# CONFIG_NVS_ASSERT_ERROR_CHECK is not set +# CONFIG_NVS_LEGACY_DUP_KEYS_COMPATIBILITY is not set +# end of NVS + +# +# OpenThread +# +# CONFIG_OPENTHREAD_ENABLED is not set + +# +# OpenThread Spinel +# +# CONFIG_OPENTHREAD_SPINEL_ONLY is not set +# end of OpenThread Spinel + +# CONFIG_OPENTHREAD_DEBUG is not set +# end of OpenThread + +# +# Protocomm +# +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_0=y +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_1=y +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_VERSION_2=y +CONFIG_ESP_PROTOCOMM_SUPPORT_SECURITY_PATCH_VERSION=y +# end of Protocomm + +# +# PThreads +# +CONFIG_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_PTHREAD_STACK_MIN=768 +CONFIG_PTHREAD_DEFAULT_CORE_NO_AFFINITY=y +# CONFIG_PTHREAD_DEFAULT_CORE_0 is not set +# CONFIG_PTHREAD_DEFAULT_CORE_1 is not set +CONFIG_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_PTHREAD_TASK_NAME_DEFAULT="pthread" +# end of PThreads + +# +# MMU Config +# +CONFIG_MMU_PAGE_SIZE_64KB=y +CONFIG_MMU_PAGE_MODE="64KB" +CONFIG_MMU_PAGE_SIZE=0x10000 +# end of MMU Config + +# +# Main Flash configuration +# + +# +# SPI Flash behavior when brownout +# +CONFIG_SPI_FLASH_BROWNOUT_RESET_XMC=y +CONFIG_SPI_FLASH_BROWNOUT_RESET=y +# end of SPI Flash behavior when brownout + +# +# Optional and Experimental Features (READ DOCS FIRST) +# + +# +# Features here require specific hardware (READ DOCS FIRST!) +# +CONFIG_SPI_FLASH_SUSPEND_TSUS_VAL_US=50 +# CONFIG_SPI_FLASH_FORCE_ENABLE_XMC_C_SUSPEND is not set +# CONFIG_SPI_FLASH_FORCE_ENABLE_C6_H2_SUSPEND is not set +# end of Optional and Experimental Features (READ DOCS FIRST) +# end of Main Flash configuration + +# +# SPI Flash driver +# +# CONFIG_SPI_FLASH_VERIFY_WRITE is not set +# CONFIG_SPI_FLASH_ENABLE_COUNTERS is not set +CONFIG_SPI_FLASH_ROM_DRIVER_PATCH=y +CONFIG_SPI_FLASH_DANGEROUS_WRITE_ABORTS=y +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_FAILS is not set +# CONFIG_SPI_FLASH_DANGEROUS_WRITE_ALLOWED is not set +# CONFIG_SPI_FLASH_SHARE_SPI1_BUS is not set +# CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE is not set +CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y +CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=20 +CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=1 +CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=8192 +# CONFIG_SPI_FLASH_SIZE_OVERRIDE is not set +# CONFIG_SPI_FLASH_CHECK_ERASE_TIMEOUT_DISABLED is not set +# CONFIG_SPI_FLASH_OVERRIDE_CHIP_DRIVER_LIST is not set + +# +# Auto-detect flash chips +# +CONFIG_SPI_FLASH_VENDOR_XMC_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_GD_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_ISSI_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_MXIC_SUPPORTED=y +CONFIG_SPI_FLASH_VENDOR_WINBOND_SUPPORTED=y +CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_GD_CHIP=y +CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP=y +# CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP is not set +# CONFIG_SPI_FLASH_SUPPORT_TH_CHIP is not set +# end of Auto-detect flash chips + +CONFIG_SPI_FLASH_ENABLE_ENCRYPTED_READ_WRITE=y +# end of SPI Flash driver + +# +# SPIFFS Configuration +# +CONFIG_SPIFFS_MAX_PARTITIONS=3 + +# +# SPIFFS Cache Configuration +# +CONFIG_SPIFFS_CACHE=y +CONFIG_SPIFFS_CACHE_WR=y +# CONFIG_SPIFFS_CACHE_STATS is not set +# end of SPIFFS Cache Configuration + +CONFIG_SPIFFS_PAGE_CHECK=y +CONFIG_SPIFFS_GC_MAX_RUNS=10 +# CONFIG_SPIFFS_GC_STATS is not set +CONFIG_SPIFFS_PAGE_SIZE=256 +CONFIG_SPIFFS_OBJ_NAME_LEN=32 +# CONFIG_SPIFFS_FOLLOW_SYMLINKS is not set +CONFIG_SPIFFS_USE_MAGIC=y +CONFIG_SPIFFS_USE_MAGIC_LENGTH=y +CONFIG_SPIFFS_META_LENGTH=4 +CONFIG_SPIFFS_USE_MTIME=y + +# +# Debug Configuration +# +# CONFIG_SPIFFS_DBG is not set +# CONFIG_SPIFFS_API_DBG is not set +# CONFIG_SPIFFS_GC_DBG is not set +# CONFIG_SPIFFS_CACHE_DBG is not set +# CONFIG_SPIFFS_CHECK_DBG is not set +# CONFIG_SPIFFS_TEST_VISUALISATION is not set +# end of Debug Configuration +# end of SPIFFS Configuration + +# +# TCP Transport +# + +# +# Websocket +# +CONFIG_WS_TRANSPORT=y +CONFIG_WS_BUFFER_SIZE=1024 +# CONFIG_WS_DYNAMIC_BUFFER is not set +# end of Websocket +# end of TCP Transport + +# +# Ultra Low Power (ULP) Co-processor +# +# CONFIG_ULP_COPROC_ENABLED is not set + +# +# ULP Debugging Options +# +# end of ULP Debugging Options +# end of Ultra Low Power (ULP) Co-processor + +# +# Unity unit testing library +# +CONFIG_UNITY_ENABLE_FLOAT=y +CONFIG_UNITY_ENABLE_DOUBLE=y +# CONFIG_UNITY_ENABLE_64BIT is not set +# CONFIG_UNITY_ENABLE_COLOR is not set +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y +# CONFIG_UNITY_ENABLE_FIXTURE is not set +# CONFIG_UNITY_ENABLE_BACKTRACE_ON_FAIL is not set +# end of Unity unit testing library + +# +# Virtual file system +# +CONFIG_VFS_SUPPORT_IO=y +CONFIG_VFS_SUPPORT_DIR=y +CONFIG_VFS_SUPPORT_SELECT=y +CONFIG_VFS_SUPPRESS_SELECT_DEBUG_OUTPUT=y +# CONFIG_VFS_SELECT_IN_RAM is not set +CONFIG_VFS_SUPPORT_TERMIOS=y +CONFIG_VFS_MAX_COUNT=8 + +# +# Host File System I/O (Semihosting) +# +CONFIG_VFS_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +# end of Host File System I/O (Semihosting) + +CONFIG_VFS_INITIALIZE_DEV_NULL=y +# end of Virtual file system + +# +# Wear Levelling +# +# CONFIG_WL_SECTOR_SIZE_512 is not set +CONFIG_WL_SECTOR_SIZE_4096=y +CONFIG_WL_SECTOR_SIZE=4096 +# end of Wear Levelling + +# +# Wi-Fi Provisioning Manager +# +CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 +CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 +CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y +# CONFIG_WIFI_PROV_STA_FAST_SCAN is not set +# end of Wi-Fi Provisioning Manager + +# +# mDNS +# +CONFIG_MDNS_MAX_INTERFACES=3 +CONFIG_MDNS_MAX_SERVICES=10 +CONFIG_MDNS_TASK_PRIORITY=1 +CONFIG_MDNS_ACTION_QUEUE_LEN=16 +CONFIG_MDNS_TASK_STACK_SIZE=4096 +# CONFIG_MDNS_TASK_AFFINITY_NO_AFFINITY is not set +CONFIG_MDNS_TASK_AFFINITY_CPU0=y +# CONFIG_MDNS_TASK_AFFINITY_CPU1 is not set +CONFIG_MDNS_TASK_AFFINITY=0x0 + +# +# MDNS Memory Configuration +# +CONFIG_MDNS_TASK_CREATE_FROM_INTERNAL=y +CONFIG_MDNS_MEMORY_ALLOC_INTERNAL=y +# CONFIG_MDNS_MEMORY_CUSTOM_IMPL is not set +# end of MDNS Memory Configuration + +CONFIG_MDNS_SERVICE_ADD_TIMEOUT_MS=2000 +CONFIG_MDNS_TIMER_PERIOD_MS=100 +# CONFIG_MDNS_NETWORKING_SOCKET is not set +# CONFIG_MDNS_SKIP_SUPPRESSING_OWN_QUERIES is not set +# CONFIG_MDNS_ENABLE_DEBUG_PRINTS is not set +CONFIG_MDNS_ENABLE_CONSOLE_CLI=y +# CONFIG_MDNS_RESPOND_REVERSE_QUERIES is not set +CONFIG_MDNS_MULTIPLE_INSTANCE=y + +# +# MDNS Predefined interfaces +# +CONFIG_MDNS_PREDEF_NETIF_STA=y +CONFIG_MDNS_PREDEF_NETIF_AP=y +CONFIG_MDNS_PREDEF_NETIF_ETH=y +# end of MDNS Predefined interfaces +# end of mDNS + +# +# LittleFS +# +# CONFIG_LITTLEFS_SDMMC_SUPPORT is not set +CONFIG_LITTLEFS_MAX_PARTITIONS=3 +CONFIG_LITTLEFS_PAGE_SIZE=256 +CONFIG_LITTLEFS_OBJ_NAME_LEN=64 +CONFIG_LITTLEFS_READ_SIZE=128 +CONFIG_LITTLEFS_WRITE_SIZE=128 +CONFIG_LITTLEFS_LOOKAHEAD_SIZE=128 +CONFIG_LITTLEFS_CACHE_SIZE=512 +CONFIG_LITTLEFS_BLOCK_CYCLES=512 +CONFIG_LITTLEFS_USE_MTIME=y +# CONFIG_LITTLEFS_USE_ONLY_HASH is not set +# CONFIG_LITTLEFS_HUMAN_READABLE is not set +CONFIG_LITTLEFS_MTIME_USE_SECONDS=y +# CONFIG_LITTLEFS_MTIME_USE_NONCE is not set +# CONFIG_LITTLEFS_SPIFFS_COMPAT is not set +# CONFIG_LITTLEFS_FLUSH_FILE_EVERY_WRITE is not set +# CONFIG_LITTLEFS_FCNTL_GET_PATH is not set +# CONFIG_LITTLEFS_MULTIVERSION is not set +# CONFIG_LITTLEFS_MALLOC_STRATEGY_DISABLE is not set +CONFIG_LITTLEFS_MALLOC_STRATEGY_DEFAULT=y +# CONFIG_LITTLEFS_MALLOC_STRATEGY_INTERNAL is not set +CONFIG_LITTLEFS_ASSERTS=y +# end of LittleFS + +# +# Telnet Server +# +CONFIG_TELNET_SERVER_DEFAULT_PORT=23 +CONFIG_TELNET_SERVER_MAX_CONNECTIONS=5 +CONFIG_TELNET_SERVER_STACK_SIZE=4096 +CONFIG_TELNET_SERVER_TASK_PRIORITY=24 +CONFIG_TELNET_SERVER_TASK_CORE=0 +CONFIG_TELNET_SERVER_REDIRECT_LOGS=0 +# end of Telnet Server +# end of Component config + +# CONFIG_IDF_EXPERIMENTAL_FEATURES is not set + +# Deprecated options for backward compatibility +# CONFIG_APP_BUILD_TYPE_ELF_RAM is not set +# CONFIG_NO_BLOBS is not set +# CONFIG_ESP32_NO_BLOBS is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V2_1_BOOTLOADERS is not set +# CONFIG_ESP32_COMPATIBLE_PRE_V3_1_BOOTLOADERS is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_NONE is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_ERROR is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_WARN is not set +CONFIG_LOG_BOOTLOADER_LEVEL_INFO=y +# CONFIG_LOG_BOOTLOADER_LEVEL_DEBUG is not set +# CONFIG_LOG_BOOTLOADER_LEVEL_VERBOSE is not set +CONFIG_LOG_BOOTLOADER_LEVEL=3 +# CONFIG_APP_ROLLBACK_ENABLE is not set +# CONFIG_FLASH_ENCRYPTION_ENABLED is not set +# CONFIG_FLASHMODE_QIO is not set +# CONFIG_FLASHMODE_QOUT is not set +CONFIG_FLASHMODE_DIO=y +# CONFIG_FLASHMODE_DOUT is not set +CONFIG_MONITOR_BAUD=115200 +CONFIG_OPTIMIZATION_LEVEL_DEBUG=y +CONFIG_COMPILER_OPTIMIZATION_LEVEL_DEBUG=y +CONFIG_COMPILER_OPTIMIZATION_DEFAULT=y +# CONFIG_OPTIMIZATION_LEVEL_RELEASE is not set +# CONFIG_COMPILER_OPTIMIZATION_LEVEL_RELEASE is not set +CONFIG_OPTIMIZATION_ASSERTIONS_ENABLED=y +# CONFIG_OPTIMIZATION_ASSERTIONS_SILENT is not set +# CONFIG_OPTIMIZATION_ASSERTIONS_DISABLED is not set +CONFIG_OPTIMIZATION_ASSERTION_LEVEL=2 +CONFIG_CXX_EXCEPTIONS=y +CONFIG_CXX_EXCEPTIONS_EMG_POOL_SIZE=0 +CONFIG_STACK_CHECK_NONE=y +# CONFIG_STACK_CHECK_NORM is not set +# CONFIG_STACK_CHECK_STRONG is not set +# CONFIG_STACK_CHECK_ALL is not set +# CONFIG_WARN_WRITE_STRINGS is not set +# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set +CONFIG_ESP32_APPTRACE_DEST_NONE=y +CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y +CONFIG_ADC2_DISABLE_DAC=y +# CONFIG_MCPWM_ISR_IN_IRAM is not set +# CONFIG_EVENT_LOOP_PROFILING is not set +CONFIG_POST_EVENTS_FROM_ISR=y +CONFIG_POST_EVENTS_FROM_IRAM_ISR=y +CONFIG_GDBSTUB_SUPPORT_TASKS=y +CONFIG_GDBSTUB_MAX_TASKS=32 +# CONFIG_OTA_ALLOW_HTTP is not set +# CONFIG_TWO_UNIVERSAL_MAC_ADDRESS is not set +CONFIG_FOUR_UNIVERSAL_MAC_ADDRESS=y +CONFIG_NUMBER_OF_UNIVERSAL_MAC_ADDRESS=4 +# CONFIG_ESP_SYSTEM_PD_FLASH is not set +CONFIG_ESP32_DEEP_SLEEP_WAKEUP_DELAY=2000 +CONFIG_ESP_SLEEP_DEEP_SLEEP_WAKEUP_DELAY=2000 +CONFIG_ESP32_RTC_CLK_SRC_INT_RC=y +CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_RC=y +# CONFIG_ESP32_RTC_CLK_SRC_EXT_CRYS is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_CRYSTAL is not set +# CONFIG_ESP32_RTC_CLK_SRC_EXT_OSC is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_EXTERNAL_OSC is not set +# CONFIG_ESP32_RTC_CLK_SRC_INT_8MD256 is not set +# CONFIG_ESP32_RTC_CLOCK_SOURCE_INTERNAL_8MD256 is not set +CONFIG_ESP32_RTC_CLK_CAL_CYCLES=1024 +# CONFIG_ESP32_XTAL_FREQ_26 is not set +CONFIG_ESP32_XTAL_FREQ_40=y +# CONFIG_ESP32_XTAL_FREQ_AUTO is not set +CONFIG_ESP32_XTAL_FREQ=40 +CONFIG_ESP32_PHY_CALIBRATION_AND_DATA_STORAGE=y +# CONFIG_ESP32_PHY_INIT_DATA_IN_PARTITION is not set +CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER=20 +CONFIG_ESP32_PHY_MAX_TX_POWER=20 +# CONFIG_REDUCE_PHY_TX_POWER is not set +# CONFIG_ESP32_REDUCE_PHY_TX_POWER is not set +# CONFIG_SPIRAM_SUPPORT is not set +# CONFIG_ESP32_SPIRAM_SUPPORT is not set +# CONFIG_ESP32_DEFAULT_CPU_FREQ_80 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_160=y +# CONFIG_ESP32_DEFAULT_CPU_FREQ_240 is not set +CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ=160 +CONFIG_TRACEMEM_RESERVE_DRAM=0x0 +# CONFIG_ESP32_PANIC_PRINT_HALT is not set +CONFIG_ESP32_PANIC_PRINT_REBOOT=y +# CONFIG_ESP32_PANIC_SILENT_REBOOT is not set +# CONFIG_ESP32_PANIC_GDBSTUB is not set +CONFIG_SYSTEM_EVENT_QUEUE_SIZE=32 +CONFIG_SYSTEM_EVENT_TASK_STACK_SIZE=2304 +CONFIG_MAIN_TASK_STACK_SIZE=3584 +CONFIG_CONSOLE_UART_DEFAULT=y +# CONFIG_CONSOLE_UART_CUSTOM is not set +# CONFIG_CONSOLE_UART_NONE is not set +# CONFIG_ESP_CONSOLE_UART_NONE is not set +CONFIG_CONSOLE_UART=y +CONFIG_CONSOLE_UART_NUM=0 +CONFIG_CONSOLE_UART_BAUDRATE=115200 +CONFIG_INT_WDT=y +CONFIG_INT_WDT_TIMEOUT_MS=300 +CONFIG_INT_WDT_CHECK_CPU1=y +CONFIG_TASK_WDT=y +CONFIG_ESP_TASK_WDT=y +# CONFIG_TASK_WDT_PANIC is not set +CONFIG_TASK_WDT_TIMEOUT_S=5 +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_TASK_WDT_CHECK_IDLE_TASK_CPU1=y +# CONFIG_ESP32_DEBUG_STUBS_ENABLE is not set +CONFIG_ESP32_DEBUG_OCDAWARE=y +CONFIG_BROWNOUT_DET=y +CONFIG_ESP32_BROWNOUT_DET=y +CONFIG_BROWNOUT_DET_LVL_SEL_0=y +CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_0=y +# CONFIG_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_1 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_2 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_3 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_4 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_5 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_6 is not set +# CONFIG_BROWNOUT_DET_LVL_SEL_7 is not set +# CONFIG_ESP32_BROWNOUT_DET_LVL_SEL_7 is not set +CONFIG_BROWNOUT_DET_LVL=0 +CONFIG_ESP32_BROWNOUT_DET_LVL=0 +# CONFIG_DISABLE_BASIC_ROM_CONSOLE is not set +CONFIG_IPC_TASK_STACK_SIZE=1024 +CONFIG_TIMER_TASK_STACK_SIZE=3584 +CONFIG_ESP32_WIFI_ENABLED=y +CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 +CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_STATIC_TX_BUFFER is not set +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER=y +CONFIG_ESP32_WIFI_TX_BUFFER_TYPE=1 +CONFIG_ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM=32 +# CONFIG_ESP32_WIFI_CSI_ENABLED is not set +CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED=y +CONFIG_ESP32_WIFI_TX_BA_WIN=6 +CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED=y +CONFIG_ESP32_WIFI_RX_BA_WIN=6 +CONFIG_ESP32_WIFI_NVS_ENABLED=y +CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_0=y +# CONFIG_ESP32_WIFI_TASK_PINNED_TO_CORE_1 is not set +CONFIG_ESP32_WIFI_SOFTAP_BEACON_MAX_LEN=752 +CONFIG_ESP32_WIFI_MGMT_SBUF_NUM=32 +CONFIG_ESP32_WIFI_IRAM_OPT=y +CONFIG_ESP32_WIFI_RX_IRAM_OPT=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_SAE=y +CONFIG_ESP32_WIFI_ENABLE_WPA3_OWE_STA=y +CONFIG_WPA_MBEDTLS_CRYPTO=y +CONFIG_WPA_MBEDTLS_TLS_CLIENT=y +# CONFIG_WPA_WAPI_PSK is not set +# CONFIG_WPA_11KV_SUPPORT is not set +# CONFIG_WPA_MBO_SUPPORT is not set +# CONFIG_WPA_DPP_SUPPORT is not set +# CONFIG_WPA_11R_SUPPORT is not set +# CONFIG_WPA_WPS_SOFTAP_REGISTRAR is not set +# CONFIG_WPA_WPS_STRICT is not set +# CONFIG_WPA_DEBUG_PRINT is not set +# CONFIG_WPA_TESTING_OPTIONS is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_FLASH is not set +# CONFIG_ESP32_ENABLE_COREDUMP_TO_UART is not set +CONFIG_ESP32_ENABLE_COREDUMP_TO_NONE=y +CONFIG_TIMER_TASK_PRIORITY=1 +CONFIG_TIMER_TASK_STACK_DEPTH=2048 +CONFIG_TIMER_QUEUE_LENGTH=10 +# CONFIG_ENABLE_STATIC_TASK_CLEAN_UP_HOOK is not set +# CONFIG_HAL_ASSERTION_SILIENT is not set +# CONFIG_L2_TO_L3_COPY is not set +CONFIG_ESP_GRATUITOUS_ARP=y +CONFIG_GARP_TMR_INTERVAL=60 +CONFIG_TCPIP_RECVMBOX_SIZE=32 +CONFIG_TCP_MAXRTX=12 +CONFIG_TCP_SYNMAXRTX=12 +CONFIG_TCP_MSS=1440 +CONFIG_TCP_MSL=60000 +CONFIG_TCP_SND_BUF_DEFAULT=5760 +CONFIG_TCP_WND_DEFAULT=5760 +CONFIG_TCP_RECVMBOX_SIZE=6 +CONFIG_TCP_QUEUE_OOSEQ=y +CONFIG_TCP_OVERSIZE_MSS=y +# CONFIG_TCP_OVERSIZE_QUARTER_MSS is not set +# CONFIG_TCP_OVERSIZE_DISABLE is not set +CONFIG_UDP_RECVMBOX_SIZE=6 +CONFIG_TCPIP_TASK_STACK_SIZE=3072 +CONFIG_TCPIP_TASK_AFFINITY_NO_AFFINITY=y +# CONFIG_TCPIP_TASK_AFFINITY_CPU0 is not set +# CONFIG_TCPIP_TASK_AFFINITY_CPU1 is not set +CONFIG_TCPIP_TASK_AFFINITY=0x7FFFFFFF +# CONFIG_PPP_SUPPORT is not set +CONFIG_ESP32_TIME_SYSCALL_USE_RTC_HRT=y +CONFIG_ESP32_TIME_SYSCALL_USE_RTC_FRC1=y +# CONFIG_ESP32_TIME_SYSCALL_USE_RTC is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_HRT is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_FRC1 is not set +# CONFIG_ESP32_TIME_SYSCALL_USE_NONE is not set +CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT=5 +CONFIG_ESP32_PTHREAD_TASK_STACK_SIZE_DEFAULT=3072 +CONFIG_ESP32_PTHREAD_STACK_MIN=768 +CONFIG_ESP32_DEFAULT_PTHREAD_CORE_NO_AFFINITY=y +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_0 is not set +# CONFIG_ESP32_DEFAULT_PTHREAD_CORE_1 is not set +CONFIG_ESP32_PTHREAD_TASK_CORE_DEFAULT=-1 +CONFIG_ESP32_PTHREAD_TASK_NAME_DEFAULT="pthread" +CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ABORTS=y +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_FAILS is not set +# CONFIG_SPI_FLASH_WRITING_DANGEROUS_REGIONS_ALLOWED is not set +# CONFIG_ESP32_ULP_COPROC_ENABLED is not set +CONFIG_SUPPRESS_SELECT_DEBUG_OUTPUT=y +CONFIG_SUPPORT_TERMIOS=y +CONFIG_SEMIHOSTFS_MAX_MOUNT_POINTS=1 +# End of deprecated options diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..5adba1b --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,25 @@ +# +# Compiler options +# +CONFIG_COMPILER_CXX_EXCEPTIONS=y + +# +# Wi-Fi +# +# CONFIG_ESP_WIFI_GMAC_SUPPORT is not set + +# +# LWIP +# +CONFIG_LWIP_LOCAL_HOSTNAME="LeoLights" + +# +# Ethernet +# +CONFIG_USE_INTERNAL_ETHERNET=y +CONFIG_ETH_PHY_LAN87XX=y + +CONFIG_ETH_MDC_GPIO=23 +CONFIG_ETH_MDIO_GPIO=33 +CONFIG_ETH_PHY_RST_GPIO=5 +CONFIG_ETH_PHY_ADDR=1 diff --git a/startCode.sh b/startCode.sh new file mode 100644 index 0000000..4442e30 --- /dev/null +++ b/startCode.sh @@ -0,0 +1,3 @@ +#!/bin/bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +code --profile Embed-Esp ${SCRIPT_DIR} diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt new file mode 100644 index 0000000..473c9ab --- /dev/null +++ b/test/integration/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +set(EXTRA_COMPONENT_DIRS + "../../core/components" + "../../core/managed_components" + "../../components" + # "../../managed_components" +) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(IntegrationTest) + diff --git a/test/integration/main/CMakeLists.txt b/test/integration/main/CMakeLists.txt new file mode 100644 index 0000000..39a435f --- /dev/null +++ b/test/integration/main/CMakeLists.txt @@ -0,0 +1,2 @@ +idf_component_register(SRCS "movementTests.cpp" + INCLUDE_DIRS ".") diff --git a/test/integration/main/movementTests.cpp b/test/integration/main/movementTests.cpp new file mode 100644 index 0000000..5f91e34 --- /dev/null +++ b/test/integration/main/movementTests.cpp @@ -0,0 +1,340 @@ +/** + * @file watchMain.cpp + * @author Gustice + * @brief ChaosWatch Application + * + * @copyright Copyright (c) 2024 + */ + +/// NOTE: Create IDF shell navigate to directory ("test/integration") +// and run "idf.py -p COM9 flash monitor" + +#include "Processor.hpp" +#include "PwmExpanderPca9685.hpp" +#include "Task.hpp" +#include "esp_event.h" +#include "esp_log.h" +#include "sdkconfig.h" +#include "stepperDrv.hpp" +#include "stepperMotor.hpp" +#include "watchFace.hpp" +#include +#include +#include +#include + +/* Watchface Indicies + 6/ 5/ + 5 / 1 4 / 0 + 4 2 3 1 + 3 2 +*/ + +extern "C" { // This switch allows the ROS C-implementation to find this main +void app_main(void); +} + +static const char *TAG = "IT"; + +struct DeviceState { + uint8_t minJumps{6}; + uint8_t maxJumps{32}; + uint8_t profile{}; + uint8_t restTime{}; + float speed = 1.0; // Rad/sec + float accel = 1.0; // Rad/sec^2 + Color color{0x80, 0x80, 0x80}; + uint8_t pictogram{}; // 0..5 active Pictogramm +}; + +class IndexIllumination { + public: + IndexIllumination() + : master(I2C_NUM_0, /*sda=*/GPIO_NUM_10, /*scl=*/GPIO_NUM_11), + ledDriver1(master, /*iverted=*/false), + ledDriver2(master, /*iverted=*/false, 0x41), + pwmEnable(GPIO_NUM_1, GpioPort::Logic::Inverted), + RED{CRed.getColor()}, + GREEN{CGreen.getColor()}, + BLUE{CBlue.getColor()} { + pwmEnable.write(true); + clearColorData(); + syncToDevice(); + } + + void clearColorData() { + for (size_t i = 0; i < 2; i++) { + for (size_t j = 0; j < PmwExpanderPca9685::PwmChannels; j++) { + Values[i][j] = 0; + } + } + } + + void setColorOfIndexTo(uint8_t idx, Color::Rgbw color) { + if (idx >= 6) { + return; + } + auto sp = LedStartPoints[idx]; + sp[0] = (uint16_t)(color.white) << 5; + sp[1] = (uint16_t)(color.blue) << 5; + sp[2] = (uint16_t)(color.green) << 5; + sp[3] = (uint16_t)(color.red) << 5; + } + + void syncToDevice() { + ledDriver1.sendImage(Values[0]); + ledDriver2.sendImage(Values[1]); + } + + void flashAll(Color::Rgbw color, int duration = 200) { + clearColorData(); + for (size_t i = 0; i < 6; i++) { + setColorOfIndexTo(i, color); + } + syncToDevice(); + fos::Task::delayMs(duration); + clearColorData(); + syncToDevice(); + } + + private: + PmwExpanderPca9685::GrayValues Values[2]{}; + std::array LedStartPoints{ + &(Values[0][4]), // 1 + &(Values[1][12]), + &(Values[1][8]), + &(Values[1][4]), + &(Values[0][12]), + &(Values[0][8]), // 6 + }; + + I2cMaster master; + PmwExpanderPca9685 ledDriver1; + PmwExpanderPca9685 ledDriver2; + OutputPort pwmEnable; + const Color::Rgbw RED; + const Color::Rgbw GREEN; + const Color::Rgbw BLUE; +}; + +DeviceState activeState; + +void IndexColorSweep(IndexIllumination &illum) { + const auto RED = CRed.getColor(); + const auto GREEN = CGreen.getColor(); + const auto BLUE = CBlue.getColor(); + + illum.flashAll(RED); + fos::Task::delayMs(200); + illum.flashAll(GREEN); + fos::Task::delayMs(200); + illum.flashAll(BLUE); + fos::Task::delayMs(200); + + illum.clearColorData(); + illum.setColorOfIndexTo(0, GREEN); + illum.syncToDevice(); + fos::Task::delayMs(500); + + illum.clearColorData(); + illum.setColorOfIndexTo(0, RED); + illum.setColorOfIndexTo(1, GREEN); + illum.syncToDevice(); + fos::Task::delayMs(500); + + illum.clearColorData(); + illum.setColorOfIndexTo(1, RED); + illum.setColorOfIndexTo(2, GREEN); + illum.syncToDevice(); + fos::Task::delayMs(500); + + illum.clearColorData(); + illum.setColorOfIndexTo(2, RED); + illum.setColorOfIndexTo(3, GREEN); + illum.syncToDevice(); + fos::Task::delayMs(500); + + illum.clearColorData(); + illum.setColorOfIndexTo(3, RED); + illum.setColorOfIndexTo(4, GREEN); + illum.syncToDevice(); + fos::Task::delayMs(500); + + illum.clearColorData(); + illum.setColorOfIndexTo(4, RED); + illum.setColorOfIndexTo(5, GREEN); + illum.syncToDevice(); + fos::Task::delayMs(500); + + illum.clearColorData(); + illum.setColorOfIndexTo(5, RED); + illum.syncToDevice(); + fos::Task::delayMs(500); + + illum.clearColorData(); + illum.setColorOfIndexTo(0, GREEN); + illum.syncToDevice(); +} + +constexpr bool DoFlushAtBeginning = false; +constexpr bool DoFullTurns = false; +constexpr bool DoTurnSteps = false; +constexpr bool DoTurnProfiles = true; + +void AllmostDone(uint8_t newIdx) { + ESP_LOGW(TAG, "Done callback"); +} + +void testMovementTask() { + esp_log_level_set(TAG, ESP_LOG_DEBUG); + esp_log_level_set("STP", ESP_LOG_DEBUG); + ESP_LOGD(TAG, "Init Lights for signaling"); + + IndexIllumination illum; + + if constexpr (DoFlushAtBeginning) { + IndexColorSweep(illum); + } + const auto BLUE = CBlue.getColor(); + + ESP_LOGD(TAG, "Init Stepper"); + + constexpr int MicroSteps = 32; + constexpr float Intensity = 0.25; + constexpr float StepWidth = 1.8; + StepperDriverDrv8833::Pins stepperPins({LEDC_TIMER_0, + {LEDC_CHANNEL_0, LEDC_CHANNEL_1, LEDC_CHANNEL_2, LEDC_CHANNEL_3}, + {GPIO_NUM_21, GPIO_NUM_20, GPIO_NUM_23, GPIO_NUM_22}}); + StepperDriverDrv8833 stepperDriver(stepperPins, MicroSteps, Intensity); + StepperConfig stepperCfg(StepWidth, MicroSteps); + InputPort initSens(GPIO_NUM_18); + OutputPort motorSleep(GPIO_NUM_15, GpioPort::Logic::Inverted); + StepperMotor stepper(stepperCfg, stepperDriver, initSens); + + WatchMotor watch(stepper, motorSleep, AllmostDone); + + /// + /// Full Turns with full steps + /// + if constexpr (DoFullTurns) { + ESP_LOGD(TAG, "Find Home"); + stepper.findRef(); + ESP_LOGD(TAG, "Fist Position is %3d / %.2f / Micro %d", stepper.getQuanted(), stepper.getScaledPos() * 6, stepper.getSubPos()); + fos::Task::delayMs(2000); + + ESP_LOGD(TAG, "Make one turns with full steps"); + // stepper.setPos(0); + illum.flashAll(BLUE); + int i = 0; + + ESP_LOGD(TAG, "Position at begin is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + while (i++ < 200) { + stepper.fullStep(1); + fos::Task::delayMs(100); + ESP_LOGD(TAG, "Position is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + } + fos::Task::delayMs(2000); + + int spB = stepper.getSubPos(); + stepper.demoFullTurn(); + int spE = stepper.getSubPos(); + ESP_LOGD(TAG, "Fullsteps SubPos begin %d / end %d", spB, spE); + fos::Task::delayMs(2000); + + ESP_LOGD(TAG, "Position at begin is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + i = 0; + while (i++ < 64) { + stepper.step(1); + fos::Task::delayMs(100); + ESP_LOGD(TAG, "Position is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + } + } + + /// + /// Homing + /// + ESP_LOGD(TAG, "Find Home"); + stepper.findRef(); + ESP_LOGD(TAG, "Fist Position is %3d / %.2f / Micro %d", stepper.getQuanted(), stepper.getScaledPos() * 6, stepper.getSubPos()); + for (size_t i = 0; i < 7; i++) { + stepper.fullStep(1); + fos::Task::delayMs(40); + } + illum.flashAll(BLUE); + fos::Task::delayMs(1000); + + auto ref = stepper.zeroPosition(); + ESP_LOGD(TAG, "Reference saved at %.2f ", ref); + ESP_LOGD(TAG, "Correct Home"); + + /// + /// Multiple Turns continuous + /// + if constexpr (DoTurnSteps) { + + ESP_LOGD(TAG, "Fist Position is %3d / %.2f / Micro %d", stepper.getQuanted(), stepper.getScaledPos() * 6, stepper.getSubPos()); + fos::Task::delayMs(1000); + ESP_LOGI(TAG, "Move to each index multiple times"); + illum.flashAll(BLUE); + for (size_t i = 1; i <= 2 * 3; i++) { + watch.moveTo(3 * i, 0, 0, 1, 1); + ESP_LOGD(TAG, "Position is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + } + + ESP_LOGI(TAG, "Move to each index multiple times"); + illum.flashAll(BLUE); + for (size_t i = 1; i <= 6 * 3; i++) { + watch.moveTo(i, 0, 0, 1, 1); + ESP_LOGD(TAG, "Position is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + } + } + + /// + /// All other Profiles + /// + if constexpr (DoTurnProfiles) { + for (size_t i = ProfileGenerator::Smooth; i < ProfileGenerator::NumProfiles; i++) { + // for (size_t i = ProfileGenerator::JumpIn; i < ProfileGenerator::NumProfiles; i++) { // If only part is needed + ESP_LOGI(TAG, "Running Profile %d ", i); + ESP_LOGD(TAG, "Fist Position is %3d / %.2f / Micro %d", stepper.getQuanted(), stepper.getScaledPos() * 6, stepper.getSubPos()); + fos::Task::delayMs(1000); + illum.flashAll(BLUE); + int steps[3]{4, 2, 0}; + for (size_t j = 0; j < 3; j++) { + watch.moveTo(steps[j], i, 0, 1, 1); + ESP_LOGI(TAG, "Last position is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + fos::Task::delayMs(1000); + } + } + + for (size_t i = ProfileGenerator::Smooth; i < ProfileGenerator::NumProfiles; i++) { + // for (size_t i = ProfileGenerator::JumpIn; i < ProfileGenerator::NumProfiles; i++) { // If only part is needed + ESP_LOGI(TAG, "Running Profile %d ", i); + ESP_LOGD(TAG, "Fist Position is %3d / %.2f / Micro %d", stepper.getQuanted(), stepper.getScaledPos() * 6, stepper.getSubPos()); + fos::Task::delayMs(1000); + illum.flashAll(BLUE); + int steps[3]{4, 2, 0}; + for (size_t j = 0; j < 3; j++) { + watch.moveTo(steps[j], i, 0, 3, 3); + ESP_LOGI(TAG, "Last position is %3d / %.2f", stepper.getQuanted(), stepper.getScaledPos() * 6); + fos::Task::delayMs(1000); + } + } + } + + while (true) { + ESP_LOGI(TAG, "Finish"); + fos::Task::delayMs(60000); + } +} + +void app_main(void) { + ESP_LOGI(TAG, "Start Movement Integration Tests"); + + static fos::Task movementTest(testMovementTask, "Move", fos::Task::DefaultStackSize * 8); + + while (true) { + fos::Task::delayMs(5000); + // ESP_LOGI(TAG, "Finish"); + } +} diff --git a/test/unitOnHost/CMakeLists.txt b/test/unitOnHost/CMakeLists.txt new file mode 100644 index 0000000..ba2839b --- /dev/null +++ b/test/unitOnHost/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +project(UnitTests) +include(../../core/test/unitOnHost/test_core.cmake) + +UnitTestSetup() + +addSubDirs_ofRoot("${CMAKE_CURRENT_LIST_DIR}/../../components") +addSubDirs_ofRoot("${CMAKE_CURRENT_LIST_DIR}/../../core/components") +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../core/test/unitOnHost/hwMock" hwMock) +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../core/test/unitOnHost/netMock" netMock) +add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../core/test/unitOnHost/osMock" osMock) diff --git a/test/unitOnTarget/CMakeLists.txt b/test/unitOnTarget/CMakeLists.txt new file mode 100644 index 0000000..cf204be --- /dev/null +++ b/test/unitOnTarget/CMakeLists.txt @@ -0,0 +1,20 @@ +# This is the project CMakeLists.txt file for the test subproject +cmake_minimum_required(VERSION 3.16) + +# Include the components directory of the main application: +# +set(EXTRA_COMPONENT_DIRS + "../../components" + "../../core/components" + "../../core/managed_components" +) + +# Set the components to include the tests for. +# This can be overriden from CMake cache: +# - when invoking CMake directly: cmake -D TEST_COMPONENTS="xxxxx" .. +# - when using idf.py: idf.py -T xxxxx build +# +set(TEST_COMPONENTS "testable freeRtosApi hwHal spit" CACHE STRING "List of components to test" FORCE) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(unit_test) diff --git a/test/unitOnTarget/ReadMe.md b/test/unitOnTarget/ReadMe.md new file mode 100644 index 0000000..efdc98c --- /dev/null +++ b/test/unitOnTarget/ReadMe.md @@ -0,0 +1,3 @@ +# Unit Testing + +see Redme in `Project/core/test/unitOnTarget/ReadMe.md` \ No newline at end of file diff --git a/test/unitOnTarget/dependencies.lock b/test/unitOnTarget/dependencies.lock new file mode 100644 index 0000000..e9e979e --- /dev/null +++ b/test/unitOnTarget/dependencies.lock @@ -0,0 +1,10 @@ +dependencies: + idf: + source: + type: idf + version: 5.4.1 +direct_dependencies: +- idf +manifest_hash: 00d8efc8e59d9f2cc6a576ada310175e19e6f7506dd0b59211849f065dadccae +target: esp32c6 +version: 2.0.0 diff --git a/test/unitOnTarget/main/CMakeLists.txt b/test/unitOnTarget/main/CMakeLists.txt new file mode 100644 index 0000000..72f3ce0 --- /dev/null +++ b/test/unitOnTarget/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRCS "unitTest.cpp" + INCLUDE_DIRS "." +) diff --git a/test/unitOnTarget/main/makefile b/test/unitOnTarget/main/makefile new file mode 100644 index 0000000..e099bd1 --- /dev/null +++ b/test/unitOnTarget/main/makefile @@ -0,0 +1,7 @@ +# Run Exportport Script first + +build: + idf.py build + +run: + idf.py -p /dev/ttyUSB0 flash monitor \ No newline at end of file diff --git a/test/unitOnTarget/main/unitTest.cpp b/test/unitOnTarget/main/unitTest.cpp new file mode 100644 index 0000000..90aea1d --- /dev/null +++ b/test/unitOnTarget/main/unitTest.cpp @@ -0,0 +1,43 @@ +/** + * @file unitTest.cpp + * @author Gustice + * @brief Unit/Integration-Test frame for hardware bound components + * + * @copyright Copyright (c) 2024 + */ + +#include "unity.h" +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +extern "C" { // This switch allows the ROS C-implementation to find this main +void app_main(void); +} + +void app_main(void) { + printf("\n#### Running all the registered tests #####\n\n"); + UNITY_BEGIN(); + unity_run_all_tests(); + // Alternatively: + // "Executing one test by its name" + // unity_run_test_by_name("Mean of an empty array is zero"); + // Running tests with [mean] tag" + // unity_run_tests_by_tag("[mean]", false); + // print_banner("Running tests without [fails] tag"); + // unity_run_tests_by_tag("[fails]", true); + UNITY_END(); + + printf("\n#### Starting interactive test menu: ####\n"); + printf("# - run explicit #\n"); + printf("- run all with *, \n"); + printf("# - run tagged with [tag], \n"); + printf("# - run all other than ![tag])\n"); + printf("# Exit with 'Ctrl-]'\n"); + // unity_run_menu(); // Watchdog needs to + + while (true) { + vTaskDelay(10); + } +} diff --git a/test/unitOnTarget/sdkconfig.defaults b/test/unitOnTarget/sdkconfig.defaults new file mode 100644 index 0000000..1368849 --- /dev/null +++ b/test/unitOnTarget/sdkconfig.defaults @@ -0,0 +1,3 @@ +CONFIG_COMPILER_CXX_EXCEPTIONS=y + +CONFIG_ESP_TASK_WDT_EN=n diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/tools/trajectoryGen/trajectoryGen.py b/tools/trajectoryGen/trajectoryGen.py new file mode 100644 index 0000000..3f90510 --- /dev/null +++ b/tools/trajectoryGen/trajectoryGen.py @@ -0,0 +1,54 @@ +import math +import matplotlib.pyplot as plt + +from utils.defines import * +from utils.generator import * +from matplotlib.ticker import (MultipleLocator, AutoMinorLocator) + +def main(): + st = Stepper(1.8, 32) + print(f"Step = {st.Step}; Steps = {st.Steps}") + + wv = QuadWave(64) + # plt.plot(wv.Sine, label="sin") + # plt.plot(wv.Cosine, label="cos") + # plt.legend() + # plt.show() + + gen = Generator(st, 0.001, 1, 1) + + curves = [] + curves.append(gen.createSmoothStep(math.pi)) # 180 deg + curves.append(gen.createSmoothStep(math.pi*1/6)) # 60 deg + curves.append(gen.createBuildUpStep(math.pi)) # 180 deg + curves.append(gen.createRapidStartSwingInEnd(math.pi)) # 180 deg + curves.append(gen.createJumpInTarget(math.pi)) # 180 deg + + fig, axes = plt.subplots(len(curves), 1, layout='constrained') + + for (i,ax) in enumerate(axes): + crv = curves[i] + ax.plot(crv["p"], label="ϕ") + ax.plot(crv["w"], label="ω", linestyle="--") + + dax = ax.twinx() # instantiate a second Axes that shares the same x-axis + dax.step(range(len(crv["o"])), crv["o"], label="o", linewidth=.5) + print(f"EndPos = {crv["pE"]}; (Delta = {math.pi - crv["pE"]}) in {crv["T"] * 1000} steps") + + tMax = max(map(lambda x : x["T"], curves)) + tEnd = math.ceil(tMax)*1000 + + for ax in axes: + ax.yaxis.set_ticks([0, math.pi/3, math.pi*2/3, math.pi],["Picto0", "Picto1", "Picto2", "Picto3"]) + ax.xaxis.set_ticks(range(0,tEnd+1000,1000), range(0,math.ceil(tMax)+1,1)) + ax.xaxis.set_ticks(range(0,tEnd+1000,100), minor = True) + ax.yaxis.grid(linestyle='dotted') + ax.set_xlabel('time (s)') + ax. set_ylabel('angle') + # ax.legend() + + plt.show() + + +if __name__ == '__main__': + main() diff --git a/tools/trajectoryGen/utils/defines.py b/tools/trajectoryGen/utils/defines.py new file mode 100644 index 0000000..b0e33ac --- /dev/null +++ b/tools/trajectoryGen/utils/defines.py @@ -0,0 +1,33 @@ +if __name__ == "__main__": # If the Helper.py file is run directly + print("This is a modul and should be imported") + +import math + +class Stepper: + # step as degrees + Step : float + # step as radiant + StepRad : float + # steps for full resolution + Steps : int + # microsteps per step + Resolution : int + + def __init__(self, step, res): + self.Step = step + self.Steps = 360 / step + self.StepRad = math.pi * step/180 + self.Resolution = res + +class QuadWave: + Sine : [] + Cosine : [] + def __init__(self, resolution): + sine = [] + cosine = [] + s = 2 * math.pi / resolution + for i in range(resolution): + sine.append(math.sin(s * i)) + cosine.append(math.cos(s * i)) + self.Sine = sine + self.Cosine = cosine \ No newline at end of file diff --git a/tools/trajectoryGen/utils/generator.py b/tools/trajectoryGen/utils/generator.py new file mode 100644 index 0000000..5bcd49e --- /dev/null +++ b/tools/trajectoryGen/utils/generator.py @@ -0,0 +1,180 @@ +if __name__ == "__main__": # If the Helper.py file is run directly + print("This is a modul and should be imported") + +import math + +class Generator: + dT = 0 + aMax = 0 + wMax = 0 # v = a * t + # s = a * t^2 / 2 + # s = v * t + + def __init__(self, stpr, dt, a, v): + self.stepper = stpr + self.dT = dt + self.aMax = a + self.wMax = v + + def __initVars(self): + self.__pArr = [0] + self.__wArr = [0] + self.__tArr = [0] + self.__t = 0 + + def __appendPoint(self, p, w, t): + self.__wArr.append(w) + self.__pArr.append(p) + self.__tArr.append(t) + + def __accelUntil(self, a, wInit, check): + t = 0 + pX = self.__pArr[-1] + while True: + w = wInit + a * t + p = pX + wInit * t + a * t**2 / 2 + if check(w, p): + break + self.__appendPoint(p, w, self.__t + t) + t = t + self.dT + self.__t = self.__t + t + + def __getStopDistance(self, a, w): + tDec = w / a + pDec = a * tDec**2 / 2 + return (pDec, tDec) + + def __moveUntil(self, wM, pEnd): + t = 0 + pX = self.__pArr[-1] + while True: + p = pX + wM * t + if p > pEnd: + break + self.__appendPoint(p, wM, self.__t + t) + t = t + self.dT + self.__t = self.__t + t + + def __buildUpSwing(self, a, vM, swings): + t = 0 + pX = self.__pArr[-1] + + TwMax = vM / a + tau = 2*math.pi + sTau = swings*tau + + while True: + t = t + self.dT + env = a * t + p = math.sin(sTau*t/TwMax) * env * TwMax/sTau + w = (p-self.__pArr[-1])/self.dT + if w > self.wMax: + break + self.__appendPoint(p, w, self.__t + t) + + self.__t = self.__t + t + + def __transformToStepper(self) : + stp = self.stepper.StepRad + res = stp / self.stepper.Resolution + oArr = map(lambda x : int((x % stp) / res), self.__pArr) + oArr = list(oArr) + return {"p": self.__pArr, "w": self.__wArr, "t": self.__tArr, "pE": self.__pArr[-1], "T": self.__t, "o": oArr} + + def __smoothSubStep(self, delta, aM, wM): + (pDec,_) = self.__getStopDistance(aM, wM) + + if (pDec < (delta - self.__pArr[-1])/2): + # t0..t1: acceleration to max velocity + self.__accelUntil(aM, 0, lambda w,p : w > wM) + + # t1..t2: movement to until almost destination + self.__moveUntil(wM, delta - pDec) + + # t2..r3: deceleration until destination + self.__accelUntil(-aM, wM, lambda w,p : w < 0) + else: + # t0..t1: acceleration until center is reached + pCen = ((delta - self.__pArr[-1]) /2 + self.__pArr[-1]) + self.__accelUntil(aM, 0, lambda w,p : p > pCen) + + # t2..r3: deceleration until destination + self.__accelUntil(-aM, self.__wArr[-1], lambda w,p : w < 0) + + def createSmoothStep(self, delta): + self.__initVars() + + (pDec,_) = self.__getStopDistance(self.aMax, self.wMax) + + if (pDec < delta/2): + # t0..t1: acceleration to max velocity + self.__accelUntil(self.aMax, 0, lambda w,p : w > self.wMax) + + # t1..t2: movement to until almost destination + self.__moveUntil(self.wMax, delta - pDec) + + # t2..r3: deceleration until destination + self.__accelUntil(-self.aMax, self.wMax, lambda w,p : w < 0) + else: + # t0..t1: acceleration until center is reached + pCen = (self.__pArr[-1]) + delta /2 + self.__accelUntil(self.aMax, 0, lambda w,p : p > pCen) + + # t2..r3: deceleration until destination + self.__accelUntil(-self.aMax, self.__wArr[-1] , lambda w,p : w < 0) + + return self.__transformToStepper() + + def createBuildUpStep(self, delta): + self.__initVars() + + # t0..t1: increasing swing + self.__buildUpSwing(self.aMax/4, self.wMax, 3) + + (pDec,_) = self.__getStopDistance(self.aMax, self.wMax) + + # t1..t2: movement to until almost destination + self.__moveUntil(self.wMax, delta - pDec) + + # t2..r3: deceleration until destination + self.__accelUntil(-self.aMax, self.wMax, lambda w,p : w < 0) + + return self.__transformToStepper() + + def createRapidStartSwingInEnd(self, delta): + self.__initVars() + + (pDec,_) = self.__getStopDistance(self.aMax, self.wMax) + + # t0..t1: acceleration to max velocity + self.__accelUntil(self.aMax*2, 0, lambda w,p : w > self.wMax*2) + + # t1..r2: deceleration until moving speed + self.__accelUntil(-self.aMax, self.wMax*2, lambda w,p : w < self.wMax ) + + # t2..t3: movement to until destination (override) + self.__moveUntil(self.wMax, delta) + + # t3..t4: deceleration + self.__accelUntil(-self.aMax, self.wMax, lambda w,p : w < 0) + + pCen = delta + (self.__pArr[-1] - delta) /2 + self.__accelUntil(-self.aMax, 0, lambda w,p : p < pCen ) + self.__accelUntil(self.aMax, self.__wArr[-1], lambda w,p : w > 0) + + return self.__transformToStepper() + + def createJumpInTarget(self, delta): + self.__initVars() + + jumpInRange = math.pi/3 + deltaPre = delta - jumpInRange + + self.__smoothSubStep(deltaPre, self.aMax, self.wMax) + + jumpInSteps = [jumpInRange/2, jumpInRange/3, jumpInRange/6] + nextPoint = deltaPre + for step in jumpInSteps: + nextPoint = nextPoint + step + self.__smoothSubStep(nextPoint, self.aMax*10, self.wMax*2) + return self.__transformToStepper()