From dfa060895e9d031c0700eca2291f8b27368ba3af Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Fri, 7 Oct 2022 18:30:51 +0200 Subject: [PATCH 001/107] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BE=D0=BA=D0=B5=D1=82=D0=BE=D0=B2=20=D0=B2=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 11 +- include/WsServer.h | 3 + lib/WebSockets/.clang-format | 63 ++ lib/WebSockets/.github/workflows/main.yml | 186 ++++ lib/WebSockets/.gitignore | 37 + lib/WebSockets/.piopm | 1 + lib/WebSockets/.travis.yml | 45 + lib/WebSockets/LICENSE | 502 +++++++++ lib/WebSockets/README.md | 102 ++ .../Nginx/esp8266.ssl.reverse.proxy.conf | 83 ++ .../WebSocketClientAVR/WebSocketClientAVR.ino | 84 ++ .../esp32/WebSocketClient/WebSocketClient.ino | 110 ++ .../WebSocketClientSSL/WebSocketClientSSL.ino | 106 ++ .../WebSocketClientSocketIOack.ino | 155 +++ .../esp32/WebSocketServer/WebSocketServer.ino | 104 ++ .../WebSocketClient/WebSocketClient.ino | 106 ++ .../esp8266/WebSocketClientOTA/README.md | 27 + .../WebSocketClientOTA/WebSocketClientOTA.ino | 263 +++++ .../python_ota_server/main.py | 235 +++++ .../python_ota_server/requirements.txt | 2 + .../WebSocketClientSSL/WebSocketClientSSL.ino | 88 ++ .../WebSocketClientSSLWithCA.ino | 103 ++ .../WebSocketClientSocketIO.ino | 128 +++ .../WebSocketClientSocketIOack.ino | 165 +++ .../WebSocketClientStomp.ino | 149 +++ .../WebSocketClientStompOverSockJs.ino | 150 +++ .../WebSocketServer/WebSocketServer.ino | 86 ++ .../WebSocketServerAllFunctionsDemo.ino | 132 +++ .../WebSocketServerFragmentation.ino | 94 ++ .../WebSocketServerHooked.ino | 103 ++ .../esp8266/WebSocketServerHooked/emu | 20 + .../WebSocketServerHooked/ws-testclient.py | 45 + .../WebSocketServerHttpHeaderValidation.ino | 86 ++ .../WebSocketServer_LEDcontrol.ino | 121 +++ .../ParticleWebSocketClient/application.cpp | 46 + lib/WebSockets/library.json | 25 + lib/WebSockets/library.properties | 9 + lib/WebSockets/src/SocketIOclient.cpp | 260 +++++ lib/WebSockets/src/SocketIOclient.h | 105 ++ lib/WebSockets/src/WebSockets.cpp | 761 ++++++++++++++ lib/WebSockets/src/WebSockets.h | 367 +++++++ lib/WebSockets/src/WebSockets4WebServer.h | 80 ++ lib/WebSockets/src/WebSocketsClient.cpp | 975 ++++++++++++++++++ lib/WebSockets/src/WebSocketsClient.h | 169 +++ lib/WebSockets/src/WebSocketsServer.cpp | 955 +++++++++++++++++ lib/WebSockets/src/WebSocketsServer.h | 243 +++++ lib/WebSockets/src/WebSocketsVersion.h | 36 + lib/WebSockets/src/libb64/AUTHORS | 7 + lib/WebSockets/src/libb64/LICENSE | 29 + lib/WebSockets/src/libb64/cdecode.c | 98 ++ lib/WebSockets/src/libb64/cdecode_inc.h | 28 + lib/WebSockets/src/libb64/cencode.c | 119 +++ lib/WebSockets/src/libb64/cencode_inc.h | 31 + lib/WebSockets/src/libsha1/libsha1.c | 202 ++++ lib/WebSockets/src/libsha1/libsha1.h | 21 + lib/WebSockets/travis/common.sh | 134 +++ lib/WebSockets/travis/version.py | 132 +++ platformio.ini | 1 - src/Main.cpp | 1 - src/WsServer.cpp | 42 + src/modules/sensors/Pzem004t/modinfo.json | 8 +- src/utils/FileUtils.cpp | 6 +- 62 files changed, 8573 insertions(+), 12 deletions(-) create mode 100644 lib/WebSockets/.clang-format create mode 100644 lib/WebSockets/.github/workflows/main.yml create mode 100644 lib/WebSockets/.gitignore create mode 100644 lib/WebSockets/.piopm create mode 100644 lib/WebSockets/.travis.yml create mode 100644 lib/WebSockets/LICENSE create mode 100644 lib/WebSockets/README.md create mode 100644 lib/WebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf create mode 100644 lib/WebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino create mode 100644 lib/WebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino create mode 100644 lib/WebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino create mode 100644 lib/WebSockets/examples/esp32/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino create mode 100644 lib/WebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientOTA/README.md create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientOTA/WebSocketClientOTA.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/main.py create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/requirements.txt create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientSSLWithCA/WebSocketClientSSLWithCA.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServerHooked/WebSocketServerHooked.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServerHooked/emu create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServerHooked/ws-testclient.py create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino create mode 100644 lib/WebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino create mode 100644 lib/WebSockets/examples/particle/ParticleWebSocketClient/application.cpp create mode 100644 lib/WebSockets/library.json create mode 100644 lib/WebSockets/library.properties create mode 100644 lib/WebSockets/src/SocketIOclient.cpp create mode 100644 lib/WebSockets/src/SocketIOclient.h create mode 100644 lib/WebSockets/src/WebSockets.cpp create mode 100644 lib/WebSockets/src/WebSockets.h create mode 100644 lib/WebSockets/src/WebSockets4WebServer.h create mode 100644 lib/WebSockets/src/WebSocketsClient.cpp create mode 100644 lib/WebSockets/src/WebSocketsClient.h create mode 100644 lib/WebSockets/src/WebSocketsServer.cpp create mode 100644 lib/WebSockets/src/WebSocketsServer.h create mode 100644 lib/WebSockets/src/WebSocketsVersion.h create mode 100644 lib/WebSockets/src/libb64/AUTHORS create mode 100644 lib/WebSockets/src/libb64/LICENSE create mode 100644 lib/WebSockets/src/libb64/cdecode.c create mode 100644 lib/WebSockets/src/libb64/cdecode_inc.h create mode 100644 lib/WebSockets/src/libb64/cencode.c create mode 100644 lib/WebSockets/src/libb64/cencode_inc.h create mode 100644 lib/WebSockets/src/libsha1/libsha1.c create mode 100644 lib/WebSockets/src/libsha1/libsha1.h create mode 100644 lib/WebSockets/travis/common.sh create mode 100644 lib/WebSockets/travis/version.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 61cd5f78..4c76fa80 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,15 @@ "string_view": "cpp", "initializer_list": "cpp", "ranges": "cpp", - "thread": "cpp" + "thread": "cpp", + "optional": "cpp", + "istream": "cpp", + "ostream": "cpp", + "ratio": "cpp", + "system_error": "cpp", + "functional": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp" } } \ No newline at end of file diff --git a/include/WsServer.h b/include/WsServer.h index cc4116ef..58b9ab07 100644 --- a/include/WsServer.h +++ b/include/WsServer.h @@ -20,6 +20,9 @@ void periodicWsSend(); void sendStringToWs(const String& msg, uint8_t num, String name); void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, String id); + +void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t client_id, size_t frameSize); + // void sendMark(const char* filename, const char* mark, uint8_t num); // void sendFileToWs3(const String& filename, uint8_t num); // void sendFileToWs4(const String& filename, uint8_t num); \ No newline at end of file diff --git a/lib/WebSockets/.clang-format b/lib/WebSockets/.clang-format new file mode 100644 index 00000000..e72c54b1 --- /dev/null +++ b/lib/WebSockets/.clang-format @@ -0,0 +1,63 @@ +--- +BasedOnStyle: Google +AccessModifierOffset: '-2' +AlignAfterOpenBracket: DontAlign +AlignConsecutiveAssignments: 'true' +AlignConsecutiveDeclarations: 'false' +AlignEscapedNewlines: Left +AlignTrailingComments: 'true' +AllowAllParametersOfDeclarationOnNextLine: 'false' +AllowShortBlocksOnASingleLine: 'false' +AllowShortCaseLabelsOnASingleLine: 'false' +AllowShortFunctionsOnASingleLine: InlineOnly +AllowShortIfStatementsOnASingleLine: 'true' +AllowShortLoopsOnASingleLine: 'true' +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: 'true' +AlwaysBreakTemplateDeclarations: 'false' +BinPackParameters: 'true' +BreakAfterJavaFieldAnnotations: 'false' +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Attach +BreakBeforeInheritanceComma: 'false' +BreakBeforeTernaryOperators: 'false' +BreakConstructorInitializers: BeforeColon +BreakStringLiterals: 'false' +ColumnLimit: '0' +CompactNamespaces: 'true' +ConstructorInitializerAllOnOneLineOrOnePerLine: 'true' +ConstructorInitializerIndentWidth: '4' +ContinuationIndentWidth: '4' +Cpp11BracedListStyle: 'false' +DerivePointerAlignment: 'false' +FixNamespaceComments: 'true' +IndentCaseLabels: 'true' +IndentWidth: '4' +IndentWrappedFunctionNames: 'false' +JavaScriptQuotes: Single +JavaScriptWrapImports: 'false' +KeepEmptyLinesAtTheStartOfBlocks: 'false' +MaxEmptyLinesToKeep: '1' +NamespaceIndentation: All +ObjCBlockIndentWidth: '4' +ObjCSpaceAfterProperty: 'false' +ObjCSpaceBeforeProtocolList: 'false' +PointerAlignment: Middle +SortIncludes: 'false' +SortUsingDeclarations: 'true' +SpaceAfterCStyleCast: 'false' +SpaceAfterTemplateKeyword: 'false' +SpaceBeforeAssignmentOperators: 'true' +SpaceBeforeParens: Never +SpaceInEmptyParentheses: 'false' +SpacesBeforeTrailingComments: '4' +SpacesInAngles: 'false' +SpacesInCStyleCastParentheses: 'false' +SpacesInContainerLiterals: 'false' +SpacesInParentheses: 'false' +SpacesInSquareBrackets: 'false' +TabWidth: '4' +UseTab: Never + +... diff --git a/lib/WebSockets/.github/workflows/main.yml b/lib/WebSockets/.github/workflows/main.yml new file mode 100644 index 00000000..d7bea656 --- /dev/null +++ b/lib/WebSockets/.github/workflows/main.yml @@ -0,0 +1,186 @@ +name: CI +on: + schedule: + - cron: '0 0 * * 5' + push: + branches: [ master ] + pull_request: + branches: [ master ] + release: + types: [ published, created, edited ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + check_version_files: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: check version + run: | + $GITHUB_WORKSPACE/travis/version.py --check + + prepare_example_json: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: generate examples + id: set-matrix + run: | + source $GITHUB_WORKSPACE/travis/common.sh + cd $GITHUB_WORKSPACE + echo -en "::set-output name=matrix::" + echo -en "[" + + get_sketches_json_matrix arduino $GITHUB_WORKSPACE/examples/esp8266 esp8266 1.8.19 esp8266com:esp8266:generic:xtal=80,dbg=Serial1 + echo -en "," + + get_sketches_json_matrix arduino $GITHUB_WORKSPACE/examples/esp8266 esp8266 1.8.19 esp8266com:esp8266:generic:xtal=80,eesz=1M,FlashMode=qio,FlashFreq=80 + echo -en "," + + get_sketches_json_matrix arduino $GITHUB_WORKSPACE/examples/esp32 esp32 1.8.19 espressif:esp32:esp32:FlashFreq=80 + + echo -en "]" + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + prepare_ide: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + IDE_VERSION: [1.8.19] + env: + IDE_VERSION: ${{ matrix.IDE_VERSION }} + + steps: + - uses: actions/checkout@v2 + + - name: Get Date + id: get-date + run: | + echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")" + shell: bash + + - uses: actions/cache@v2 + id: cache_all + with: + path: | + /home/runner/arduino_ide + /home/runner/Arduino + key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ matrix.IDE_VERSION }} + + - name: download IDE + if: steps.cache_all.outputs.cache-hit != 'true' + run: | + wget http://downloads.arduino.cc/arduino-$IDE_VERSION-linux64.tar.xz -q + tar xf arduino-$IDE_VERSION-linux64.tar.xz + mv arduino-$IDE_VERSION $HOME/arduino_ide + + - name: download ArduinoJson + if: steps.cache_all.outputs.cache-hit != 'true' + run: | + mkdir -p $HOME/Arduino/libraries + wget https://github.com/bblanchon/ArduinoJson/archive/6.x.zip -q + unzip 6.x.zip + mv ArduinoJson-6.x $HOME/Arduino/libraries/ArduinoJson + + - name: download esp8266 + if: steps.cache_all.outputs.cache-hit != 'true' + run: | + source $GITHUB_WORKSPACE/travis/common.sh + get_core esp8266 + + - name: download esp32 + if: steps.cache_all.outputs.cache-hit != 'true' + run: | + source $GITHUB_WORKSPACE/travis/common.sh + get_core esp32 + + build: + needs: [prepare_ide, prepare_example_json] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.prepare_example_json.outputs.matrix) }} + env: + CPU: ${{ matrix.cpu }} + BOARD: ${{ matrix.board }} + IDE_VERSION: ${{ matrix.ideversion }} + SKETCH: ${{ matrix.sketch }} + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - uses: actions/checkout@v2 + + - name: install libgtk2.0-0 + run: | + sudo apt-get install -y libgtk2.0-0 + + - name: Get Date + id: get-date + run: | + echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")" + shell: bash + + - uses: actions/cache@v2 + id: cache_all + with: + path: | + /home/runner/arduino_ide + /home/runner/Arduino + key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ matrix.ideversion }} + + - name: install python serial + if: matrix.cpu == 'esp32' + run: | + sudo pip3 install pyserial + sudo pip install pyserial +# sudo apt install python-is-python3 + + - name: start DISPLAY + run: | + /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_1.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16 + export DISPLAY=:1.0 + sleep 3 + + - name: test IDE + run: | + export PATH="$HOME/arduino_ide:$PATH" + which arduino + + - name: copy code + run: | + mkdir -p $HOME/Arduino/libraries/ + cp -r $GITHUB_WORKSPACE $HOME/Arduino/libraries/arduinoWebSockets + + - name: config IDE + run: | + export DISPLAY=:1.0 + export PATH="$HOME/arduino_ide:$PATH" + arduino --board $BOARD --save-prefs + arduino --get-pref sketchbook.path + arduino --pref update.check=false + + - name: build example + timeout-minutes: 20 + run: | + set -ex + export DISPLAY=:1.0 + export PATH="$HOME/arduino_ide:$PATH" + source $GITHUB_WORKSPACE/travis/common.sh + cd $GITHUB_WORKSPACE + build_sketch arduino $SKETCH + + done: + needs: [prepare_ide, prepare_example_json, build, check_version_files] + runs-on: ubuntu-latest + steps: + - name: Done + run: | + echo DONE diff --git a/lib/WebSockets/.gitignore b/lib/WebSockets/.gitignore new file mode 100644 index 00000000..267af0de --- /dev/null +++ b/lib/WebSockets/.gitignore @@ -0,0 +1,37 @@ +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app +/tests/webSocketServer/node_modules + +# IDE +.vscode +.cproject +.project +.settings +*.swp + diff --git a/lib/WebSockets/.piopm b/lib/WebSockets/.piopm new file mode 100644 index 00000000..3871c04b --- /dev/null +++ b/lib/WebSockets/.piopm @@ -0,0 +1 @@ +{"type": "library", "name": "WebSockets", "version": "2.3.7", "spec": {"owner": "links2004", "id": 549, "name": "WebSockets", "requirements": null, "uri": null}} \ No newline at end of file diff --git a/lib/WebSockets/.travis.yml b/lib/WebSockets/.travis.yml new file mode 100644 index 00000000..6cdf5db4 --- /dev/null +++ b/lib/WebSockets/.travis.yml @@ -0,0 +1,45 @@ +sudo: false +dist: + - xenial +addons: + apt: + packages: + - xvfb +language: bash +os: + - linux +env: + matrix: + - CPU="esp8266" BOARD="esp8266com:esp8266:generic:xtal=80" IDE_VERSION=1.6.13 + - CPU="esp8266" BOARD="esp8266com:esp8266:generic:xtal=80,dbg=Serial1" IDE_VERSION=1.6.13 + - CPU="esp8266" BOARD="esp8266com:esp8266:generic:xtal=80,eesz=1M,FlashMode=qio,FlashFreq=80" IDE_VERSION=1.8.13 + - CPU="esp32" BOARD="espressif:esp32:esp32:FlashFreq=80" IDE_VERSION=1.8.5 + - CPU="esp32" BOARD="espressif:esp32:esp32:FlashFreq=80" IDE_VERSION=1.8.9 + - CPU="esp32" BOARD="espressif:esp32:esp32:FlashFreq=80" IDE_VERSION=1.8.13 +script: + - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_1.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16 + - export DISPLAY=:1.0 + - sleep 3 + - wget http://downloads.arduino.cc/arduino-$IDE_VERSION-linux64.tar.xz + - tar xf arduino-$IDE_VERSION-linux64.tar.xz + - mv arduino-$IDE_VERSION $HOME/arduino_ide + - export PATH="$HOME/arduino_ide:$PATH" + - which arduino + - mkdir -p $HOME/Arduino/libraries + + - wget https://github.com/bblanchon/ArduinoJson/archive/6.x.zip + - unzip 6.x.zip + - mv ArduinoJson-6.x $HOME/Arduino/libraries/ArduinoJson + - cp -r $TRAVIS_BUILD_DIR $HOME/Arduino/libraries/arduinoWebSockets + - source $TRAVIS_BUILD_DIR/travis/common.sh + - get_core $CPU + - cd $TRAVIS_BUILD_DIR + - arduino --board $BOARD --save-prefs + - arduino --get-pref sketchbook.path + - arduino --pref update.check=false + - build_sketches arduino $HOME/Arduino/libraries/arduinoWebSockets/examples/$CPU $CPU + +notifications: + email: + on_success: change + on_failure: change diff --git a/lib/WebSockets/LICENSE b/lib/WebSockets/LICENSE new file mode 100644 index 00000000..f166cc57 --- /dev/null +++ b/lib/WebSockets/LICENSE @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/lib/WebSockets/README.md b/lib/WebSockets/README.md new file mode 100644 index 00000000..ae2497f0 --- /dev/null +++ b/lib/WebSockets/README.md @@ -0,0 +1,102 @@ +WebSocket Server and Client for Arduino [![Build Status](https://github.com/Links2004/arduinoWebSockets/workflows/CI/badge.svg?branch=master)](https://github.com/Links2004/arduinoWebSockets/actions?query=workflow%3ACI+branch%3Amaster) +=========================================== + +a WebSocket Server and Client for Arduino based on RFC6455. + + +##### Supported features of RFC6455 ##### + - text frame + - binary frame + - connection close + - ping + - pong + - continuation frame + +##### Limitations ##### + - max input length is limited to the ram size and the ```WEBSOCKETS_MAX_DATA_SIZE``` define + - max output length has no limit (the hardware is the limit) + - Client send big frames with mask 0x00000000 (on AVR all frames) + - continuation frame reassembly need to be handled in the application code + + ##### Limitations for Async ##### + - Functions called from within the context of the websocket event might not honor `yield()` and/or `delay()`. See [this issue](https://github.com/Links2004/arduinoWebSockets/issues/58#issuecomment-192376395) for more info and a potential workaround. + - wss / SSL is not possible. + +##### Supported Hardware ##### + - ESP8266 [Arduino for ESP8266](https://github.com/esp8266/Arduino/) + - ESP32 [Arduino for ESP32](https://github.com/espressif/arduino-esp32) + - ESP31B + - Particle with STM32 ARM Cortex M3 + - ATmega328 with Ethernet Shield (ATmega branch) + - ATmega328 with enc28j60 (ATmega branch) + - ATmega2560 with Ethernet Shield (ATmega branch) + - ATmega2560 with enc28j60 (ATmega branch) + +###### Note: ###### + + version 2.0.0 and up is not compatible with AVR/ATmega, check ATmega branch. + + version 2.3.0 has API changes for the ESP8266 BareSSL (may brakes existing code) + + Arduino for AVR not supports std namespace of c++. + +### wss / SSL ### + supported for: + - wss client on the ESP8266 + - wss / SSL is not natively supported in WebSocketsServer however it is possible to achieve secure websockets + by running the device behind an SSL proxy. See [Nginx](examples/Nginx/esp8266.ssl.reverse.proxy.conf) for a + sample Nginx server configuration file to enable this. + +### ESP Async TCP ### + +This libary can run in Async TCP mode on the ESP. + +The mode can be activated in the ```WebSockets.h``` (see WEBSOCKETS_NETWORK_TYPE define). + +[ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) libary is required. + + +### High Level Client API ### + + - `begin` : Initiate connection sequence to the websocket host. +```c++ +void begin(const char *host, uint16_t port, const char * url = "/", const char * protocol = "arduino"); +void begin(String host, uint16_t port, String url = "/", String protocol = "arduino"); + ``` + - `onEvent`: Callback to handle for websocket events + + ```c++ + void onEvent(WebSocketClientEvent cbEvent); + ``` + + - `WebSocketClientEvent`: Handler for websocket events + ```c++ + void (*WebSocketClientEvent)(WStype_t type, uint8_t * payload, size_t length) + ``` +Where `WStype_t type` is defined as: + ```c++ + typedef enum { + WStype_ERROR, + WStype_DISCONNECTED, + WStype_CONNECTED, + WStype_TEXT, + WStype_BIN, + WStype_FRAGMENT_TEXT_START, + WStype_FRAGMENT_BIN_START, + WStype_FRAGMENT, + WStype_FRAGMENT_FIN, + WStype_PING, + WStype_PONG, + } WStype_t; + ``` + +### Issues ### +Submit issues to: https://github.com/Links2004/arduinoWebSockets/issues + +[![Join the chat at https://gitter.im/Links2004/arduinoWebSockets](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Links2004/arduinoWebSockets?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +### License and credits ### + +The library is licensed under [LGPLv2.1](https://github.com/Links2004/arduinoWebSockets/blob/master/LICENSE) + +[libb64](http://libb64.sourceforge.net/) written by Chris Venter. It is distributed under Public Domain see [LICENSE](https://github.com/Links2004/arduinoWebSockets/blob/master/src/libb64/LICENSE). diff --git a/lib/WebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf b/lib/WebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf new file mode 100644 index 00000000..ec5aa89f --- /dev/null +++ b/lib/WebSockets/examples/Nginx/esp8266.ssl.reverse.proxy.conf @@ -0,0 +1,83 @@ +# ESP8266 nginx SSL reverse proxy configuration file (tested and working on nginx v1.10.0) + +# proxy cache location +proxy_cache_path /opt/etc/nginx/cache levels=1:2 keys_zone=ESP8266_cache:10m max_size=10g inactive=5m use_temp_path=off; + +# webserver proxy +server { + + # general server parameters + listen 50080; + server_name myDomain.net; + access_log /opt/var/log/nginx/myDomain.net.access.log; + + # SSL configuration + ssl on; + ssl_certificate /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/fullchain.pem; + ssl_certificate_key /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/privkey.pem; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + location / { + + # proxy caching configuration + proxy_cache ESP8266_cache; + proxy_cache_revalidate on; + proxy_cache_min_uses 1; + proxy_cache_use_stale off; + proxy_cache_lock on; + # proxy_cache_bypass $http_cache_control; + # include the sessionId cookie value as part of the cache key - keeps the cache per user + # proxy_cache_key $proxy_host$request_uri$cookie_sessionId; + + # header pass through configuration + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # ESP8266 custom headers which identify to the device that it's running through an SSL proxy + proxy_set_header X-SSL On; + proxy_set_header X-SSL-WebserverPort 50080; + proxy_set_header X-SSL-WebsocketPort 50081; + + # extra debug headers + add_header X-Proxy-Cache $upstream_cache_status; + add_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # actual proxying configuration + proxy_ssl_session_reuse on; + # target the IP address of the device with proxy_pass + proxy_pass http://192.168.0.20; + proxy_read_timeout 90; + } + } + +# websocket proxy +server { + + # general server parameters + listen 50081; + server_name myDomain.net; + access_log /opt/var/log/nginx/myDomain.net.wss.access.log; + + # SSL configuration + ssl on; + ssl_certificate /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/fullchain.pem; + ssl_certificate_key /usr/builtin/etc/certificate/lets-encrypt/myDomain.net/privkey.pem; + ssl_session_cache builtin:1000 shared:SSL:10m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4; + ssl_prefer_server_ciphers on; + + location / { + + # websocket upgrade tunnel configuration + proxy_pass http://192.168.0.20:81; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_read_timeout 86400; + } + } diff --git a/lib/WebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino b/lib/WebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino new file mode 100644 index 00000000..9d49d149 --- /dev/null +++ b/lib/WebSockets/examples/avr/WebSocketClientAVR/WebSocketClientAVR.ino @@ -0,0 +1,84 @@ +/* + * WebSocketClientAVR.ino + * + * Created on: 10.12.2015 + * + */ + +#include + +#include +#include + +#include + + + +// Enter a MAC address for your controller below. +// Newer Ethernet shields have a MAC address printed on a sticker on the shield +byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; + +// Set the static IP address to use if the DHCP fails to assign +IPAddress ip(192, 168, 0, 177); + +WebSocketsClient webSocket; + + + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + + switch(type) { + case WStype_DISCONNECTED: + Serial.println("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + Serial.print("[WSc] Connected to url: "); + Serial.println((char *)payload); + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + Serial.print("[WSc] get text: "); + Serial.println((char *)payload); + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + Serial.print("[WSc] get binary length: "); + Serial.println(length); + // hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() +{ + // Open serial communications and wait for port to open: + Serial.begin(115200); + while (!Serial) {} + + // start the Ethernet connection: + if (Ethernet.begin(mac) == 0) { + Serial.println("Failed to configure Ethernet using DHCP"); + // no point in carrying on, so do nothing forevermore: + // try to congifure using IP address instead of DHCP: + Ethernet.begin(mac, ip); + } + + webSocket.begin("192.168.0.123", 8011); + webSocket.onEvent(webSocketEvent); + +} + + +void loop() +{ + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino b/lib/WebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino new file mode 100644 index 00000000..5e5ead46 --- /dev/null +++ b/lib/WebSockets/examples/esp32/WebSocketClient/WebSocketClient.ino @@ -0,0 +1,110 @@ +/* + * WebSocketClient.ino + * + * Created on: 24.05.2015 + * + */ + +#include + +#include +#include +#include + +#include + + +WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial1 + +void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) { + const uint8_t* src = (const uint8_t*) mem; + USE_SERIAL.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len); + for(uint32_t i = 0; i < len; i++) { + if(i % cols == 0) { + USE_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i); + } + USE_SERIAL.printf("%02X ", *src); + src++; + } + USE_SERIAL.printf("\n"); +} + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // server address, port and URL + webSocket.begin("192.168.0.123", 81, "/"); + + // event handler + webSocket.onEvent(webSocketEvent); + + // use HTTP Basic Authorization this is optional remove if not needed + webSocket.setAuthorization("user", "Password"); + + // try ever 5000 again if connection has failed + webSocket.setReconnectInterval(5000); + +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino b/lib/WebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino new file mode 100644 index 00000000..9d722427 --- /dev/null +++ b/lib/WebSockets/examples/esp32/WebSocketClientSSL/WebSocketClientSSL.ino @@ -0,0 +1,106 @@ +/* + * WebSocketClientSSL.ino + * + * Created on: 10.12.2015 + * + * note SSL is only possible with the ESP8266 + * + */ + +#include + +#include +#include +#include + +#include + + +WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial1 + +void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) { + const uint8_t* src = (const uint8_t*) mem; + USE_SERIAL.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len); + for(uint32_t i = 0; i < len; i++) { + if(i % cols == 0) { + USE_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i); + } + USE_SERIAL.printf("%02X ", *src); + src++; + } + USE_SERIAL.printf("\n"); +} + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.beginSSL("192.168.0.123", 81); + webSocket.onEvent(webSocketEvent); + +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp32/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino b/lib/WebSockets/examples/esp32/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino new file mode 100644 index 00000000..af3572f9 --- /dev/null +++ b/lib/WebSockets/examples/esp32/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino @@ -0,0 +1,155 @@ +/* + * WebSocketClientSocketIOack.ino + * + * Created on: 20.07.2019 + * + */ + +#include + +#include +#include +#include + +#include + +#include +#include + +WiFiMulti WiFiMulti; +SocketIOclient socketIO; + +#define USE_SERIAL Serial + + +void socketIOEvent(socketIOmessageType_t type, uint8_t * payload, size_t length) { + switch(type) { + case sIOtype_DISCONNECT: + USE_SERIAL.printf("[IOc] Disconnected!\n"); + break; + case sIOtype_CONNECT: + USE_SERIAL.printf("[IOc] Connected to url: %s\n", payload); + + // join default namespace (no auto join in Socket.IO V3) + socketIO.send(sIOtype_CONNECT, "/"); + break; + case sIOtype_EVENT: + { + char * sptr = NULL; + int id = strtol((char *)payload, &sptr, 10); + USE_SERIAL.printf("[IOc] get event: %s id: %d\n", payload, id); + if(id) { + payload = (uint8_t *)sptr; + } + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, payload, length); + if(error) { + USE_SERIAL.print(F("deserializeJson() failed: ")); + USE_SERIAL.println(error.c_str()); + return; + } + + String eventName = doc[0]; + USE_SERIAL.printf("[IOc] event name: %s\n", eventName.c_str()); + + // Message Includes a ID for a ACK (callback) + if(id) { + // creat JSON message for Socket.IO (ack) + DynamicJsonDocument docOut(1024); + JsonArray array = docOut.to(); + + // add payload (parameters) for the ack (callback function) + JsonObject param1 = array.createNestedObject(); + param1["now"] = millis(); + + // JSON to String (serializion) + String output; + output += id; + serializeJson(docOut, output); + + // Send event + socketIO.send(sIOtype_ACK, output); + } + } + break; + case sIOtype_ACK: + USE_SERIAL.printf("[IOc] get ack: %u\n", length); + break; + case sIOtype_ERROR: + USE_SERIAL.printf("[IOc] get error: %u\n", length); + break; + case sIOtype_BINARY_EVENT: + USE_SERIAL.printf("[IOc] get binary: %u\n", length); + break; + case sIOtype_BINARY_ACK: + USE_SERIAL.printf("[IOc] get binary ack: %u\n", length); + break; + } +} + +void setup() { + //USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + String ip = WiFi.localIP().toString(); + USE_SERIAL.printf("[SETUP] WiFi Connected %s\n", ip.c_str()); + + // server address, port and URL + socketIO.begin("10.11.100.100", 8880, "/socket.io/?EIO=4"); + + // event handler + socketIO.onEvent(socketIOEvent); +} + +unsigned long messageTimestamp = 0; +void loop() { + socketIO.loop(); + + uint64_t now = millis(); + + if(now - messageTimestamp > 2000) { + messageTimestamp = now; + + // creat JSON message for Socket.IO (event) + DynamicJsonDocument doc(1024); + JsonArray array = doc.to(); + + // add evnet name + // Hint: socket.on('event_name', .... + array.add("event_name"); + + // add payload (parameters) for the event + JsonObject param1 = array.createNestedObject(); + param1["now"] = (uint32_t) now; + + // JSON to String (serializion) + String output; + serializeJson(doc, output); + + // Send event + socketIO.sendEVENT(output); + + // Print JSON for debugging + USE_SERIAL.println(output); + } +} diff --git a/lib/WebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino b/lib/WebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino new file mode 100644 index 00000000..3e0d4f5b --- /dev/null +++ b/lib/WebSockets/examples/esp32/WebSocketServer/WebSocketServer.ino @@ -0,0 +1,104 @@ +/* + * WebSocketServer.ino + * + * Created on: 22.05.2015 + * + */ + +#include + +#include +#include +#include + +#include + +WiFiMulti WiFiMulti; +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial1 + +void hexdump(const void *mem, uint32_t len, uint8_t cols = 16) { + const uint8_t* src = (const uint8_t*) mem; + USE_SERIAL.printf("\n[HEXDUMP] Address: 0x%08X len: 0x%X (%d)", (ptrdiff_t)src, len, len); + for(uint32_t i = 0; i < len; i++) { + if(i % cols == 0) { + USE_SERIAL.printf("\n[0x%08X] 0x%08X: ", (ptrdiff_t)src, i); + } + USE_SERIAL.printf("%02X ", *src); + src++; + } + USE_SERIAL.printf("\n"); +} + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + // send message to client + // webSocket.sendTXT(num, "message here"); + + // send data to all connected clients + // webSocket.broadcastTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + hexdump(payload, length); + + // send message to client + // webSocket.sendBIN(num, payload, length); + break; + case WStype_ERROR: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.begin(); + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino b/lib/WebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino new file mode 100644 index 00000000..5ee489cd --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClient/WebSocketClient.ino @@ -0,0 +1,106 @@ +/* + * WebSocketClient.ino + * + * Created on: 24.05.2015 + * + */ + +#include + +#include +#include + +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial1 + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + case WStype_PING: + // pong will be send automatically + USE_SERIAL.printf("[WSc] get ping\n"); + break; + case WStype_PONG: + // answer to a ping we send + USE_SERIAL.printf("[WSc] get pong\n"); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // server address, port and URL + webSocket.begin("192.168.0.123", 81, "/"); + + // event handler + webSocket.onEvent(webSocketEvent); + + // use HTTP Basic Authorization this is optional remove if not needed + webSocket.setAuthorization("user", "Password"); + + // try ever 5000 again if connection has failed + webSocket.setReconnectInterval(5000); + + // start heartbeat (optional) + // ping server every 15000 ms + // expect pong from server within 3000 ms + // consider connection disconnected if pong is not received 2 times + webSocket.enableHeartbeat(15000, 3000, 2); + +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientOTA/README.md b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/README.md new file mode 100644 index 00000000..496eef25 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/README.md @@ -0,0 +1,27 @@ +## Minimal example of WebsocketClientOTA and Python server + +Take this as small example, how achieve OTA update on ESP8266 and ESP32. + +Python server was wrote from train so take it only as bare example. +It's working, but it's not mean to run in production. + + +### Usage: + +Start server: +```bash +cd python_ota_server +python3 -m venv .venv +source .venv/bin/activate +pip3 install -r requirements.txt +python3 main.py +``` + +Flash ESP with example sketch and start it. + +Change version inside example sketch to higher and compile it and save it to bin file. + +Rename it to `mydevice-1.0.1-esp8266.bin` and place it inside new folder firmware (server create it). + +When the ESP connect to server, it check if version flashed is equal to fw in firmware folder. If higher FW version is present, +start the flash process. \ No newline at end of file diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientOTA/WebSocketClientOTA.ino b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/WebSocketClientOTA.ino new file mode 100644 index 00000000..2c87c251 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/WebSocketClientOTA.ino @@ -0,0 +1,263 @@ +/* + * WebSocketClientOTA.ino + * + * Created on: 25.10.2021 + * + */ + +#include +#include + +#ifdef ESP8266 + #include + #include + #include +#endif +#ifdef ESP32 + #include "WiFi.h" + #include "ESPmDNS.h" + #include +#endif + +#include +#include + +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial + +// Variables: +// Settable: +const char *version = "1.0.0"; +const char *name = "mydevice"; + +// Others: +#ifdef ESP8266 + const char *chip = "esp8266"; +#endif +#ifdef ESP32 + const char *chip = "esp32"; +#endif + +uint32_t maxSketchSpace = 0; +int SketchSize = 0; +bool ws_conn = false; + +String IpAddress2String(const IPAddress& ipAddress) +{ + return String(ipAddress[0]) + String(".") + + String(ipAddress[1]) + String(".") + + String(ipAddress[2]) + String(".") + + String(ipAddress[3]); +} + +void greetings_(){ + StaticJsonDocument<200> doc; + doc["type"] = "greetings"; + doc["mac"] = WiFi.macAddress(); + doc["ip"] = IpAddress2String(WiFi.localIP()); + doc["version"] = version; + doc["name"] = name; + doc["chip"] = chip; + + char data[200]; + serializeJson(doc, data); + webSocket.sendTXT(data); +} + +void register_(){ + StaticJsonDocument<200> doc; + doc["type"] = "register"; + doc["mac"] = WiFi.macAddress(); + + char data[200]; + serializeJson(doc, data); + webSocket.sendTXT(data); + ws_conn = true; +} + +typedef void (*CALLBACK_FUNCTION)(JsonDocument &msg); + +typedef struct { + char type[50]; + CALLBACK_FUNCTION func; +} RESPONSES_STRUCT; + +void OTA(JsonDocument &msg){ + USE_SERIAL.print(F("[WSc] OTA mode: ")); + const char* go = "go"; + const char* ok = "ok"; + if(strncmp( msg["value"], go, strlen(go)) == 0 ) { + USE_SERIAL.print(F("go\n")); + SketchSize = int(msg["size"]); + maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; + USE_SERIAL.printf("[WSc] Max sketch size: %u\n", maxSketchSpace); + USE_SERIAL.printf("[WSc] Sketch size: %d\n", SketchSize); + USE_SERIAL.setDebugOutput(true); + if (!Update.begin(maxSketchSpace)) { //start with max available size + Update.printError(Serial); + ESP.restart(); + } + } else if (strncmp( msg["value"], ok, strlen(ok)) == 0) { + USE_SERIAL.print(F("OK\n")); + register_(); + } else { + USE_SERIAL.print(F("unknown value : ")); + USE_SERIAL.print(msg["value"].as()); + USE_SERIAL.print(F("\n")); + } +} + +void STATE(JsonDocument &msg){ + // Do something with message +} + +RESPONSES_STRUCT responses[] = { + {"ota", OTA}, + {"state", STATE}, +}; + +void text(uint8_t * payload, size_t length){ + // Convert mesage to something usable + char msgch[length]; + for (unsigned int i = 0; i < length; i++) + { + USE_SERIAL.print((char)payload[i]); + msgch[i] = ((char)payload[i]); + } + msgch[length] = '\0'; + + // Parse Json + StaticJsonDocument<200> doc_in; + DeserializationError error = deserializeJson(doc_in, msgch); + + if (error) { + USE_SERIAL.print(F("deserializeJson() failed: ")); + USE_SERIAL.println(error.c_str()); + return; + } + + // Handle each TYPE of message + int b = 0; + + for( b=0 ; strlen(responses[b].type) ; b++ ) + { + if( strncmp(doc_in["type"], responses[b].type, strlen(responses[b].type)) == 0 ) { + responses[b].func(doc_in); + } + } +} + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + // webSocket.sendTXT("Connected"); + greetings_(); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + text(payload, length); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + // hexdump(payload, length); + if (Update.write(payload, length) != length) { + Update.printError(Serial); + ESP.restart(); + } + yield(); + SketchSize -= length; + USE_SERIAL.printf("[WSc] Sketch size left: %u\n", SketchSize); + if (SketchSize < 1){ + if (Update.end(true)) { //true to set the size to the current progress + USE_SERIAL.printf("Update Success: \nRebooting...\n"); + delay(5); + yield(); + ESP.restart(); + } else { + Update.printError(USE_SERIAL); + ESP.restart(); + } + USE_SERIAL.setDebugOutput(false); + } + + // send data to server + // webSocket.sendBIN(payload, length); + break; + case WStype_PING: + // pong will be send automatically + USE_SERIAL.printf("[WSc] get ping\n"); + break; + case WStype_PONG: + // answer to a ping we send + USE_SERIAL.printf("[WSc] get pong\n"); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.print(F("\nMAC: ")); + USE_SERIAL.println(WiFi.macAddress()); + USE_SERIAL.print(F("\nDevice: ")); + USE_SERIAL.println(name); + USE_SERIAL.printf("\nVersion: %s\n", version); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "PASS"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // server address, port and URL + webSocket.begin("10.0.1.5", 8081, "/"); + + // event handler + webSocket.onEvent(webSocketEvent); + + // use HTTP Basic Authorization this is optional remove if not needed + // webSocket.setAuthorization("USER", "PASS"); + + // try ever 5000 again if connection has failed + webSocket.setReconnectInterval(5000); + + // start heartbeat (optional) + // ping server every 15000 ms + // expect pong from server within 3000 ms + // consider connection disconnected if pong is not received 2 times + webSocket.enableHeartbeat(15000, 3000, 2); + +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/main.py b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/main.py new file mode 100644 index 00000000..7e7fba11 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/main.py @@ -0,0 +1,235 @@ +"""Minimal example of Python websocket server +handling OTA updates for ESP32 amd ESP8266 + +Check and upload of firmware works. +Register and state function are jus for example. +""" +# pylint: disable=W0703,E1101 +import asyncio +import copy +import json +import logging +import subprocess +import threading +import time +from os import listdir +from os.path import join as join_pth +from pathlib import Path + +import websockets +from packaging import version + +# Logger settings +logging.basicConfig(filename="ws_server.log") +Logger = logging.getLogger('WS-OTA') +Logger.addHandler(logging.StreamHandler()) +Logger.setLevel(logging.INFO) + +# Path to directory with FW +fw_path = join_pth(Path().absolute(), "firmware") + + +def create_path(path: str) -> None: + """Check if path exist or create it""" + Path(path).mkdir(parents=True, exist_ok=True) + + +def shell(command): + """Handle execution of shell commands""" + with subprocess.Popen(command, shell=True, + stdout=subprocess.PIPE, + universal_newlines=True + ) as process: + for stdout_line in iter(process.stdout.readline, ""): + Logger.debug(stdout_line) + process.stdout.close() + return_code = process.wait() + Logger.debug("Shell returned: %s", return_code) + + return process.returncode + return None + + +async def binary_send(websocket, fw_file): + """Read firmware file, divide it to chunks and send them""" + with open(fw_file, "rb") as binaryfile: + + while True: + chunk = binaryfile.read(2048) + if not chunk: + break + try: + await websocket.send(chunk) + except Exception as exception: + Logger.exception(exception) + return False + time.sleep(0.2) + + +def version_checker(name, vdev, vapp): + """Parse and compare FW version""" + + if version.parse(vdev) < version.parse(vapp): + Logger.info("Client(%s) version %s is smaller than %s: Go for update", name, vdev, vapp) + return True + Logger.info("Client(%s) version %s is greater or equal to %s: Not updating", name, vdev, vapp) + return False + + +class WsOtaHandler (threading.Thread): + """Thread handling ota update + + Runing ota directly from message would kill WS + as message bus would timeout. + """ + def __init__(self, name, message, websocket): + threading.Thread.__init__(self, daemon=True) + self.name = name + self.msg = message + self.websocket = websocket + + def run(self, ): + try: + asyncio.run(self.start_) + except Exception as exception: + Logger.exception(exception) + finally: + pass + + async def start_(self): + """Start _ota se asyncio future""" + msg_task = asyncio.ensure_future( + self._ota()) + + done, pending = await asyncio.wait( + [msg_task], + return_when=asyncio.FIRST_COMPLETED, + ) + Logger.info("WS Ota Handler done: %s", done) + for task in pending: + task.cancel() + + async def _ota(self): + """Check for new fw and update or pass""" + device_name = self.msg['name'] + device_chip = self.msg['chip'] + device_version = self.msg['version'] + fw_version = '' + fw_name = '' + fw_device = '' + + for filename in listdir(fw_path): + fw_info = filename.split("-") + fw_device = fw_info[0] + if fw_device == device_name: + fw_version = fw_info[1] + fw_name = filename + break + + if not fw_version: + Logger.info("Client(%s): No fw found!", device_name) + msg = '{"type": "ota", "value":"ok"}' + await self.websocket.send(msg) + return + + if not version_checker(device_name, device_version, fw_version): + return + + fw_file = join_pth(fw_path, fw_name) + if device_chip == 'esp8266' and not fw_file.endswith('.gz'): + # We can compress fw to make it smaller for upload + fw_cpress = fw_file + fw_file = fw_cpress + ".gz" + cpress = f"gzip -9 {fw_cpress}" + cstate = shell(cpress) + if cstate: + Logger.error("Cannot compress firmware: %s", fw_name) + return + + # Get size of fw + size = Path(fw_file).stat().st_size + + # Request ota mode + msg = '{"type": "ota", "value":"go", "size":' + str(size) + '}' + await self.websocket.send(msg) + + # send file by chunks trough websocket + await binary_send(self.websocket, fw_file) + + +async def _register(websocket, message): + mac = message.get('mac') + name = message.get('name') + Logger.info("Client(%s) mac: %s", name, mac) + # Some code + + response = {'response_type': 'registry', 'state': 'ok'} + await websocket.send(json.dumps(response)) + + +async def _state(websocket, message): + mac = message.get('mac') + name = message.get('name') + Logger.info("Client(%s) mac: %s", name, mac) + # Some code + + response = {'response_type': 'state', 'state': 'ok'} + await websocket.send(json.dumps(response)) + + +async def _unhandleld(websocket, msg): + Logger.info("Unhandled message from device: %s", str(msg)) + response = {'response_type': 'response', 'state': 'nok'} + await websocket.send(json.dumps(response)) + + +async def _greetings(websocket, message): + WsOtaHandler('thread_ota', copy.deepcopy(message), websocket).start() + + +async def message_received(websocket, message) -> None: + """Handle incoming messages + + Check if message contain json and run waned function + """ + switcher = {"greetings": _greetings, + "register": _register, + "state": _state + } + + if message[0:1] == "{": + try: + msg_json = json.loads(message) + except Exception as exception: + Logger.error(exception) + return + + type_ = msg_json.get('type') + name = msg_json.get('name') + func = switcher.get(type_, _unhandleld) + Logger.debug("Client(%s)said: %s", name, type_) + + try: + await func(websocket, msg_json) + except Exception as exception: + Logger.error(exception) + + +# pylint: disable=W0613 +async def ws_server(websocket, path) -> None: + """Run in cycle and wait for new messages""" + async for message in websocket: + await message_received(websocket, message) + + +async def main(): + """Server starter + + Normal user can bind only port nubers greater than 1024 + """ + async with websockets.serve(ws_server, "10.0.1.5", 8081): + await asyncio.Future() # run forever + + +create_path(fw_path) +asyncio.run(main()) diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/requirements.txt b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/requirements.txt new file mode 100644 index 00000000..4fc2553f --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientOTA/python_ota_server/requirements.txt @@ -0,0 +1,2 @@ +packaging +websockets \ No newline at end of file diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino b/lib/WebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino new file mode 100644 index 00000000..d45060e9 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientSSL/WebSocketClientSSL.ino @@ -0,0 +1,88 @@ +/* + * WebSocketClientSSL.ino + * + * Created on: 10.12.2015 + * + * note SSL is only possible with the ESP8266 + * + */ + +#include + +#include +#include + +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + + +#define USE_SERIAL Serial1 + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + // send message to server when Connected + webSocket.sendTXT("Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + // send message to server + // webSocket.sendTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.beginSSL("192.168.0.123", 81); + webSocket.onEvent(webSocketEvent); + +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientSSLWithCA/WebSocketClientSSLWithCA.ino b/lib/WebSockets/examples/esp8266/WebSocketClientSSLWithCA/WebSocketClientSSLWithCA.ino new file mode 100644 index 00000000..214f5e61 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientSSLWithCA/WebSocketClientSSLWithCA.ino @@ -0,0 +1,103 @@ +/* + * WebSocketClientSSLWithCA.ino + * + * Created on: 27.10.2019 + * + * note SSL is only possible with the ESP8266 + * + */ + +#include + +#include +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +WebSocketsClient webSocket; + +#define USE_SERIAL Serial1 + + +// Can be obtained with: +// openssl s_client -showcerts -connect echo.websocket.org:443 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + //When using BearSSL, client certificate and private key can be set: + //webSocket.setSSLClientCertKey(clientCert, clientPrivateKey); + //clientCert and clientPrivateKey can be of types (const char *, const char *) , or of types (BearSSL::X509List, BearSSL::PrivateKey) + + webSocket.beginSslWithCA("echo.websocket.org", 443, "/", ENDPOINT_CA_CERT); + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino b/lib/WebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino new file mode 100644 index 00000000..5ceacea4 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientSocketIO/WebSocketClientSocketIO.ino @@ -0,0 +1,128 @@ +/* + * WebSocketClientSocketIO.ino + * + * Created on: 06.06.2016 + * + */ + +#include + +#include +#include + +#include + +#include +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +SocketIOclient socketIO; + +#define USE_SERIAL Serial1 + +void socketIOEvent(socketIOmessageType_t type, uint8_t * payload, size_t length) { + switch(type) { + case sIOtype_DISCONNECT: + USE_SERIAL.printf("[IOc] Disconnected!\n"); + break; + case sIOtype_CONNECT: + USE_SERIAL.printf("[IOc] Connected to url: %s\n", payload); + + // join default namespace (no auto join in Socket.IO V3) + socketIO.send(sIOtype_CONNECT, "/"); + break; + case sIOtype_EVENT: + USE_SERIAL.printf("[IOc] get event: %s\n", payload); + break; + case sIOtype_ACK: + USE_SERIAL.printf("[IOc] get ack: %u\n", length); + hexdump(payload, length); + break; + case sIOtype_ERROR: + USE_SERIAL.printf("[IOc] get error: %u\n", length); + hexdump(payload, length); + break; + case sIOtype_BINARY_EVENT: + USE_SERIAL.printf("[IOc] get binary: %u\n", length); + hexdump(payload, length); + break; + case sIOtype_BINARY_ACK: + USE_SERIAL.printf("[IOc] get binary ack: %u\n", length); + hexdump(payload, length); + break; + } +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + // disable AP + if(WiFi.getMode() & WIFI_AP) { + WiFi.softAPdisconnect(true); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + String ip = WiFi.localIP().toString(); + USE_SERIAL.printf("[SETUP] WiFi Connected %s\n", ip.c_str()); + + // server address, port and URL + socketIO.begin("10.11.100.100", 8880, "/socket.io/?EIO=4"); + + // event handler + socketIO.onEvent(socketIOEvent); +} + +unsigned long messageTimestamp = 0; +void loop() { + socketIO.loop(); + + uint64_t now = millis(); + + if(now - messageTimestamp > 2000) { + messageTimestamp = now; + + // creat JSON message for Socket.IO (event) + DynamicJsonDocument doc(1024); + JsonArray array = doc.to(); + + // add evnet name + // Hint: socket.on('event_name', .... + array.add("event_name"); + + // add payload (parameters) for the event + JsonObject param1 = array.createNestedObject(); + param1["now"] = (uint32_t) now; + + // JSON to String (serializion) + String output; + serializeJson(doc, output); + + // Send event + socketIO.sendEVENT(output); + + // Print JSON for debugging + USE_SERIAL.println(output); + } +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino b/lib/WebSockets/examples/esp8266/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino new file mode 100644 index 00000000..3e4f87e1 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientSocketIOack/WebSocketClientSocketIOack.ino @@ -0,0 +1,165 @@ +/* + * WebSocketClientSocketIOack.ino + * + * Created on: 20.07.2019 + * + */ + +#include + +#include +#include + +#include + +#include +#include + +#include + +ESP8266WiFiMulti WiFiMulti; +SocketIOclient socketIO; + +#define USE_SERIAL Serial + + +void socketIOEvent(socketIOmessageType_t type, uint8_t * payload, size_t length) { + switch(type) { + case sIOtype_DISCONNECT: + USE_SERIAL.printf("[IOc] Disconnected!\n"); + break; + case sIOtype_CONNECT: + USE_SERIAL.printf("[IOc] Connected to url: %s\n", payload); + + // join default namespace (no auto join in Socket.IO V3) + socketIO.send(sIOtype_CONNECT, "/"); + break; + case sIOtype_EVENT: + { + char * sptr = NULL; + int id = strtol((char *)payload, &sptr, 10); + USE_SERIAL.printf("[IOc] get event: %s id: %d\n", payload, id); + if(id) { + payload = (uint8_t *)sptr; + } + DynamicJsonDocument doc(1024); + DeserializationError error = deserializeJson(doc, payload, length); + if(error) { + USE_SERIAL.print(F("deserializeJson() failed: ")); + USE_SERIAL.println(error.c_str()); + return; + } + + String eventName = doc[0]; + USE_SERIAL.printf("[IOc] event name: %s\n", eventName.c_str()); + + // Message Includes a ID for a ACK (callback) + if(id) { + // creat JSON message for Socket.IO (ack) + DynamicJsonDocument docOut(1024); + JsonArray array = docOut.to(); + + // add payload (parameters) for the ack (callback function) + JsonObject param1 = array.createNestedObject(); + param1["now"] = millis(); + + // JSON to String (serializion) + String output; + output += id; + serializeJson(docOut, output); + + // Send event + socketIO.send(sIOtype_ACK, output); + } + } + break; + case sIOtype_ACK: + USE_SERIAL.printf("[IOc] get ack: %u\n", length); + hexdump(payload, length); + break; + case sIOtype_ERROR: + USE_SERIAL.printf("[IOc] get error: %u\n", length); + hexdump(payload, length); + break; + case sIOtype_BINARY_EVENT: + USE_SERIAL.printf("[IOc] get binary: %u\n", length); + hexdump(payload, length); + break; + case sIOtype_BINARY_ACK: + USE_SERIAL.printf("[IOc] get binary ack: %u\n", length); + hexdump(payload, length); + break; + } +} + +void setup() { + //USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + // disable AP + if(WiFi.getMode() & WIFI_AP) { + WiFi.softAPdisconnect(true); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + //WiFi.disconnect(); + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + String ip = WiFi.localIP().toString(); + USE_SERIAL.printf("[SETUP] WiFi Connected %s\n", ip.c_str()); + + // server address, port and URL + socketIO.begin("10.11.100.100", 8880, "/socket.io/?EIO=4"); + + // event handler + socketIO.onEvent(socketIOEvent); +} + +unsigned long messageTimestamp = 0; +void loop() { + socketIO.loop(); + + uint64_t now = millis(); + + if(now - messageTimestamp > 2000) { + messageTimestamp = now; + + // creat JSON message for Socket.IO (event) + DynamicJsonDocument doc(1024); + JsonArray array = doc.to(); + + // add evnet name + // Hint: socket.on('event_name', .... + array.add("event_name"); + + // add payload (parameters) for the event + JsonObject param1 = array.createNestedObject(); + param1["now"] = (uint32_t) now; + + // JSON to String (serializion) + String output; + serializeJson(doc, output); + + // Send event + socketIO.sendEVENT(output); + + // Print JSON for debugging + USE_SERIAL.println(output); + } +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino b/lib/WebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino new file mode 100644 index 00000000..a0eb011f --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientStomp/WebSocketClientStomp.ino @@ -0,0 +1,149 @@ +/* + WebSocketClientStomp.ino + + Example for connecting and maintining a connection with a STOMP websocket connection. + In this example, we connect to a Spring application (see https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html). + + Created on: 25.09.2017 + Author: Martin Becker , Contact: becker@informatik.uni-wuerzburg.de +*/ + +// PRE + +#define USE_SERIAL Serial + + +// LIBRARIES + +#include +#include + +#include +#include + + +// SETTINGS + +const char* wlan_ssid = "yourssid"; +const char* wlan_password = "somepassword"; + +const char* ws_host = "the.host.net"; +const int ws_port = 80; + +// URL for STOMP endpoint. +// For the default config of Spring's STOMP support, the default URL is "/socketentry/websocket". +const char* stompUrl = "/socketentry/websocket"; // don't forget the leading "/" !!! + + +// VARIABLES + +WebSocketsClient webSocket; + + +// FUNCTIONS + +/** + * STOMP messages need to be NULL-terminated (i.e., \0 or \u0000). + * However, when we send a String or a char[] array without specifying + * a length, the size of the message payload is derived by strlen() internally, + * thus dropping any NULL values appended to the "msg"-String. + * + * To solve this, we first convert the String to a NULL terminated char[] array + * via "c_str" and set the length of the payload to include the NULL value. + */ +void sendMessage(String & msg) { + webSocket.sendTXT(msg.c_str(), msg.length() + 1); +} + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch (type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + + String msg = "CONNECT\r\naccept-version:1.1,1.0\r\nheart-beat:10000,10000\r\n\r\n"; + sendMessage(msg); + } + break; + case WStype_TEXT: + { + // ##################### + // handle STOMP protocol + // ##################### + + String text = (char*) payload; + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + if (text.startsWith("CONNECTED")) { + + // subscribe to some channels + + String msg = "SUBSCRIBE\nid:sub-0\ndestination:/user/queue/messages\n\n"; + sendMessage(msg); + delay(1000); + + // and send a message + + msg = "SEND\ndestination:/app/message\n\n{\"user\":\"esp\",\"message\":\"Hello!\"}"; + sendMessage(msg); + delay(1000); + + } else { + + // do something with messages + + } + + break; + } + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + + // setup serial + + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + // USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + + + // connect to WiFi + + USE_SERIAL.print("Logging into WLAN: "); Serial.print(wlan_ssid); Serial.print(" ..."); + WiFi.mode(WIFI_STA); + WiFi.begin(wlan_ssid, wlan_password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + USE_SERIAL.print("."); + } + USE_SERIAL.println(" success."); + USE_SERIAL.print("IP: "); USE_SERIAL.println(WiFi.localIP()); + + + // connect to websocket + webSocket.begin(ws_host, ws_port, stompUrl); + webSocket.setExtraHeaders(); // remove "Origin: file://" header because it breaks the connection with Spring's default websocket config + // webSocket.setExtraHeaders("foo: I am so funny\r\nbar: not"); // some headers, in case you feel funny + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino b/lib/WebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino new file mode 100644 index 00000000..cb0c45be --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketClientStompOverSockJs/WebSocketClientStompOverSockJs.ino @@ -0,0 +1,150 @@ +/* + WebSocketClientStompOverSockJs.ino + + Example for connecting and maintining a connection with a SockJS+STOMP websocket connection. + In this example, we connect to a Spring application (see https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html). + + Created on: 18.07.2017 + Author: Martin Becker , Contact: becker@informatik.uni-wuerzburg.de +*/ + +// PRE + +#define USE_SERIAL Serial + + +// LIBRARIES + +#include +#include + +#include +#include + + +// SETTINGS + +const char* wlan_ssid = "yourssid"; +const char* wlan_password = "somepassword"; + +const char* ws_host = "the.host.net"; +const int ws_port = 80; + +// base URL for SockJS (websocket) connection +// The complete URL will look something like this(cf. http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-36): +// ws://://<3digits>//websocket +// For the default config of Spring's SockJS/STOMP support, the default base URL is "/socketentry/". +const char* ws_baseurl = "/socketentry/"; // don't forget leading and trailing "/" !!! + + +// VARIABLES + +WebSocketsClient webSocket; + + +// FUNCTIONS + +void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) { + + switch (type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[WSc] Disconnected!\n"); + break; + case WStype_CONNECTED: + { + USE_SERIAL.printf("[WSc] Connected to url: %s\n", payload); + } + break; + case WStype_TEXT: + { + // ##################### + // handle SockJs+STOMP protocol + // ##################### + + String text = (char*) payload; + + USE_SERIAL.printf("[WSc] get text: %s\n", payload); + + if (payload[0] == 'h') { + + USE_SERIAL.println("Heartbeat!"); + + } else if (payload[0] == 'o') { + + // on open connection + char *msg = "[\"CONNECT\\naccept-version:1.1,1.0\\nheart-beat:10000,10000\\n\\n\\u0000\"]"; + webSocket.sendTXT(msg); + + } else if (text.startsWith("a[\"CONNECTED")) { + + // subscribe to some channels + + char *msg = "[\"SUBSCRIBE\\nid:sub-0\\ndestination:/user/queue/messages\\n\\n\\u0000\"]"; + webSocket.sendTXT(msg); + delay(1000); + + // and send a message + + msg = "[\"SEND\\ndestination:/app/message\\n\\n{\\\"user\\\":\\\"esp\\\",\\\"message\\\":\\\"Hello!\\\"}\\u0000\"]"; + webSocket.sendTXT(msg); + delay(1000); + } + + break; + } + case WStype_BIN: + USE_SERIAL.printf("[WSc] get binary length: %u\n", length); + hexdump(payload, length); + + // send data to server + // webSocket.sendBIN(payload, length); + break; + } + +} + +void setup() { + + // setup serial + + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + // USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + + + // connect to WiFi + + USE_SERIAL.print("Logging into WLAN: "); Serial.print(wlan_ssid); Serial.print(" ..."); + WiFi.mode(WIFI_STA); + WiFi.begin(wlan_ssid, wlan_password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + USE_SERIAL.print("."); + } + USE_SERIAL.println(" success."); + USE_SERIAL.print("IP: "); USE_SERIAL.println(WiFi.localIP()); + + + // ##################### + // create socket url according to SockJS protocol (cf. http://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html#section-36) + // ##################### + String socketUrl = ws_baseurl; + socketUrl += random(0, 999); + socketUrl += "/"; + socketUrl += random(0, 999999); // should be a random string, but this works (see ) + socketUrl += "/websocket"; + + // connect to websocket + webSocket.begin(ws_host, ws_port, socketUrl); + webSocket.setExtraHeaders(); // remove "Origin: file://" header because it breaks the connection with Spring's default websocket config + // webSocket.setExtraHeaders("foo: I am so funny\r\nbar: not"); // some headers, in case you feel funny + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino b/lib/WebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino new file mode 100644 index 00000000..1ac3002d --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServer/WebSocketServer.ino @@ -0,0 +1,86 @@ +/* + * WebSocketServer.ino + * + * Created on: 22.05.2015 + * + */ + +#include + +#include +#include +#include +#include + +ESP8266WiFiMulti WiFiMulti; + +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial1 + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + // send message to client + // webSocket.sendTXT(num, "message here"); + + // send data to all connected clients + // webSocket.broadcastTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + hexdump(payload, length); + + // send message to client + // webSocket.sendBIN(num, payload, length); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.begin(); + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} + diff --git a/lib/WebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino b/lib/WebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino new file mode 100644 index 00000000..5fed1a95 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServerAllFunctionsDemo/WebSocketServerAllFunctionsDemo.ino @@ -0,0 +1,132 @@ +/* + * WebSocketServerAllFunctionsDemo.ino + * + * Created on: 10.05.2018 + * + */ + +#include + +#include +#include +#include +#include +#include +#include + +#define LED_RED 15 +#define LED_GREEN 12 +#define LED_BLUE 13 + +#define USE_SERIAL Serial + +ESP8266WiFiMulti WiFiMulti; + +ESP8266WebServer server(80); +WebSocketsServer webSocket = WebSocketsServer(81); + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + if(payload[0] == '#') { + // we get RGB data + + // decode rgb data + uint32_t rgb = (uint32_t) strtol((const char *) &payload[1], NULL, 16); + + analogWrite(LED_RED, ((rgb >> 16) & 0xFF)); + analogWrite(LED_GREEN, ((rgb >> 8) & 0xFF)); + analogWrite(LED_BLUE, ((rgb >> 0) & 0xFF)); + } + + break; + } + +} + +void setup() { + //USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + pinMode(LED_RED, OUTPUT); + pinMode(LED_GREEN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + + digitalWrite(LED_RED, 1); + digitalWrite(LED_GREEN, 1); + digitalWrite(LED_BLUE, 1); + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // start webSocket server + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + + if(MDNS.begin("esp8266")) { + USE_SERIAL.println("MDNS responder started"); + } + + // handle index + server.on("/", []() { + // send index.html + server.send(200, "text/html", "LED Control:

R:
G:
B:
"); + }); + + server.begin(); + + // Add service to MDNS + MDNS.addService("http", "tcp", 80); + MDNS.addService("ws", "tcp", 81); + + digitalWrite(LED_RED, 0); + digitalWrite(LED_GREEN, 0); + digitalWrite(LED_BLUE, 0); + +} + +unsigned long last_10sec = 0; +unsigned int counter = 0; + +void loop() { + unsigned long t = millis(); + webSocket.loop(); + server.handleClient(); + + if((t - last_10sec) > 10 * 1000) { + counter++; + bool ping = (counter % 2); + int i = webSocket.connectedClients(ping); + USE_SERIAL.printf("%d Connected websocket clients ping: %d\n", i, ping); + last_10sec = millis(); + } +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino b/lib/WebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino new file mode 100644 index 00000000..84c9775d --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServerFragmentation/WebSocketServerFragmentation.ino @@ -0,0 +1,94 @@ +/* + * WebSocketServer.ino + * + * Created on: 22.05.2015 + * + */ + +#include + +#include +#include +#include +#include + +ESP8266WiFiMulti WiFiMulti; + +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial + +String fragmentBuffer = ""; + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + break; + case WStype_BIN: + USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + hexdump(payload, length); + break; + + // Fragmentation / continuation opcode handling + // case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT_TEXT_START: + fragmentBuffer = (char*)payload; + USE_SERIAL.printf("[%u] get start start of Textfragment: %s\n", num, payload); + break; + case WStype_FRAGMENT: + fragmentBuffer += (char*)payload; + USE_SERIAL.printf("[%u] get Textfragment : %s\n", num, payload); + break; + case WStype_FRAGMENT_FIN: + fragmentBuffer += (char*)payload; + USE_SERIAL.printf("[%u] get end of Textfragment: %s\n", num, payload); + USE_SERIAL.printf("[%u] full frame: %s\n", num, fragmentBuffer.c_str()); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + webSocket.begin(); + webSocket.onEvent(webSocketEvent); +} + +void loop() { + webSocket.loop(); +} + diff --git a/lib/WebSockets/examples/esp8266/WebSocketServerHooked/WebSocketServerHooked.ino b/lib/WebSockets/examples/esp8266/WebSocketServerHooked/WebSocketServerHooked.ino new file mode 100644 index 00000000..e762626b --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServerHooked/WebSocketServerHooked.ino @@ -0,0 +1,103 @@ +/* + * WebSocketServerHooked.ino + * + * Created on: 22.05.2015 + * Hooked on: 28.10.2020 + * + */ + +#include + +#include +#include +#include +#include +#include + +ESP8266WiFiMulti WiFiMulti; + +ESP8266WebServer server(80); +WebSockets4WebServer webSocket; + +#define USE_SERIAL Serial + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: + { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + // send message to client + // webSocket.sendTXT(num, "message here"); + + // send data to all connected clients + // webSocket.broadcastTXT("message here"); + break; + case WStype_BIN: + USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); + hexdump(payload, length); + + // send message to client + // webSocket.sendBIN(num, payload, length); + break; + } + +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + server.on("/", []() { + server.send(200, "text/plain", "I am a regular webserver on port 80!\r\n"); + server.send(200, "text/plain", "I am also a websocket server on '/ws' on the same port 80\r\n"); + }); + + server.addHook(webSocket.hookForWebserver("/ws", webSocketEvent)); + + server.begin(); + Serial.println("HTTP server started on port 80"); + Serial.println("WebSocket server started on the same port"); + Serial.printf("my network address is either 'arduinoWebsockets.local' (mDNS) or '%s'\n", WiFi.localIP().toString().c_str()); + + if (!MDNS.begin("arduinoWebsockets")) { + Serial.println("Error setting up MDNS responder!"); + } +} + +void loop() { + server.handleClient(); + webSocket.loop(); + MDNS.update(); +} diff --git a/lib/WebSockets/examples/esp8266/WebSocketServerHooked/emu b/lib/WebSockets/examples/esp8266/WebSocketServerHooked/emu new file mode 100644 index 00000000..867a8cd7 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServerHooked/emu @@ -0,0 +1,20 @@ +#!/bin/sh + +# linux script to compile&run arduinoWebSockets in a mock environment + +if [ -z "$ESP8266ARDUINO" ]; then + echo "please set ESP8266ARDUINO env-var to where esp8266/arduino sits" + exit 1 +fi + +set -e + +where=$(pwd) + +cd $ESP8266ARDUINO/tests/host/ + +make -j FORCE32=0 \ + ULIBDIRS=../../libraries/Hash/:~/dev/proj/arduino/libraries/arduinoWebSockets \ + ${where}/WebSocketServerHooked + +valgrind ./bin/WebSocketServerHooked/WebSocketServerHooked -b "$@" diff --git a/lib/WebSockets/examples/esp8266/WebSocketServerHooked/ws-testclient.py b/lib/WebSockets/examples/esp8266/WebSocketServerHooked/ws-testclient.py new file mode 100644 index 00000000..546c7ff2 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServerHooked/ws-testclient.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +# python websocket client to test with +# emulator: server is at ws://127.0.0.1:9080/ws +# esp8266: server is at ws:///ws +# (uncomment the right line below) + +#uri = "ws://127.0.0.1:9080/ws" +uri = "ws://arduinoWebsockets.local/ws" + +import websocket +try: + import thread +except ImportError: + import _thread as thread +import time + +def on_message(ws, message): + print("message"); + print(message) + +def on_error(ws, error): + print("error") + print(error) + +def on_close(ws): + print("### closed ###") + +def on_open(ws): + print("opened") + def run(*args): + for i in range(3): + time.sleep(1) + ws.send("Hello %d" % i) + time.sleep(1) + ws.close() + print("thread terminating...") + thread.start_new_thread(run, ()) + + +if __name__ == "__main__": + websocket.enableTrace(True) + ws = websocket.WebSocketApp(uri, on_message = on_message, on_error = on_error, on_close = on_close) + ws.on_open = on_open + ws.run_forever() diff --git a/lib/WebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino b/lib/WebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino new file mode 100644 index 00000000..8bc646c4 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServerHttpHeaderValidation/WebSocketServerHttpHeaderValidation.ino @@ -0,0 +1,86 @@ +/* + * WebSocketServerHttpHeaderValidation.ino + * + * Created on: 08.06.2016 + * + */ + +#include + +#include +#include +#include +#include + +ESP8266WiFiMulti WiFiMulti; + +WebSocketsServer webSocket = WebSocketsServer(81); + +#define USE_SERIAL Serial1 + +const unsigned long int validSessionId = 12345; //some arbitrary value to act as a valid sessionId + +/* + * Returns a bool value as an indicator to describe whether a user is allowed to initiate a websocket upgrade + * based on the value of a cookie. This function expects the rawCookieHeaderValue to look like this "sessionId=|" + */ +bool isCookieValid(String rawCookieHeaderValue) { + + if (rawCookieHeaderValue.indexOf("sessionId") != -1) { + String sessionIdStr = rawCookieHeaderValue.substring(rawCookieHeaderValue.indexOf("sessionId=") + 10, rawCookieHeaderValue.indexOf("|")); + unsigned long int sessionId = strtoul(sessionIdStr.c_str(), NULL, 10); + return sessionId == validSessionId; + } + return false; +} + +/* + * The WebSocketServerHttpHeaderValFunc delegate passed to webSocket.onValidateHttpHeader + */ +bool validateHttpHeader(String headerName, String headerValue) { + + //assume a true response for any headers not handled by this validator + bool valid = true; + + if(headerName.equalsIgnoreCase("Cookie")) { + //if the header passed is the Cookie header, validate it according to the rules in 'isCookieValid' function + valid = isCookieValid(headerValue); + } + + return valid; +} + +void setup() { + // USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //Serial.setDebugOutput(true); + USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + //connecting clients must supply a valid session cookie at websocket upgrade handshake negotiation time + const char * headerkeys[] = { "Cookie" }; + size_t headerKeyCount = sizeof(headerkeys) / sizeof(char*); + webSocket.onValidateHttpHeader(validateHttpHeader, headerkeys, headerKeyCount); + webSocket.begin(); +} + +void loop() { + webSocket.loop(); +} + diff --git a/lib/WebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino b/lib/WebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino new file mode 100644 index 00000000..8f32e753 --- /dev/null +++ b/lib/WebSockets/examples/esp8266/WebSocketServer_LEDcontrol/WebSocketServer_LEDcontrol.ino @@ -0,0 +1,121 @@ +/* + * WebSocketServer_LEDcontrol.ino + * + * Created on: 26.11.2015 + * + */ + +#include + +#include +#include +#include +#include +#include +#include + +#define LED_RED 15 +#define LED_GREEN 12 +#define LED_BLUE 13 + +#define USE_SERIAL Serial + + +ESP8266WiFiMulti WiFiMulti; + +ESP8266WebServer server(80); +WebSocketsServer webSocket = WebSocketsServer(81); + +void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + + switch(type) { + case WStype_DISCONNECTED: + USE_SERIAL.printf("[%u] Disconnected!\n", num); + break; + case WStype_CONNECTED: { + IPAddress ip = webSocket.remoteIP(num); + USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); + + // send message to client + webSocket.sendTXT(num, "Connected"); + } + break; + case WStype_TEXT: + USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); + + if(payload[0] == '#') { + // we get RGB data + + // decode rgb data + uint32_t rgb = (uint32_t) strtol((const char *) &payload[1], NULL, 16); + + analogWrite(LED_RED, ((rgb >> 16) & 0xFF)); + analogWrite(LED_GREEN, ((rgb >> 8) & 0xFF)); + analogWrite(LED_BLUE, ((rgb >> 0) & 0xFF)); + } + + break; + } + +} + +void setup() { + //USE_SERIAL.begin(921600); + USE_SERIAL.begin(115200); + + //USE_SERIAL.setDebugOutput(true); + + USE_SERIAL.println(); + USE_SERIAL.println(); + USE_SERIAL.println(); + + for(uint8_t t = 4; t > 0; t--) { + USE_SERIAL.printf("[SETUP] BOOT WAIT %d...\n", t); + USE_SERIAL.flush(); + delay(1000); + } + + pinMode(LED_RED, OUTPUT); + pinMode(LED_GREEN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); + + digitalWrite(LED_RED, 1); + digitalWrite(LED_GREEN, 1); + digitalWrite(LED_BLUE, 1); + + WiFiMulti.addAP("SSID", "passpasspass"); + + while(WiFiMulti.run() != WL_CONNECTED) { + delay(100); + } + + // start webSocket server + webSocket.begin(); + webSocket.onEvent(webSocketEvent); + + if(MDNS.begin("esp8266")) { + USE_SERIAL.println("MDNS responder started"); + } + + // handle index + server.on("/", []() { + // send index.html + server.send(200, "text/html", "LED Control:

R:
G:
B:
"); + }); + + server.begin(); + + // Add service to MDNS + MDNS.addService("http", "tcp", 80); + MDNS.addService("ws", "tcp", 81); + + digitalWrite(LED_RED, 0); + digitalWrite(LED_GREEN, 0); + digitalWrite(LED_BLUE, 0); + +} + +void loop() { + webSocket.loop(); + server.handleClient(); +} diff --git a/lib/WebSockets/examples/particle/ParticleWebSocketClient/application.cpp b/lib/WebSockets/examples/particle/ParticleWebSocketClient/application.cpp new file mode 100644 index 00000000..461228f3 --- /dev/null +++ b/lib/WebSockets/examples/particle/ParticleWebSocketClient/application.cpp @@ -0,0 +1,46 @@ +/* To compile using make CLI, create a folder under \firmware\user\applications and copy application.cpp there. +* Then, copy src files under particleWebSocket folder. +*/ + +#include "application.h" +#include "particleWebSocket/WebSocketsClient.h" + +WebSocketsClient webSocket; + +void webSocketEvent(WStype_t type, uint8_t* payload, size_t length) +{ + switch (type) + { + case WStype_DISCONNECTED: + Serial.printlnf("[WSc] Disconnected!"); + break; + case WStype_CONNECTED: + Serial.printlnf("[WSc] Connected to URL: %s", payload); + webSocket.sendTXT("Connected\r\n"); + break; + case WStype_TEXT: + Serial.printlnf("[WSc] get text: %s", payload); + break; + case WStype_BIN: + Serial.printlnf("[WSc] get binary length: %u", length); + break; + } +} + +void setup() +{ + Serial.begin(9600); + + WiFi.setCredentials("[SSID]", "[PASSWORD]", WPA2, WLAN_CIPHER_AES_TKIP); + WiFi.connect(); + + webSocket.begin("192.168.1.153", 85, "/ClientService/?variable=Test1212"); + webSocket.onEvent(webSocketEvent); +} + +void loop() +{ + webSocket.sendTXT("Hello world!"); + delay(500); + webSocket.loop(); +} diff --git a/lib/WebSockets/library.json b/lib/WebSockets/library.json new file mode 100644 index 00000000..86758316 --- /dev/null +++ b/lib/WebSockets/library.json @@ -0,0 +1,25 @@ +{ + "authors": [ + { + "maintainer": true, + "name": "Markus Sattler", + "url": "https://github.com/Links2004" + } + ], + "description": "WebSocket Server and Client for Arduino based on RFC6455", + "export": { + "exclude": [ + "tests" + ] + }, + "frameworks": "arduino", + "keywords": "wifi, http, web, server, client, websocket", + "license": "LGPL-2.1", + "name": "WebSockets", + "platforms": "atmelavr, espressif8266, espressif32", + "repository": { + "type": "git", + "url": "https://github.com/Links2004/arduinoWebSockets.git" + }, + "version": "2.3.7" +} \ No newline at end of file diff --git a/lib/WebSockets/library.properties b/lib/WebSockets/library.properties new file mode 100644 index 00000000..aab19e85 --- /dev/null +++ b/lib/WebSockets/library.properties @@ -0,0 +1,9 @@ +name=WebSockets +version=2.3.7 +author=Markus Sattler +maintainer=Markus Sattler +sentence=WebSockets for Arduino (Server + Client) +paragraph=use 2.x.x for ESP and 1.3 for AVR +category=Communication +url=https://github.com/Links2004/arduinoWebSockets +architectures=* diff --git a/lib/WebSockets/src/SocketIOclient.cpp b/lib/WebSockets/src/SocketIOclient.cpp new file mode 100644 index 00000000..bf611953 --- /dev/null +++ b/lib/WebSockets/src/SocketIOclient.cpp @@ -0,0 +1,260 @@ +/* + * SocketIOclient.cpp + * + * Created on: May 12, 2018 + * Author: links + */ + +#include "WebSockets.h" +#include "WebSocketsClient.h" +#include "SocketIOclient.h" + +SocketIOclient::SocketIOclient() { +} + +SocketIOclient::~SocketIOclient() { +} + +void SocketIOclient::begin(const char * host, uint16_t port, const char * url, const char * protocol) { + WebSocketsClient::beginSocketIO(host, port, url, protocol); + WebSocketsClient::enableHeartbeat(60 * 1000, 90 * 1000, 5); + initClient(); +} + +void SocketIOclient::begin(String host, uint16_t port, String url, String protocol) { + WebSocketsClient::beginSocketIO(host, port, url, protocol); + WebSocketsClient::enableHeartbeat(60 * 1000, 90 * 1000, 5); + initClient(); +} +#if defined(HAS_SSL) +void SocketIOclient::beginSSL(const char * host, uint16_t port, const char * url, const char * protocol) { + WebSocketsClient::beginSocketIOSSL(host, port, url, protocol); + WebSocketsClient::enableHeartbeat(60 * 1000, 90 * 1000, 5); + initClient(); +} + +void SocketIOclient::beginSSL(String host, uint16_t port, String url, String protocol) { + WebSocketsClient::beginSocketIOSSL(host, port, url, protocol); + WebSocketsClient::enableHeartbeat(60 * 1000, 90 * 1000, 5); + initClient(); +} +#if defined(SSL_BARESSL) +void SocketIOclient::beginSSLWithCA(const char * host, uint16_t port, const char * url, const char * CA_cert, const char * protocol) { + WebSocketsClient::beginSocketIOSSLWithCA(host, port, url, CA_cert, protocol); + WebSocketsClient::enableHeartbeat(60 * 1000, 90 * 1000, 5); + initClient(); +} + +void SocketIOclient::beginSSLWithCA(const char * host, uint16_t port, const char * url, BearSSL::X509List * CA_cert, const char * protocol) { + WebSocketsClient::beginSocketIOSSLWithCA(host, port, url, CA_cert, protocol); + WebSocketsClient::enableHeartbeat(60 * 1000, 90 * 1000, 5); + initClient(); +} + +void SocketIOclient::setSSLClientCertKey(const char * clientCert, const char * clientPrivateKey) { + WebSocketsClient::setSSLClientCertKey(clientCert, clientPrivateKey); +} + +void SocketIOclient::setSSLClientCertKey(BearSSL::X509List * clientCert, BearSSL::PrivateKey * clientPrivateKey) { + WebSocketsClient::setSSLClientCertKey(clientCert, clientPrivateKey); +} + +#endif +#endif + +void SocketIOclient::configureEIOping(bool disableHeartbeat) { + _disableHeartbeat = disableHeartbeat; +} + +void SocketIOclient::initClient(void) { + if(_client.cUrl.indexOf("EIO=4") != -1) { + DEBUG_WEBSOCKETS("[wsIOc] found EIO=4 disable EIO ping on client\n"); + configureEIOping(true); + } +} + +/** + * set callback function + * @param cbEvent SocketIOclientEvent + */ +void SocketIOclient::onEvent(SocketIOclientEvent cbEvent) { + _cbEvent = cbEvent; +} + +bool SocketIOclient::isConnected(void) { + return WebSocketsClient::isConnected(); +} + +void SocketIOclient::setExtraHeaders(const char * extraHeaders) { + return WebSocketsClient::setExtraHeaders(extraHeaders); +} + +void SocketIOclient::setReconnectInterval(unsigned long time) { + return WebSocketsClient::setReconnectInterval(time); +} + +/** + * send text data to client + * @param num uint8_t client id + * @param type socketIOmessageType_t + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool SocketIOclient::send(socketIOmessageType_t type, uint8_t * payload, size_t length, bool headerToPayload) { + bool ret = false; + if(length == 0) { + length = strlen((const char *)payload); + } + if(clientIsConnected(&_client) && _client.status == WSC_CONNECTED) { + if(!headerToPayload) { + // webSocket Header + ret = WebSocketsClient::sendFrameHeader(&_client, WSop_text, length + 2, true); + // Engine.IO / Socket.IO Header + if(ret) { + uint8_t buf[3] = { eIOtype_MESSAGE, type, 0x00 }; + ret = WebSocketsClient::write(&_client, buf, 2); + } + if(ret && payload && length > 0) { + ret = WebSocketsClient::write(&_client, payload, length); + } + return ret; + } else { + // TODO implement + } + } + return false; +} + +bool SocketIOclient::send(socketIOmessageType_t type, const uint8_t * payload, size_t length) { + return send(type, (uint8_t *)payload, length); +} + +bool SocketIOclient::send(socketIOmessageType_t type, char * payload, size_t length, bool headerToPayload) { + return send(type, (uint8_t *)payload, length, headerToPayload); +} + +bool SocketIOclient::send(socketIOmessageType_t type, const char * payload, size_t length) { + return send(type, (uint8_t *)payload, length); +} + +bool SocketIOclient::send(socketIOmessageType_t type, String & payload) { + return send(type, (uint8_t *)payload.c_str(), payload.length()); +} + +/** + * send text data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool SocketIOclient::sendEVENT(uint8_t * payload, size_t length, bool headerToPayload) { + return send(sIOtype_EVENT, payload, length, headerToPayload); +} + +bool SocketIOclient::sendEVENT(const uint8_t * payload, size_t length) { + return sendEVENT((uint8_t *)payload, length); +} + +bool SocketIOclient::sendEVENT(char * payload, size_t length, bool headerToPayload) { + return sendEVENT((uint8_t *)payload, length, headerToPayload); +} + +bool SocketIOclient::sendEVENT(const char * payload, size_t length) { + return sendEVENT((uint8_t *)payload, length); +} + +bool SocketIOclient::sendEVENT(String & payload) { + return sendEVENT((uint8_t *)payload.c_str(), payload.length()); +} + +void SocketIOclient::loop(void) { + WebSocketsClient::loop(); + unsigned long t = millis(); + if(!_disableHeartbeat && (t - _lastHeartbeat) > EIO_HEARTBEAT_INTERVAL) { + _lastHeartbeat = t; + DEBUG_WEBSOCKETS("[wsIOc] send ping\n"); + WebSocketsClient::sendTXT(eIOtype_PING); + } +} + +void SocketIOclient::handleCbEvent(WStype_t type, uint8_t * payload, size_t length) { + switch(type) { + case WStype_DISCONNECTED: + runIOCbEvent(sIOtype_DISCONNECT, NULL, 0); + DEBUG_WEBSOCKETS("[wsIOc] Disconnected!\n"); + break; + case WStype_CONNECTED: { + DEBUG_WEBSOCKETS("[wsIOc] Connected to url: %s\n", payload); + // send message to server when Connected + // Engine.io upgrade confirmation message (required) + WebSocketsClient::sendTXT("2probe"); + WebSocketsClient::sendTXT(eIOtype_UPGRADE); + runIOCbEvent(sIOtype_CONNECT, payload, length); + } break; + case WStype_TEXT: { + if(length < 1) { + break; + } + + engineIOmessageType_t eType = (engineIOmessageType_t)payload[0]; + switch(eType) { + case eIOtype_PING: + payload[0] = eIOtype_PONG; + DEBUG_WEBSOCKETS("[wsIOc] get ping send pong (%s)\n", payload); + WebSocketsClient::sendTXT(payload, length, false); + break; + case eIOtype_PONG: + DEBUG_WEBSOCKETS("[wsIOc] get pong\n"); + break; + case eIOtype_MESSAGE: { + if(length < 2) { + break; + } + socketIOmessageType_t ioType = (socketIOmessageType_t)payload[1]; + uint8_t * data = &payload[2]; + size_t lData = length - 2; + switch(ioType) { + case sIOtype_EVENT: + DEBUG_WEBSOCKETS("[wsIOc] get event (%d): %s\n", lData, data); + break; + case sIOtype_CONNECT: + DEBUG_WEBSOCKETS("[wsIOc] connected (%d): %s\n", lData, data); + return; + case sIOtype_DISCONNECT: + case sIOtype_ACK: + case sIOtype_ERROR: + case sIOtype_BINARY_EVENT: + case sIOtype_BINARY_ACK: + default: + DEBUG_WEBSOCKETS("[wsIOc] Socket.IO Message Type %c (%02X) is not implemented\n", ioType, ioType); + DEBUG_WEBSOCKETS("[wsIOc] get text: %s\n", payload); + break; + } + + runIOCbEvent(ioType, data, lData); + } break; + case eIOtype_OPEN: + case eIOtype_CLOSE: + case eIOtype_UPGRADE: + case eIOtype_NOOP: + default: + DEBUG_WEBSOCKETS("[wsIOc] Engine.IO Message Type %c (%02X) is not implemented\n", eType, eType); + DEBUG_WEBSOCKETS("[wsIOc] get text: %s\n", payload); + break; + } + } break; + case WStype_ERROR: + case WStype_BIN: + case WStype_FRAGMENT_TEXT_START: + case WStype_FRAGMENT_BIN_START: + case WStype_FRAGMENT: + case WStype_FRAGMENT_FIN: + case WStype_PING: + case WStype_PONG: + break; + } +} diff --git a/lib/WebSockets/src/SocketIOclient.h b/lib/WebSockets/src/SocketIOclient.h new file mode 100644 index 00000000..2eccea3d --- /dev/null +++ b/lib/WebSockets/src/SocketIOclient.h @@ -0,0 +1,105 @@ +/** + * SocketIOclient.h + * + * Created on: May 12, 2018 + * Author: links + */ + +#ifndef SOCKETIOCLIENT_H_ +#define SOCKETIOCLIENT_H_ + +#include "WebSockets.h" +#include "WebSocketsClient.h" + +#define EIO_HEARTBEAT_INTERVAL 20000 + +#define EIO_MAX_HEADER_SIZE (WEBSOCKETS_MAX_HEADER_SIZE + 1) +#define SIO_MAX_HEADER_SIZE (EIO_MAX_HEADER_SIZE + 1) + +typedef enum { + eIOtype_OPEN = '0', ///< Sent from the server when a new transport is opened (recheck) + eIOtype_CLOSE = '1', ///< Request the close of this transport but does not shutdown the connection itself. + eIOtype_PING = '2', ///< Sent by the client. Server should answer with a pong packet containing the same data + eIOtype_PONG = '3', ///< Sent by the server to respond to ping packets. + eIOtype_MESSAGE = '4', ///< actual message, client and server should call their callbacks with the data + eIOtype_UPGRADE = '5', ///< Before engine.io switches a transport, it tests, if server and client can communicate over this transport. If this test succeed, the client sends an upgrade packets which requests the server to flush its cache on the old transport and switch to the new transport. + eIOtype_NOOP = '6', ///< A noop packet. Used primarily to force a poll cycle when an incoming websocket connection is received. +} engineIOmessageType_t; + +typedef enum { + sIOtype_CONNECT = '0', + sIOtype_DISCONNECT = '1', + sIOtype_EVENT = '2', + sIOtype_ACK = '3', + sIOtype_ERROR = '4', + sIOtype_BINARY_EVENT = '5', + sIOtype_BINARY_ACK = '6', +} socketIOmessageType_t; + +class SocketIOclient : protected WebSocketsClient { + public: +#ifdef __AVR__ + typedef void (*SocketIOclientEvent)(socketIOmessageType_t type, uint8_t * payload, size_t length); +#else + typedef std::function SocketIOclientEvent; +#endif + + SocketIOclient(void); + virtual ~SocketIOclient(void); + + void begin(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * protocol = "arduino"); + void begin(String host, uint16_t port, String url = "/socket.io/?EIO=3", String protocol = "arduino"); + +#ifdef HAS_SSL + void beginSSL(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * protocol = "arduino"); + void beginSSL(String host, uint16_t port, String url = "/socket.io/?EIO=3", String protocol = "arduino"); +#ifndef SSL_AXTLS + void beginSSLWithCA(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * CA_cert = NULL, const char * protocol = "arduino"); + void beginSSLWithCA(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", BearSSL::X509List * CA_cert = NULL, const char * protocol = "arduino"); + void setSSLClientCertKey(const char * clientCert = NULL, const char * clientPrivateKey = NULL); + void setSSLClientCertKey(BearSSL::X509List * clientCert = NULL, BearSSL::PrivateKey * clientPrivateKey = NULL); +#endif +#endif + bool isConnected(void); + + void onEvent(SocketIOclientEvent cbEvent); + + bool sendEVENT(uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool sendEVENT(const uint8_t * payload, size_t length = 0); + bool sendEVENT(char * payload, size_t length = 0, bool headerToPayload = false); + bool sendEVENT(const char * payload, size_t length = 0); + bool sendEVENT(String & payload); + + bool send(socketIOmessageType_t type, uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool send(socketIOmessageType_t type, const uint8_t * payload, size_t length = 0); + bool send(socketIOmessageType_t type, char * payload, size_t length = 0, bool headerToPayload = false); + bool send(socketIOmessageType_t type, const char * payload, size_t length = 0); + bool send(socketIOmessageType_t type, String & payload); + + void setExtraHeaders(const char * extraHeaders = NULL); + void setReconnectInterval(unsigned long time); + + void loop(void); + + void configureEIOping(bool disableHeartbeat = false); + + protected: + bool _disableHeartbeat = false; + uint64_t _lastHeartbeat = 0; + SocketIOclientEvent _cbEvent; + virtual void runIOCbEvent(socketIOmessageType_t type, uint8_t * payload, size_t length) { + if(_cbEvent) { + _cbEvent(type, payload, length); + } + } + + void initClient(void); + + // Handeling events from websocket layer + virtual void runCbEvent(WStype_t type, uint8_t * payload, size_t length) { + handleCbEvent(type, payload, length); + } + void handleCbEvent(WStype_t type, uint8_t * payload, size_t length); +}; + +#endif /* SOCKETIOCLIENT_H_ */ diff --git a/lib/WebSockets/src/WebSockets.cpp b/lib/WebSockets/src/WebSockets.cpp new file mode 100644 index 00000000..6ab766fd --- /dev/null +++ b/lib/WebSockets/src/WebSockets.cpp @@ -0,0 +1,761 @@ +/** + * @file WebSockets.cpp + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WebSockets.h" + +#ifdef ESP8266 +#include +#endif + +extern "C" { +#ifdef CORE_HAS_LIBB64 +#include +#else +#include "libb64/cencode_inc.h" +#endif +} + +#ifdef ESP8266 +#include +#elif defined(ESP32) +#include + +#if ESP_IDF_VERSION_MAJOR >= 4 +#if(ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(1, 0, 6)) +#include "sha/sha_parallel_engine.h" +#else +#include +#endif +#else +#include +#endif + +#else + +extern "C" { +#include "libsha1/libsha1.h" +} + +#endif + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param code uint16_t see RFC + * @param reason ptr to the disconnect reason message + * @param reasonLen length of the disconnect reason message + */ +void WebSockets::clientDisconnect(WSclient_t * client, uint16_t code, char * reason, size_t reasonLen) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] clientDisconnect code: %u\n", client->num, code); + if(client->status == WSC_CONNECTED && code) { + if(reason) { + sendFrame(client, WSop_close, (uint8_t *)reason, reasonLen); + } else { + uint8_t buffer[2]; + buffer[0] = ((code >> 8) & 0xFF); + buffer[1] = (code & 0xFF); + sendFrame(client, WSop_close, &buffer[0], 2); + } + } + clientDisconnect(client); +} + +/** + * + * @param buf uint8_t * ptr to the buffer for writing + * @param opcode WSopcode_t + * @param length size_t length of the payload + * @param mask bool add dummy mask to the frame (needed for web browser) + * @param maskkey uint8_t[4] key used for payload + * @param fin bool can be used to send data in more then one frame (set fin on the last frame) + */ +uint8_t WebSockets::createHeader(uint8_t * headerPtr, WSopcode_t opcode, size_t length, bool mask, uint8_t maskKey[4], bool fin) { + uint8_t headerSize; + // calculate header Size + if(length < 126) { + headerSize = 2; + } else if(length < 0xFFFF) { + headerSize = 4; + } else { + headerSize = 10; + } + + if(mask) { + headerSize += 4; + } + + // create header + + // byte 0 + *headerPtr = 0x00; + if(fin) { + *headerPtr |= bit(7); ///< set Fin + } + *headerPtr |= opcode; ///< set opcode + headerPtr++; + + // byte 1 + *headerPtr = 0x00; + if(mask) { + *headerPtr |= bit(7); ///< set mask + } + + if(length < 126) { + *headerPtr |= length; + headerPtr++; + } else if(length < 0xFFFF) { + *headerPtr |= 126; + headerPtr++; + *headerPtr = ((length >> 8) & 0xFF); + headerPtr++; + *headerPtr = (length & 0xFF); + headerPtr++; + } else { + // Normally we never get here (to less memory) + *headerPtr |= 127; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = 0x00; + headerPtr++; + *headerPtr = ((length >> 24) & 0xFF); + headerPtr++; + *headerPtr = ((length >> 16) & 0xFF); + headerPtr++; + *headerPtr = ((length >> 8) & 0xFF); + headerPtr++; + *headerPtr = (length & 0xFF); + headerPtr++; + } + + if(mask) { + *headerPtr = maskKey[0]; + headerPtr++; + *headerPtr = maskKey[1]; + headerPtr++; + *headerPtr = maskKey[2]; + headerPtr++; + *headerPtr = maskKey[3]; + headerPtr++; + } + return headerSize; +} + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param opcode WSopcode_t + * @param length size_t length of the payload + * @param fin bool can be used to send data in more then one frame (set fin on the last frame) + * @return true if ok + */ +bool WebSockets::sendFrameHeader(WSclient_t * client, WSopcode_t opcode, size_t length, bool fin) { + uint8_t maskKey[4] = { 0x00, 0x00, 0x00, 0x00 }; + uint8_t buffer[WEBSOCKETS_MAX_HEADER_SIZE] = { 0 }; + + uint8_t headerSize = createHeader(&buffer[0], opcode, length, client->cIsClient, maskKey, fin); + + if(write(client, &buffer[0], headerSize) != headerSize) { + return false; + } + + return true; +} + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param opcode WSopcode_t + * @param payload uint8_t * ptr to the payload + * @param length size_t length of the payload + * @param fin bool can be used to send data in more then one frame (set fin on the last frame) + * @param headerToPayload bool set true if the payload has reserved 14 Byte at the beginning to dynamically add the Header (payload neet to be in RAM!) + * @return true if ok + */ +bool WebSockets::sendFrame(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin, bool headerToPayload) { + if(client->tcp && !client->tcp->connected()) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] not Connected!?\n", client->num); + return false; + } + + if(client->status != WSC_CONNECTED) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] not in WSC_CONNECTED state!?\n", client->num); + return false; + } + + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] ------- send message frame -------\n", client->num); + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] fin: %u opCode: %u mask: %u length: %u headerToPayload: %u\n", client->num, fin, opcode, client->cIsClient, length, headerToPayload); + + if(opcode == WSop_text) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] text: %s\n", client->num, (payload + (headerToPayload ? 14 : 0))); + } + + uint8_t maskKey[4] = { 0x00, 0x00, 0x00, 0x00 }; + uint8_t buffer[WEBSOCKETS_MAX_HEADER_SIZE] = { 0 }; + + uint8_t headerSize; + uint8_t * headerPtr; + uint8_t * payloadPtr = payload; + bool useInternBuffer = false; + bool ret = true; + + // calculate header Size + if(length < 126) { + headerSize = 2; + } else if(length < 0xFFFF) { + headerSize = 4; + } else { + headerSize = 10; + } + + if(client->cIsClient) { + headerSize += 4; + } + +#ifdef WEBSOCKETS_USE_BIG_MEM + // only for ESP since AVR has less HEAP + // try to send data in one TCP package (only if some free Heap is there) + if(!headerToPayload && ((length > 0) && (length < 1400)) && (GET_FREE_HEAP > 6000)) { + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] pack to one TCP package...\n", client->num); + uint8_t * dataPtr = (uint8_t *)malloc(length + WEBSOCKETS_MAX_HEADER_SIZE); + if(dataPtr) { + memcpy((dataPtr + WEBSOCKETS_MAX_HEADER_SIZE), payload, length); + headerToPayload = true; + useInternBuffer = true; + payloadPtr = dataPtr; + } + } +#endif + + // set Header Pointer + if(headerToPayload) { + // calculate offset in payload + headerPtr = (payloadPtr + (WEBSOCKETS_MAX_HEADER_SIZE - headerSize)); + } else { + headerPtr = &buffer[0]; + } + + if(client->cIsClient && useInternBuffer) { + // if we use a Intern Buffer we can modify the data + // by this fact its possible the do the masking + for(uint8_t x = 0; x < sizeof(maskKey); x++) { + maskKey[x] = random(0xFF); + } + } + + createHeader(headerPtr, opcode, length, client->cIsClient, maskKey, fin); + + if(client->cIsClient && useInternBuffer) { + uint8_t * dataMaskPtr; + + if(headerToPayload) { + dataMaskPtr = (payloadPtr + WEBSOCKETS_MAX_HEADER_SIZE); + } else { + dataMaskPtr = payloadPtr; + } + + for(size_t x = 0; x < length; x++) { + dataMaskPtr[x] = (dataMaskPtr[x] ^ maskKey[x % 4]); + } + } + +#ifndef NODEBUG_WEBSOCKETS + unsigned long start = micros(); +#endif + + if(headerToPayload) { + // header has be added to payload + // payload is forced to reserved 14 Byte but we may not need all based on the length and mask settings + // offset in payload is calculatetd 14 - headerSize + if(write(client, &payloadPtr[(WEBSOCKETS_MAX_HEADER_SIZE - headerSize)], (length + headerSize)) != (length + headerSize)) { + ret = false; + } + } else { + // send header + if(write(client, &buffer[0], headerSize) != headerSize) { + ret = false; + } + + if(payloadPtr && length > 0) { + // send payload + if(write(client, &payloadPtr[0], length) != length) { + ret = false; + } + } + } + + DEBUG_WEBSOCKETS("[WS][%d][sendFrame] sending Frame Done (%luus).\n", client->num, (micros() - start)); + +#ifdef WEBSOCKETS_USE_BIG_MEM + if(useInternBuffer && payloadPtr) { + free(payloadPtr); + } +#endif + + return ret; +} + +/** + * callen when HTTP header is done + * @param client WSclient_t * ptr to the client struct + */ +void WebSockets::headerDone(WSclient_t * client) { + client->status = WSC_CONNECTED; + client->cWsRXsize = 0; + DEBUG_WEBSOCKETS("[WS][%d][headerDone] Header Handling Done.\n", client->num); +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->cHttpLine = ""; + handleWebsocket(client); +#endif +} + +/** + * handle the WebSocket stream + * @param client WSclient_t * ptr to the client struct + */ +void WebSockets::handleWebsocket(WSclient_t * client) { + if(client->cWsRXsize == 0) { + handleWebsocketCb(client); + } +} + +/** + * wait for + * @param client + * @param size + */ +bool WebSockets::handleWebsocketWaitFor(WSclient_t * client, size_t size) { + if(!client->tcp || !client->tcp->connected()) { + return false; + } + + if(size > WEBSOCKETS_MAX_HEADER_SIZE) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor] size: %d too big!\n", client->num, size); + return false; + } + + if(client->cWsRXsize >= size) { + return true; + } + + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor] size: %d cWsRXsize: %d\n", client->num, size, client->cWsRXsize); + readCb(client, &client->cWsHeader[client->cWsRXsize], (size - client->cWsRXsize), std::bind([](WebSockets * server, size_t size, WSclient_t * client, bool ok) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocketWaitFor][readCb] size: %d ok: %d\n", client->num, size, ok); + if(ok) { + client->cWsRXsize = size; + server->handleWebsocketCb(client); + } else { + DEBUG_WEBSOCKETS("[WS][%d][readCb] failed.\n", client->num); + client->cWsRXsize = 0; + // timeout or error + server->clientDisconnect(client, 1002); + } + }, + this, size, std::placeholders::_1, std::placeholders::_2)); + return false; +} + +void WebSockets::handleWebsocketCb(WSclient_t * client) { + if(!client->tcp || !client->tcp->connected()) { + return; + } + + uint8_t * buffer = client->cWsHeader; + + WSMessageHeader_t * header = &client->cWsHeaderDecode; + uint8_t * payload = NULL; + + uint8_t headerLen = 2; + + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + + // split first 2 bytes in the data + header->fin = ((*buffer >> 7) & 0x01); + header->rsv1 = ((*buffer >> 6) & 0x01); + header->rsv2 = ((*buffer >> 5) & 0x01); + header->rsv3 = ((*buffer >> 4) & 0x01); + header->opCode = (WSopcode_t)(*buffer & 0x0F); + buffer++; + + header->mask = ((*buffer >> 7) & 0x01); + header->payloadLen = (WSopcode_t)(*buffer & 0x7F); + buffer++; + + if(header->payloadLen == 126) { + headerLen += 2; + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + header->payloadLen = buffer[0] << 8 | buffer[1]; + buffer += 2; + } else if(header->payloadLen == 127) { + headerLen += 8; + // read 64bit integer as length + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + + if(buffer[0] != 0 || buffer[1] != 0 || buffer[2] != 0 || buffer[3] != 0) { + // really too big! + header->payloadLen = 0xFFFFFFFF; + } else { + header->payloadLen = buffer[4] << 24 | buffer[5] << 16 | buffer[6] << 8 | buffer[7]; + } + buffer += 8; + } + + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] ------- read massage frame -------\n", client->num); + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] fin: %u rsv1: %u rsv2: %u rsv3 %u opCode: %u\n", client->num, header->fin, header->rsv1, header->rsv2, header->rsv3, header->opCode); + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] mask: %u payloadLen: %u\n", client->num, header->mask, header->payloadLen); + + if(header->payloadLen > WEBSOCKETS_MAX_DATA_SIZE) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] payload too big! (%u)\n", client->num, header->payloadLen); + clientDisconnect(client, 1009); + return; + } + + if(header->mask) { + headerLen += 4; + if(!handleWebsocketWaitFor(client, headerLen)) { + return; + } + header->maskKey = buffer; + buffer += 4; + } + + if(header->payloadLen > 0) { + // if text data we need one more + payload = (uint8_t *)malloc(header->payloadLen + 1); + + if(!payload) { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] to less memory to handle payload %d!\n", client->num, header->payloadLen); + clientDisconnect(client, 1011); + return; + } + readCb(client, payload, header->payloadLen, std::bind(&WebSockets::handleWebsocketPayloadCb, this, std::placeholders::_1, std::placeholders::_2, payload)); + } else { + handleWebsocketPayloadCb(client, true, NULL); + } +} + +void WebSockets::handleWebsocketPayloadCb(WSclient_t * client, bool ok, uint8_t * payload) { + WSMessageHeader_t * header = &client->cWsHeaderDecode; + if(ok) { + if(header->payloadLen > 0) { + payload[header->payloadLen] = 0x00; + + if(header->mask) { + // decode XOR + for(size_t i = 0; i < header->payloadLen; i++) { + payload[i] = (payload[i] ^ header->maskKey[i % 4]); + } + } + } + + switch(header->opCode) { + case WSop_text: + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] text: %s\n", client->num, payload); + // no break here! + case WSop_binary: + case WSop_continuation: + messageReceived(client, header->opCode, payload, header->payloadLen, header->fin); + break; + case WSop_ping: + // send pong back + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] ping received (%s)\n", client->num, payload ? (const char *)payload : ""); + sendFrame(client, WSop_pong, payload, header->payloadLen); + messageReceived(client, header->opCode, payload, header->payloadLen, header->fin); + break; + case WSop_pong: + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] get pong (%s)\n", client->num, payload ? (const char *)payload : ""); + client->pongReceived = true; + messageReceived(client, header->opCode, payload, header->payloadLen, header->fin); + break; + case WSop_close: { +#ifndef NODEBUG_WEBSOCKETS + uint16_t reasonCode = 1000; + if(header->payloadLen >= 2) { + reasonCode = payload[0] << 8 | payload[1]; + } +#endif + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] get ask for close. Code: %d\n", client->num, reasonCode); + if(header->payloadLen > 2) { + DEBUG_WEBSOCKETS(" (%s)\n", (payload + 2)); + } else { + DEBUG_WEBSOCKETS("\n"); + } + clientDisconnect(client, 1000); + } break; + default: + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] got unknown opcode: %d\n", client->num, header->opCode); + clientDisconnect(client, 1002); + break; + } + + if(payload) { + free(payload); + } + + // reset input + client->cWsRXsize = 0; +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + // register callback for next message + handleWebsocketWaitFor(client, 2); +#endif + + } else { + DEBUG_WEBSOCKETS("[WS][%d][handleWebsocket] missing data!\n", client->num); + free(payload); + clientDisconnect(client, 1002); + } +} + +/** + * generate the key for Sec-WebSocket-Accept + * @param clientKey String + * @return String Accept Key + */ +String WebSockets::acceptKey(String & clientKey) { + uint8_t sha1HashBin[20] = { 0 }; +#ifdef ESP8266 + sha1(clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", &sha1HashBin[0]); +#elif defined(ESP32) + String data = clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + esp_sha(SHA1, (unsigned char *)data.c_str(), data.length(), &sha1HashBin[0]); +#else + clientKey += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + SHA1_CTX ctx; + SHA1Init(&ctx); + SHA1Update(&ctx, (const unsigned char *)clientKey.c_str(), clientKey.length()); + SHA1Final(&sha1HashBin[0], &ctx); +#endif + + String key = base64_encode(sha1HashBin, 20); + key.trim(); + + return key; +} + +/** + * base64_encode + * @param data uint8_t * + * @param length size_t + * @return base64 encoded String + */ +String WebSockets::base64_encode(uint8_t * data, size_t length) { + size_t size = ((length * 1.6f) + 1); + char * buffer = (char *)malloc(size); + if(buffer) { + base64_encodestate _state; + base64_init_encodestate(&_state); + int len = base64_encode_block((const char *)&data[0], length, &buffer[0], &_state); + len = base64_encode_blockend((buffer + len), &_state); + + String base64 = String(buffer); + free(buffer); + return base64; + } + return String("-FAIL-"); +} + +/** + * read x byte from tcp or get timeout + * @param client WSclient_t * + * @param out uint8_t * data buffer + * @param n size_t byte count + * @return true if ok + */ +bool WebSockets::readCb(WSclient_t * client, uint8_t * out, size_t n, WSreadWaitCb cb) { +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + if(!client->tcp || !client->tcp->connected()) { + return false; + } + + client->tcp->readBytes(out, n, std::bind([](WSclient_t * client, bool ok, WSreadWaitCb cb) { + if(cb) { + cb(client, ok); + } + }, + client, std::placeholders::_1, cb)); + +#else + unsigned long t = millis(); + ssize_t len; + DEBUG_WEBSOCKETS("[readCb] n: %zu t: %lu\n", n, t); + while(n > 0) { + if(client->tcp == NULL) { + DEBUG_WEBSOCKETS("[readCb] tcp is null!\n"); + if(cb) { + cb(client, false); + } + return false; + } + + if(!client->tcp->connected()) { + DEBUG_WEBSOCKETS("[readCb] not connected!\n"); + if(cb) { + cb(client, false); + } + return false; + } + + if((millis() - t) > WEBSOCKETS_TCP_TIMEOUT) { + DEBUG_WEBSOCKETS("[readCb] receive TIMEOUT! %lu\n", (millis() - t)); + if(cb) { + cb(client, false); + } + return false; + } + + if(!client->tcp->available()) { + WEBSOCKETS_YIELD_MORE(); + continue; + } + + len = client->tcp->read((uint8_t *)out, n); + if(len > 0) { + t = millis(); + out += len; + n -= len; + // DEBUG_WEBSOCKETS("Receive %d left %d!\n", len, n); + } else { + // DEBUG_WEBSOCKETS("Receive %d left %d!\n", len, n); + } + if(n > 0) { + WEBSOCKETS_YIELD(); + } + } + if(cb) { + cb(client, true); + } + WEBSOCKETS_YIELD(); +#endif + return true; +} + +/** + * write x byte to tcp or get timeout + * @param client WSclient_t * + * @param out uint8_t * data buffer + * @param n size_t byte count + * @return bytes send + */ +size_t WebSockets::write(WSclient_t * client, uint8_t * out, size_t n) { + if(out == NULL) + return 0; + if(client == NULL) + return 0; + unsigned long t = millis(); + size_t len = 0; + size_t total = 0; + DEBUG_WEBSOCKETS("[write] n: %zu t: %lu\n", n, t); + while(n > 0) { + if(client->tcp == NULL) { + DEBUG_WEBSOCKETS("[write] tcp is null!\n"); + break; + } + + if(!client->tcp->connected()) { + DEBUG_WEBSOCKETS("[write] not connected!\n"); + break; + } + + if((millis() - t) > WEBSOCKETS_TCP_TIMEOUT) { + DEBUG_WEBSOCKETS("[write] write TIMEOUT! %lu\n", (millis() - t)); + break; + } + + len = client->tcp->write((const uint8_t *)out, n); + if(len) { + t = millis(); + out += len; + n -= len; + total += len; + // DEBUG_WEBSOCKETS("write %d left %d!\n", len, n); + } else { + DEBUG_WEBSOCKETS("WS write %d failed left %d!\n", len, n); + } + if(n > 0) { + WEBSOCKETS_YIELD(); + } + } + WEBSOCKETS_YIELD(); + return total; +} + +size_t WebSockets::write(WSclient_t * client, const char * out) { + if(client == NULL) + return 0; + if(out == NULL) + return 0; + return write(client, (uint8_t *)out, strlen(out)); +} + +/** + * enable ping/pong heartbeat process + * @param client WSclient_t * + * @param pingInterval uint32_t how often ping will be sent + * @param pongTimeout uint32_t millis after which pong should timout if not received + * @param disconnectTimeoutCount uint8_t how many timeouts before disconnect, 0=> do not disconnect + */ +void WebSockets::enableHeartbeat(WSclient_t * client, uint32_t pingInterval, uint32_t pongTimeout, uint8_t disconnectTimeoutCount) { + if(client == NULL) + return; + client->pingInterval = pingInterval; + client->pongTimeout = pongTimeout; + client->disconnectTimeoutCount = disconnectTimeoutCount; + client->pongReceived = false; +} + +/** + * handle ping/pong heartbeat timeout process + * @param client WSclient_t * + */ +void WebSockets::handleHBTimeout(WSclient_t * client) { + if(client->pingInterval) { // if heartbeat is enabled + uint32_t pi = millis() - client->lastPing; + + if(client->pongReceived) { + client->pongTimeoutCount = 0; + } else { + if(pi > client->pongTimeout) { // pong not received in time + client->pongTimeoutCount++; + client->lastPing = millis() - client->pingInterval - 500; // force ping on the next run + + DEBUG_WEBSOCKETS("[HBtimeout] pong TIMEOUT! lp=%d millis=%d pi=%d count=%d\n", client->lastPing, millis(), pi, client->pongTimeoutCount); + + if(client->disconnectTimeoutCount && client->pongTimeoutCount >= client->disconnectTimeoutCount) { + DEBUG_WEBSOCKETS("[HBtimeout] count=%d, DISCONNECTING\n", client->pongTimeoutCount); + clientDisconnect(client); + } + } + } + } +} diff --git a/lib/WebSockets/src/WebSockets.h b/lib/WebSockets/src/WebSockets.h new file mode 100644 index 00000000..3b5cb010 --- /dev/null +++ b/lib/WebSockets/src/WebSockets.h @@ -0,0 +1,367 @@ +/** + * @file WebSockets.h + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETS_H_ +#define WEBSOCKETS_H_ + +#ifdef STM32_DEVICE +#include +#define bit(b) (1UL << (b)) // Taken directly from Arduino.h +#else +#include +#include +#endif + +#ifdef ARDUINO_ARCH_AVR +#error Version 2.x.x currently does not support Arduino with AVR since there is no support for std namespace of c++. +#error Use Version 1.x.x. (ATmega branch) +#else +#include +#endif + +#include "WebSocketsVersion.h" + +#ifndef NODEBUG_WEBSOCKETS +#ifdef DEBUG_ESP_PORT +#define DEBUG_WEBSOCKETS(...) \ + { \ + DEBUG_ESP_PORT.printf(__VA_ARGS__); \ + DEBUG_ESP_PORT.flush(); \ + } +#else +//#define DEBUG_WEBSOCKETS(...) os_printf( __VA_ARGS__ ) +#endif +#endif + +#ifndef DEBUG_WEBSOCKETS +#define DEBUG_WEBSOCKETS(...) +#ifndef NODEBUG_WEBSOCKETS +#define NODEBUG_WEBSOCKETS +#endif +#endif + +#if defined(ESP8266) || defined(ESP32) + +#define WEBSOCKETS_MAX_DATA_SIZE (15 * 1024) +#define WEBSOCKETS_USE_BIG_MEM +#define GET_FREE_HEAP ESP.getFreeHeap() +// moves all Header strings to Flash (~300 Byte) +//#define WEBSOCKETS_SAVE_RAM + +#if defined(ESP8266) +#define WEBSOCKETS_YIELD() delay(0) +#define WEBSOCKETS_YIELD_MORE() delay(1) +#elif defined(ESP32) +#define WEBSOCKETS_YIELD() yield() +#define WEBSOCKETS_YIELD_MORE() delay(1) +#endif + +#elif defined(STM32_DEVICE) + +#define WEBSOCKETS_MAX_DATA_SIZE (15 * 1024) +#define WEBSOCKETS_USE_BIG_MEM +#define GET_FREE_HEAP System.freeMemory() +#define WEBSOCKETS_YIELD() +#define WEBSOCKETS_YIELD_MORE() +#else + +// atmega328p has only 2KB ram! +#define WEBSOCKETS_MAX_DATA_SIZE (1024) +// moves all Header strings to Flash +#define WEBSOCKETS_SAVE_RAM +#define WEBSOCKETS_YIELD() +#define WEBSOCKETS_YIELD_MORE() +#endif + +#define WEBSOCKETS_TCP_TIMEOUT (5000) + +#define NETWORK_ESP8266_ASYNC (0) +#define NETWORK_ESP8266 (1) +#define NETWORK_W5100 (2) +#define NETWORK_ENC28J60 (3) +#define NETWORK_ESP32 (4) +#define NETWORK_ESP32_ETH (5) + +// max size of the WS Message Header +#define WEBSOCKETS_MAX_HEADER_SIZE (14) + +#if !defined(WEBSOCKETS_NETWORK_TYPE) +// select Network type based +#if defined(ESP8266) || defined(ESP31B) +#define WEBSOCKETS_NETWORK_TYPE NETWORK_ESP8266 +//#define WEBSOCKETS_NETWORK_TYPE NETWORK_ESP8266_ASYNC +//#define WEBSOCKETS_NETWORK_TYPE NETWORK_W5100 + +#elif defined(ESP32) +#define WEBSOCKETS_NETWORK_TYPE NETWORK_ESP32 +//#define WEBSOCKETS_NETWORK_TYPE NETWORK_ESP32_ETH +#else +#define WEBSOCKETS_NETWORK_TYPE NETWORK_W5100 + +#endif +#endif + +// Includes and defined based on Network Type +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + +// Note: +// No SSL/WSS support for client in Async mode +// TLS lib need a sync interface! + +#if defined(ESP8266) +#include +#elif defined(ESP32) +#include +#include +#define SSL_AXTLS +#elif defined(ESP31B) +#include +#else +#error "network type ESP8266 ASYNC only possible on the ESP mcu!" +#endif + +#include +#include +#define WEBSOCKETS_NETWORK_CLASS AsyncTCPbuffer +#define WEBSOCKETS_NETWORK_SERVER_CLASS AsyncServer + +#elif(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + +#if !defined(ESP8266) && !defined(ESP31B) +#error "network type ESP8266 only possible on the ESP mcu!" +#endif + +#ifdef ESP8266 +#include +#if defined(wificlientbearssl_h) && !defined(USING_AXTLS) && !defined(wificlientsecure_h) +#define SSL_BARESSL +#else +#define SSL_AXTLS +#endif +#else +#include +#endif +#define WEBSOCKETS_NETWORK_CLASS WiFiClient +#define WEBSOCKETS_NETWORK_SSL_CLASS WiFiClientSecure +#define WEBSOCKETS_NETWORK_SERVER_CLASS WiFiServer + +#elif(WEBSOCKETS_NETWORK_TYPE == NETWORK_W5100) + +#ifdef STM32_DEVICE +#define WEBSOCKETS_NETWORK_CLASS TCPClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS TCPServer +#else +#include +#include +#define WEBSOCKETS_NETWORK_CLASS EthernetClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS EthernetServer +#endif + +#elif(WEBSOCKETS_NETWORK_TYPE == NETWORK_ENC28J60) + +#include +#define WEBSOCKETS_NETWORK_CLASS UIPClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS UIPServer + +#elif(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + +#include +#include +#define SSL_AXTLS +#define WEBSOCKETS_NETWORK_CLASS WiFiClient +#define WEBSOCKETS_NETWORK_SSL_CLASS WiFiClientSecure +#define WEBSOCKETS_NETWORK_SERVER_CLASS WiFiServer + +#elif(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32_ETH) + +#include +#define WEBSOCKETS_NETWORK_CLASS WiFiClient +#define WEBSOCKETS_NETWORK_SERVER_CLASS WiFiServer + +#else +#error "no network type selected!" +#endif + +#ifdef WEBSOCKETS_NETWORK_SSL_CLASS +#define HAS_SSL +#endif + +// moves all Header strings to Flash (~300 Byte) +#ifdef WEBSOCKETS_SAVE_RAM +#define WEBSOCKETS_STRING(var) F(var) +#else +#define WEBSOCKETS_STRING(var) var +#endif + +typedef enum { + WSC_NOT_CONNECTED, + WSC_HEADER, + WSC_BODY, + WSC_CONNECTED +} WSclientsStatus_t; + +typedef enum { + WStype_ERROR, + WStype_DISCONNECTED, + WStype_CONNECTED, + WStype_TEXT, + WStype_BIN, + WStype_FRAGMENT_TEXT_START, + WStype_FRAGMENT_BIN_START, + WStype_FRAGMENT, + WStype_FRAGMENT_FIN, + WStype_PING, + WStype_PONG, +} WStype_t; + +typedef enum { + WSop_continuation = 0x00, ///< %x0 denotes a continuation frame + WSop_text = 0x01, ///< %x1 denotes a text frame + WSop_binary = 0x02, ///< %x2 denotes a binary frame + ///< %x3-7 are reserved for further non-control frames + WSop_close = 0x08, ///< %x8 denotes a connection close + WSop_ping = 0x09, ///< %x9 denotes a ping + WSop_pong = 0x0A ///< %xA denotes a pong + ///< %xB-F are reserved for further control frames +} WSopcode_t; + +typedef struct { + bool fin; + bool rsv1; + bool rsv2; + bool rsv3; + + WSopcode_t opCode; + bool mask; + + size_t payloadLen; + + uint8_t * maskKey; +} WSMessageHeader_t; + +typedef struct { + void init(uint8_t num, + uint32_t pingInterval, + uint32_t pongTimeout, + uint8_t disconnectTimeoutCount) { + this->num = num; + this->pingInterval = pingInterval; + this->pongTimeout = pongTimeout; + this->disconnectTimeoutCount = disconnectTimeoutCount; + } + + uint8_t num = 0; ///< connection number + + WSclientsStatus_t status = WSC_NOT_CONNECTED; + + WEBSOCKETS_NETWORK_CLASS * tcp = nullptr; + + bool isSocketIO = false; ///< client for socket.io server + +#if defined(HAS_SSL) + bool isSSL = false; ///< run in ssl mode + WEBSOCKETS_NETWORK_SSL_CLASS * ssl; +#endif + + String cUrl; ///< http url + uint16_t cCode = 0; ///< http code + + bool cIsClient = false; ///< will be used for masking + bool cIsUpgrade = false; ///< Connection == Upgrade + bool cIsWebsocket = false; ///< Upgrade == websocket + + String cSessionId; ///< client Set-Cookie (session id) + String cKey; ///< client Sec-WebSocket-Key + String cAccept; ///< client Sec-WebSocket-Accept + String cProtocol; ///< client Sec-WebSocket-Protocol + String cExtensions; ///< client Sec-WebSocket-Extensions + uint16_t cVersion = 0; ///< client Sec-WebSocket-Version + + uint8_t cWsRXsize = 0; ///< State of the RX + uint8_t cWsHeader[WEBSOCKETS_MAX_HEADER_SIZE]; ///< RX WS Message buffer + WSMessageHeader_t cWsHeaderDecode; + + String base64Authorization; ///< Base64 encoded Auth request + String plainAuthorization; ///< Base64 encoded Auth request + + String extraHeaders; + + bool cHttpHeadersValid = false; ///< non-websocket http header validity indicator + size_t cMandatoryHeadersCount; ///< non-websocket mandatory http headers present count + + bool pongReceived = false; + uint32_t pingInterval = 0; // how often ping will be sent, 0 means "heartbeat is not active" + uint32_t lastPing = 0; // millis when last pong has been received + uint32_t pongTimeout = 0; // interval in millis after which pong is considered to timeout + uint8_t disconnectTimeoutCount = 0; // after how many subsequent pong timeouts discconnect will happen, 0 means "do not disconnect" + uint8_t pongTimeoutCount = 0; // current pong timeout count + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + String cHttpLine; ///< HTTP header lines +#endif + +} WSclient_t; + +class WebSockets { + protected: +#ifdef __AVR__ + typedef void (*WSreadWaitCb)(WSclient_t * client, bool ok); +#else + typedef std::function WSreadWaitCb; +#endif + + virtual void clientDisconnect(WSclient_t * client) = 0; + virtual bool clientIsConnected(WSclient_t * client) = 0; + + void clientDisconnect(WSclient_t * client, uint16_t code, char * reason = NULL, size_t reasonLen = 0); + + virtual void messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin) = 0; + + uint8_t createHeader(uint8_t * buf, WSopcode_t opcode, size_t length, bool mask, uint8_t maskKey[4], bool fin); + bool sendFrameHeader(WSclient_t * client, WSopcode_t opcode, size_t length = 0, bool fin = true); + bool sendFrame(WSclient_t * client, WSopcode_t opcode, uint8_t * payload = NULL, size_t length = 0, bool fin = true, bool headerToPayload = false); + + void headerDone(WSclient_t * client); + + void handleWebsocket(WSclient_t * client); + + bool handleWebsocketWaitFor(WSclient_t * client, size_t size); + void handleWebsocketCb(WSclient_t * client); + void handleWebsocketPayloadCb(WSclient_t * client, bool ok, uint8_t * payload); + + String acceptKey(String & clientKey); + String base64_encode(uint8_t * data, size_t length); + + bool readCb(WSclient_t * client, uint8_t * out, size_t n, WSreadWaitCb cb); + virtual size_t write(WSclient_t * client, uint8_t * out, size_t n); + size_t write(WSclient_t * client, const char * out); + + void enableHeartbeat(WSclient_t * client, uint32_t pingInterval, uint32_t pongTimeout, uint8_t disconnectTimeoutCount); + void handleHBTimeout(WSclient_t * client); +}; + +#ifndef UNUSED +#define UNUSED(var) (void)(var) +#endif +#endif /* WEBSOCKETS_H_ */ diff --git a/lib/WebSockets/src/WebSockets4WebServer.h b/lib/WebSockets/src/WebSockets4WebServer.h new file mode 100644 index 00000000..a542f1ea --- /dev/null +++ b/lib/WebSockets/src/WebSockets4WebServer.h @@ -0,0 +1,80 @@ +/** + * @file WebSocketsServer.cpp + * @date 28.10.2020 + * @author Markus Sattler & esp8266/arduino community + * + * Copyright (c) 2020 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef __WEBSOCKETS4WEBSERVER_H +#define __WEBSOCKETS4WEBSERVER_H + +#include +#include + +#if WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266 && WEBSERVER_HAS_HOOK + +class WebSockets4WebServer : public WebSocketsServerCore { + public: + WebSockets4WebServer(const String & origin = "", const String & protocol = "arduino") + : WebSocketsServerCore(origin, protocol) { + begin(); + } + + ESP8266WebServer::HookFunction hookForWebserver(const String & wsRootDir, WebSocketServerEvent event) { + onEvent(event); + + return [&, wsRootDir](const String & method, const String & url, WiFiClient * tcpClient, ESP8266WebServer::ContentTypeFunction contentType) { + (void)contentType; + + if(!(method == "GET" && url.indexOf(wsRootDir) == 0)) { + return ESP8266WebServer::CLIENT_REQUEST_CAN_CONTINUE; + } + + // allocate a WiFiClient copy (like in WebSocketsServer::handleNewClients()) + WEBSOCKETS_NETWORK_CLASS * newTcpClient = new WEBSOCKETS_NETWORK_CLASS(*tcpClient); + + // Then initialize a new WSclient_t (like in WebSocketsServer::handleNewClient()) + WSclient_t * client = handleNewClient(newTcpClient); + + if(client) { + // give "GET " + String headerLine; + headerLine.reserve(url.length() + 5); + headerLine = "GET "; + headerLine += url; + handleHeader(client, &headerLine); + } + + // tell webserver to not close but forget about this client + return ESP8266WebServer::CLIENT_IS_GIVEN; + }; + } +}; +#else // WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266 && WEBSERVER_HAS_HOOK + +#ifndef WEBSERVER_HAS_HOOK +#error Your current Framework / Arduino core version does not support Webserver Hook Functions +#else +#error Your Hardware Platform does not support Webserver Hook Functions +#endif + +#endif // WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266 && WEBSERVER_HAS_HOOK + +#endif // __WEBSOCKETS4WEBSERVER_H diff --git a/lib/WebSockets/src/WebSocketsClient.cpp b/lib/WebSockets/src/WebSocketsClient.cpp new file mode 100644 index 00000000..91f08d75 --- /dev/null +++ b/lib/WebSockets/src/WebSocketsClient.cpp @@ -0,0 +1,975 @@ +/** + * @file WebSocketsClient.cpp + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WebSockets.h" +#include "WebSocketsClient.h" + +WebSocketsClient::WebSocketsClient() { + _cbEvent = NULL; + _client.num = 0; + _client.cIsClient = true; + _client.extraHeaders = WEBSOCKETS_STRING("Origin: file://"); + _reconnectInterval = 500; + _port = 0; + _host = ""; +} + +WebSocketsClient::~WebSocketsClient() { + disconnect(); +} + +/** + * calles to init the Websockets server + */ +void WebSocketsClient::begin(const char * host, uint16_t port, const char * url, const char * protocol) { + _host = host; + _port = port; +#if defined(HAS_SSL) + _fingerprint = SSL_FINGERPRINT_NULL; + _CA_cert = NULL; +#endif + + _client.num = 0; + _client.status = WSC_NOT_CONNECTED; + _client.tcp = NULL; +#if defined(HAS_SSL) + _client.isSSL = false; + _client.ssl = NULL; +#endif + _client.cUrl = url; + _client.cCode = 0; + _client.cIsUpgrade = false; + _client.cIsWebsocket = true; + _client.cKey = ""; + _client.cAccept = ""; + _client.cProtocol = protocol; + _client.cExtensions = ""; + _client.cVersion = 0; + _client.base64Authorization = ""; + _client.plainAuthorization = ""; + _client.isSocketIO = false; + + _client.lastPing = 0; + _client.pongReceived = false; + _client.pongTimeoutCount = 0; + +#ifdef ESP8266 + randomSeed(RANDOM_REG32); +#else + // todo find better seed + randomSeed(millis()); +#endif +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + asyncConnect(); +#endif + + _lastConnectionFail = 0; + _lastHeaderSent = 0; + + DEBUG_WEBSOCKETS("[WS-Client] Websocket Version: " WEBSOCKETS_VERSION "\n"); +} + +void WebSocketsClient::begin(String host, uint16_t port, String url, String protocol) { + begin(host.c_str(), port, url.c_str(), protocol.c_str()); +} + +void WebSocketsClient::begin(IPAddress host, uint16_t port, const char * url, const char * protocol) { + return begin(host.toString().c_str(), port, url, protocol); +} + +#if defined(HAS_SSL) +#if defined(SSL_AXTLS) +void WebSocketsClient::beginSSL(const char * host, uint16_t port, const char * url, const char * fingerprint, const char * protocol) { + begin(host, port, url, protocol); + _client.isSSL = true; + _fingerprint = fingerprint; + _CA_cert = NULL; +} + +void WebSocketsClient::beginSSL(String host, uint16_t port, String url, String fingerprint, String protocol) { + beginSSL(host.c_str(), port, url.c_str(), fingerprint.c_str(), protocol.c_str()); +} + +void WebSocketsClient::beginSslWithCA(const char * host, uint16_t port, const char * url, const char * CA_cert, const char * protocol) { + begin(host, port, url, protocol); + _client.isSSL = true; + _fingerprint = SSL_FINGERPRINT_NULL; + _CA_cert = CA_cert; +} +#else +void WebSocketsClient::beginSSL(const char * host, uint16_t port, const char * url, const uint8_t * fingerprint, const char * protocol) { + begin(host, port, url, protocol); + _client.isSSL = true; + _fingerprint = fingerprint; + _CA_cert = NULL; +} + +void WebSocketsClient::beginSslWithCA(const char * host, uint16_t port, const char * url, BearSSL::X509List * CA_cert, const char * protocol) { + begin(host, port, url, protocol); + _client.isSSL = true; + _fingerprint = SSL_FINGERPRINT_NULL; + _CA_cert = CA_cert; +} + +void WebSocketsClient::beginSslWithCA(const char * host, uint16_t port, const char * url, const char * CA_cert, const char * protocol) { + beginSslWithCA(host, port, url, new BearSSL::X509List(CA_cert), protocol); +} + +void WebSocketsClient::setSSLClientCertKey(BearSSL::X509List * clientCert, BearSSL::PrivateKey * clientPrivateKey) { + _client_cert = clientCert; + _client_key = clientPrivateKey; +} + +void WebSocketsClient::setSSLClientCertKey(const char * clientCert, const char * clientPrivateKey) { + setSSLClientCertKey(new BearSSL::X509List(clientCert), new BearSSL::PrivateKey(clientPrivateKey)); +} + +#endif // SSL_AXTLS +#endif // HAS_SSL + +void WebSocketsClient::beginSocketIO(const char * host, uint16_t port, const char * url, const char * protocol) { + begin(host, port, url, protocol); + _client.isSocketIO = true; +} + +void WebSocketsClient::beginSocketIO(String host, uint16_t port, String url, String protocol) { + beginSocketIO(host.c_str(), port, url.c_str(), protocol.c_str()); +} + +#if defined(HAS_SSL) +void WebSocketsClient::beginSocketIOSSL(const char * host, uint16_t port, const char * url, const char * protocol) { + begin(host, port, url, protocol); + _client.isSocketIO = true; + _client.isSSL = true; + _fingerprint = SSL_FINGERPRINT_NULL; +} + +void WebSocketsClient::beginSocketIOSSL(String host, uint16_t port, String url, String protocol) { + beginSocketIOSSL(host.c_str(), port, url.c_str(), protocol.c_str()); +} + +#if defined(SSL_BARESSL) +void WebSocketsClient::beginSocketIOSSLWithCA(const char * host, uint16_t port, const char * url, BearSSL::X509List * CA_cert, const char * protocol) { + begin(host, port, url, protocol); + _client.isSocketIO = true; + _client.isSSL = true; + _fingerprint = SSL_FINGERPRINT_NULL; + _CA_cert = CA_cert; +} +#endif + +void WebSocketsClient::beginSocketIOSSLWithCA(const char * host, uint16_t port, const char * url, const char * CA_cert, const char * protocol) { + begin(host, port, url, protocol); + _client.isSocketIO = true; + _client.isSSL = true; + _fingerprint = SSL_FINGERPRINT_NULL; +#if defined(SSL_BARESSL) + _CA_cert = new BearSSL::X509List(CA_cert); +#else + _CA_cert = CA_cert; +#endif +} + +#endif + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * called in arduino loop + */ +void WebSocketsClient::loop(void) { + if(_port == 0) { + return; + } + WEBSOCKETS_YIELD(); + if(!clientIsConnected(&_client)) { + // do not flood the server + if((millis() - _lastConnectionFail) < _reconnectInterval) { + return; + } + +#if defined(HAS_SSL) + if(_client.isSSL) { + DEBUG_WEBSOCKETS("[WS-Client] connect wss...\n"); + if(_client.ssl) { + delete _client.ssl; + _client.ssl = NULL; + _client.tcp = NULL; + } + _client.ssl = new WEBSOCKETS_NETWORK_SSL_CLASS(); + _client.tcp = _client.ssl; + if(_CA_cert) { + DEBUG_WEBSOCKETS("[WS-Client] setting CA certificate"); +#if defined(ESP32) + _client.ssl->setCACert(_CA_cert); +#elif defined(ESP8266) && defined(SSL_AXTLS) + _client.ssl->setCACert((const uint8_t *)_CA_cert, strlen(_CA_cert) + 1); +#elif defined(ESP8266) && defined(SSL_BARESSL) + _client.ssl->setTrustAnchors(_CA_cert); +#else +#error setCACert not implemented +#endif +#if defined(ESP32) + } else if(!SSL_FINGERPRINT_IS_SET) { + _client.ssl->setInsecure(); +#elif defined(SSL_BARESSL) + } else if(SSL_FINGERPRINT_IS_SET) { + _client.ssl->setFingerprint(_fingerprint); + } else { + _client.ssl->setInsecure(); + } + if(_client_cert && _client_key) { + _client.ssl->setClientRSACert(_client_cert, _client_key); + DEBUG_WEBSOCKETS("[WS-Client] setting client certificate and key"); +#endif + } + } else { + DEBUG_WEBSOCKETS("[WS-Client] connect ws...\n"); + if(_client.tcp) { + delete _client.tcp; + _client.tcp = NULL; + } + _client.tcp = new WEBSOCKETS_NETWORK_CLASS(); + } +#else + _client.tcp = new WEBSOCKETS_NETWORK_CLASS(); +#endif + + if(!_client.tcp) { + DEBUG_WEBSOCKETS("[WS-Client] creating Network class failed!"); + return; + } + WEBSOCKETS_YIELD(); +#if defined(ESP32) + if(_client.tcp->connect(_host.c_str(), _port, WEBSOCKETS_TCP_TIMEOUT)) { +#else + if(_client.tcp->connect(_host.c_str(), _port)) { +#endif + connectedCb(); + _lastConnectionFail = 0; + } else { + connectFailedCb(); + _lastConnectionFail = millis(); + } + } else { + handleClientData(); + WEBSOCKETS_YIELD(); + if(_client.status == WSC_CONNECTED) { + handleHBPing(); + handleHBTimeout(&_client); + } + } +} +#endif + +/** + * set callback function + * @param cbEvent WebSocketServerEvent + */ +void WebSocketsClient::onEvent(WebSocketClientEvent cbEvent) { + _cbEvent = cbEvent; +} + +/** + * send text data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsClient::sendTXT(uint8_t * payload, size_t length, bool headerToPayload) { + if(length == 0) { + length = strlen((const char *)payload); + } + if(clientIsConnected(&_client)) { + return sendFrame(&_client, WSop_text, payload, length, true, headerToPayload); + } + return false; +} + +bool WebSocketsClient::sendTXT(const uint8_t * payload, size_t length) { + return sendTXT((uint8_t *)payload, length); +} + +bool WebSocketsClient::sendTXT(char * payload, size_t length, bool headerToPayload) { + return sendTXT((uint8_t *)payload, length, headerToPayload); +} + +bool WebSocketsClient::sendTXT(const char * payload, size_t length) { + return sendTXT((uint8_t *)payload, length); +} + +bool WebSocketsClient::sendTXT(String & payload) { + return sendTXT((uint8_t *)payload.c_str(), payload.length()); +} + +bool WebSocketsClient::sendTXT(char payload) { + uint8_t buf[WEBSOCKETS_MAX_HEADER_SIZE + 2] = { 0x00 }; + buf[WEBSOCKETS_MAX_HEADER_SIZE] = payload; + return sendTXT(buf, 1, true); +} + +/** + * send binary data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsClient::sendBIN(uint8_t * payload, size_t length, bool headerToPayload) { + if(clientIsConnected(&_client)) { + return sendFrame(&_client, WSop_binary, payload, length, true, headerToPayload); + } + return false; +} + +bool WebSocketsClient::sendBIN(const uint8_t * payload, size_t length) { + return sendBIN((uint8_t *)payload, length); +} + +/** + * sends a WS ping to Server + * @param payload uint8_t * + * @param length size_t + * @return true if ping is send out + */ +bool WebSocketsClient::sendPing(uint8_t * payload, size_t length) { + if(clientIsConnected(&_client)) { + bool sent = sendFrame(&_client, WSop_ping, payload, length); + if(sent) + _client.lastPing = millis(); + return sent; + } + return false; +} + +bool WebSocketsClient::sendPing(String & payload) { + return sendPing((uint8_t *)payload.c_str(), payload.length()); +} + +/** + * disconnect one client + * @param num uint8_t client id + */ +void WebSocketsClient::disconnect(void) { + if(clientIsConnected(&_client)) { + WebSockets::clientDisconnect(&_client, 1000); + } +} + +/** + * set the Authorizatio for the http request + * @param user const char * + * @param password const char * + */ +void WebSocketsClient::setAuthorization(const char * user, const char * password) { + if(user && password) { + String auth = user; + auth += ":"; + auth += password; + _client.base64Authorization = base64_encode((uint8_t *)auth.c_str(), auth.length()); + } +} + +/** + * set the Authorizatio for the http request + * @param auth const char * base64 + */ +void WebSocketsClient::setAuthorization(const char * auth) { + if(auth) { + //_client.base64Authorization = auth; + _client.plainAuthorization = auth; + } +} + +/** + * set extra headers for the http request; + * separate headers by "\r\n" + * @param extraHeaders const char * extraHeaders + */ +void WebSocketsClient::setExtraHeaders(const char * extraHeaders) { + _client.extraHeaders = extraHeaders; +} + +/** + * set the reconnect Interval + * how long to wait after a connection initiate failed + * @param time in ms + */ +void WebSocketsClient::setReconnectInterval(unsigned long time) { + _reconnectInterval = time; +} + +bool WebSocketsClient::isConnected(void) { + return (_client.status == WSC_CONNECTED); +} + +//################################################################################# +//################################################################################# +//################################################################################# + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param opcode WSopcode_t + * @param payload uint8_t * + * @param length size_t + */ +void WebSocketsClient::messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin) { + WStype_t type = WStype_ERROR; + + UNUSED(client); + + switch(opcode) { + case WSop_text: + type = fin ? WStype_TEXT : WStype_FRAGMENT_TEXT_START; + break; + case WSop_binary: + type = fin ? WStype_BIN : WStype_FRAGMENT_BIN_START; + break; + case WSop_continuation: + type = fin ? WStype_FRAGMENT_FIN : WStype_FRAGMENT; + break; + case WSop_ping: + type = WStype_PING; + break; + case WSop_pong: + type = WStype_PONG; + break; + case WSop_close: + default: + break; + } + + runCbEvent(type, payload, length); +} + +/** + * Disconnect an client + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsClient::clientDisconnect(WSclient_t * client) { + bool event = false; + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + if(client->isSSL && client->ssl) { + if(client->ssl->connected()) { + client->ssl->flush(); + client->ssl->stop(); + } + event = true; + delete client->ssl; + client->ssl = NULL; + client->tcp = NULL; + } +#endif + + if(client->tcp) { + if(client->tcp->connected()) { +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + client->tcp->flush(); +#endif + client->tcp->stop(); + } + event = true; +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->status = WSC_NOT_CONNECTED; +#else + delete client->tcp; +#endif + client->tcp = NULL; + } + + client->cCode = 0; + client->cKey = ""; + client->cAccept = ""; + client->cVersion = 0; + client->cIsUpgrade = false; + client->cIsWebsocket = false; + client->cSessionId = ""; + + client->status = WSC_NOT_CONNECTED; + _lastConnectionFail = millis(); + + DEBUG_WEBSOCKETS("[WS-Client] client disconnected.\n"); + if(event) { + runCbEvent(WStype_DISCONNECTED, NULL, 0); + } +} + +/** + * get client state + * @param client WSclient_t * ptr to the client struct + * @return true = conneted + */ +bool WebSocketsClient::clientIsConnected(WSclient_t * client) { + if(!client->tcp) { + return false; + } + + if(client->tcp->connected()) { + if(client->status != WSC_NOT_CONNECTED) { + return true; + } + } else { + // client lost + if(client->status != WSC_NOT_CONNECTED) { + DEBUG_WEBSOCKETS("[WS-Client] connection lost.\n"); + // do cleanup + clientDisconnect(client); + } + } + + if(client->tcp) { + // do cleanup + clientDisconnect(client); + } + + return false; +} +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * Handel incomming data from Client + */ +void WebSocketsClient::handleClientData(void) { + if((_client.status == WSC_HEADER || _client.status == WSC_BODY) && _lastHeaderSent + WEBSOCKETS_TCP_TIMEOUT < millis()) { + DEBUG_WEBSOCKETS("[WS-Client][handleClientData] header response timeout.. disconnecting!\n"); + clientDisconnect(&_client); + WEBSOCKETS_YIELD(); + return; + } + + int len = _client.tcp->available(); + if(len > 0) { + switch(_client.status) { + case WSC_HEADER: { + String headerLine = _client.tcp->readStringUntil('\n'); + handleHeader(&_client, &headerLine); + } break; + case WSC_BODY: { + char buf[256] = { 0 }; + _client.tcp->readBytes(&buf[0], std::min((size_t)len, sizeof(buf))); + String bodyLine = buf; + handleHeader(&_client, &bodyLine); + } break; + case WSC_CONNECTED: + WebSockets::handleWebsocket(&_client); + break; + default: + WebSockets::clientDisconnect(&_client, 1002); + break; + } + } + WEBSOCKETS_YIELD(); +} +#endif + +/** + * send the WebSocket header to Server + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsClient::sendHeader(WSclient_t * client) { + static const char * NEW_LINE = "\r\n"; + + DEBUG_WEBSOCKETS("[WS-Client][sendHeader] sending header...\n"); + + uint8_t randomKey[16] = { 0 }; + + for(uint8_t i = 0; i < sizeof(randomKey); i++) { + randomKey[i] = random(0xFF); + } + + client->cKey = base64_encode(&randomKey[0], 16); + +#ifndef NODEBUG_WEBSOCKETS + unsigned long start = micros(); +#endif + + String handshake; + bool ws_header = true; + String url = client->cUrl; + + if(client->isSocketIO) { + if(client->cSessionId.length() == 0) { + url += WEBSOCKETS_STRING("&transport=polling"); + ws_header = false; + } else { + url += WEBSOCKETS_STRING("&transport=websocket&sid="); + url += client->cSessionId; + } + } + + handshake = WEBSOCKETS_STRING("GET "); + handshake += url + WEBSOCKETS_STRING( + " HTTP/1.1\r\n" + "Host: "); + handshake += _host + ":" + _port + NEW_LINE; + + if(ws_header) { + handshake += WEBSOCKETS_STRING( + "Connection: Upgrade\r\n" + "Upgrade: websocket\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Sec-WebSocket-Key: "); + handshake += client->cKey + NEW_LINE; + + if(client->cProtocol.length() > 0) { + handshake += WEBSOCKETS_STRING("Sec-WebSocket-Protocol: "); + handshake += client->cProtocol + NEW_LINE; + } + + if(client->cExtensions.length() > 0) { + handshake += WEBSOCKETS_STRING("Sec-WebSocket-Extensions: "); + handshake += client->cExtensions + NEW_LINE; + } + } else { + handshake += WEBSOCKETS_STRING("Connection: keep-alive\r\n"); + } + + // add extra headers; by default this includes "Origin: file://" + if(client->extraHeaders.length() > 0) { + handshake += client->extraHeaders + NEW_LINE; + } + + handshake += WEBSOCKETS_STRING("User-Agent: arduino-WebSocket-Client\r\n"); + + if(client->base64Authorization.length() > 0) { + handshake += WEBSOCKETS_STRING("Authorization: Basic "); + handshake += client->base64Authorization + NEW_LINE; + } + + if(client->plainAuthorization.length() > 0) { + handshake += WEBSOCKETS_STRING("Authorization: "); + handshake += client->plainAuthorization + NEW_LINE; + } + + handshake += NEW_LINE; + + DEBUG_WEBSOCKETS("[WS-Client][sendHeader] handshake %s", (uint8_t *)handshake.c_str()); + write(client, (uint8_t *)handshake.c_str(), handshake.length()); + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsClient::handleHeader, this, client, &(client->cHttpLine))); +#endif + + DEBUG_WEBSOCKETS("[WS-Client][sendHeader] sending header... Done (%luus).\n", (micros() - start)); + _lastHeaderSent = millis(); +} + +/** + * handle the WebSocket header reading + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsClient::handleHeader(WSclient_t * client, String * headerLine) { + headerLine->trim(); // remove \r + + // this code handels the http body for Socket.IO V3 requests + if(headerLine->length() > 0 && client->isSocketIO && client->status == WSC_BODY && client->cSessionId.length() == 0) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] socket.io json: %s\n", headerLine->c_str()); + String sid_begin = WEBSOCKETS_STRING("\"sid\":\""); + if(headerLine->indexOf(sid_begin) > -1) { + int start = headerLine->indexOf(sid_begin) + sid_begin.length(); + int end = headerLine->indexOf('"', start); + client->cSessionId = headerLine->substring(start, end); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cSessionId: %s\n", client->cSessionId.c_str()); + + // Trigger websocket connection code path + *headerLine = ""; + } + } + + // headle HTTP header + if(headerLine->length() > 0) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] RX: %s\n", headerLine->c_str()); + + if(headerLine->startsWith(WEBSOCKETS_STRING("HTTP/1."))) { + // "HTTP/1.1 101 Switching Protocols" + client->cCode = headerLine->substring(9, headerLine->indexOf(' ', 9)).toInt(); + } else if(headerLine->indexOf(':') >= 0) { + String headerName = headerLine->substring(0, headerLine->indexOf(':')); + String headerValue = headerLine->substring(headerLine->indexOf(':') + 1); + + // remove space in the beginning (RFC2616) + if(headerValue[0] == ' ') { + headerValue.remove(0, 1); + } + + if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Connection"))) { + if(headerValue.equalsIgnoreCase(WEBSOCKETS_STRING("upgrade"))) { + client->cIsUpgrade = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Upgrade"))) { + if(headerValue.equalsIgnoreCase(WEBSOCKETS_STRING("websocket"))) { + client->cIsWebsocket = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Accept"))) { + client->cAccept = headerValue; + client->cAccept.trim(); // see rfc6455 + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Protocol"))) { + client->cProtocol = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Extensions"))) { + client->cExtensions = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Version"))) { + client->cVersion = headerValue.toInt(); + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Set-Cookie"))) { + if(headerValue.indexOf(';') > -1) { + client->cSessionId = headerValue.substring(headerValue.indexOf('=') + 1, headerValue.indexOf(";")); + } else { + client->cSessionId = headerValue.substring(headerValue.indexOf('=') + 1); + } + } + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Header error (%s)\n", headerLine->c_str()); + } + + (*headerLine) = ""; +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsClient::handleHeader, this, client, &(client->cHttpLine))); +#endif + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Header read fin.\n"); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Client settings:\n"); + + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cURL: %s\n", client->cUrl.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cKey: %s\n", client->cKey.c_str()); + + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Server header:\n"); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cCode: %d\n", client->cCode); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cIsUpgrade: %d\n", client->cIsUpgrade); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cIsWebsocket: %d\n", client->cIsWebsocket); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cAccept: %s\n", client->cAccept.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cProtocol: %s\n", client->cProtocol.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cExtensions: %s\n", client->cExtensions.c_str()); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cVersion: %d\n", client->cVersion); + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] - cSessionId: %s\n", client->cSessionId.c_str()); + + if(client->isSocketIO && client->cSessionId.length() == 0 && clientIsConnected(client)) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] still missing cSessionId try socket.io V3\n"); + client->status = WSC_BODY; + return; + } else { + client->status = WSC_HEADER; + } + + bool ok = (client->cIsUpgrade && client->cIsWebsocket); + + if(ok) { + switch(client->cCode) { + case 101: ///< Switching Protocols + + break; + case 200: + if(client->isSocketIO) { + break; + } + // falls through + case 403: ///< Forbidden + // todo handle login + // falls through + default: ///< Server dont unterstand requrst + ok = false; + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] serverCode is not 101 (%d)\n", client->cCode); + clientDisconnect(client); + _lastConnectionFail = millis(); + break; + } + } + + if(ok) { + if(client->cAccept.length() == 0) { + ok = false; + } else { + // generate Sec-WebSocket-Accept key for check + String sKey = acceptKey(client->cKey); + if(sKey != client->cAccept) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Sec-WebSocket-Accept is wrong\n"); + ok = false; + } + } + } + + if(ok) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Websocket connection init done.\n"); + headerDone(client); + + runCbEvent(WStype_CONNECTED, (uint8_t *)client->cUrl.c_str(), client->cUrl.length()); +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + } else if(client->isSocketIO) { + if(client->cSessionId.length() > 0) { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] found cSessionId\n"); + if(clientIsConnected(client) && _client.tcp->available()) { + // read not needed data + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] still data in buffer (%d), clean up.\n", _client.tcp->available()); + while(_client.tcp->available() > 0) { + _client.tcp->read(); + } + } + sendHeader(client); + } +#endif + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] no Websocket connection close.\n"); + _lastConnectionFail = millis(); + if(clientIsConnected(client)) { + write(client, "This is a webSocket client!"); + } + clientDisconnect(client); + } + } +} + +void WebSocketsClient::connectedCb() { + DEBUG_WEBSOCKETS("[WS-Client] connected to %s:%u.\n", _host.c_str(), _port); + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + _client.tcp->onDisconnect(std::bind([](WebSocketsClient * c, AsyncTCPbuffer * obj, WSclient_t * client) -> bool { + DEBUG_WEBSOCKETS("[WS-Server][%d] Disconnect client\n", client->num); + client->status = WSC_NOT_CONNECTED; + client->tcp = NULL; + + // reconnect + c->asyncConnect(); + + return true; + }, + this, std::placeholders::_1, &_client)); +#endif + + _client.status = WSC_HEADER; + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + // set Timeout for readBytesUntil and readStringUntil + _client.tcp->setTimeout(WEBSOCKETS_TCP_TIMEOUT); +#endif + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + _client.tcp->setNoDelay(true); +#endif + +#if defined(HAS_SSL) +#if defined(SSL_AXTLS) || defined(ESP32) + if(_client.isSSL && SSL_FINGERPRINT_IS_SET) { + if(!_client.ssl->verify(_fingerprint.c_str(), _host.c_str())) { + DEBUG_WEBSOCKETS("[WS-Client] certificate mismatch\n"); + WebSockets::clientDisconnect(&_client, 1000); + return; + } +#else + if(_client.isSSL && SSL_FINGERPRINT_IS_SET) { +#endif + } else if(_client.isSSL && !_CA_cert) { +#if defined(SSL_BARESSL) + _client.ssl->setInsecure(); +#endif + } +#endif + + // send Header to Server + sendHeader(&_client); +} + +void WebSocketsClient::connectFailedCb() { + DEBUG_WEBSOCKETS("[WS-Client] connection to %s:%u Failed\n", _host.c_str(), _port); +} + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + +void WebSocketsClient::asyncConnect() { + DEBUG_WEBSOCKETS("[WS-Client] asyncConnect...\n"); + + AsyncClient * tcpclient = new AsyncClient(); + + if(!tcpclient) { + DEBUG_WEBSOCKETS("[WS-Client] creating AsyncClient class failed!\n"); + return; + } + + tcpclient->onDisconnect([](void * obj, AsyncClient * c) { + c->free(); + delete c; + }); + + tcpclient->onConnect(std::bind([](WebSocketsClient * ws, AsyncClient * tcp) { + ws->_client.tcp = new AsyncTCPbuffer(tcp); + if(!ws->_client.tcp) { + DEBUG_WEBSOCKETS("[WS-Client] creating Network class failed!\n"); + ws->connectFailedCb(); + return; + } + ws->connectedCb(); + }, + this, std::placeholders::_2)); + + tcpclient->onError(std::bind([](WebSocketsClient * ws, AsyncClient * tcp) { + ws->connectFailedCb(); + + // reconnect + ws->asyncConnect(); + }, + this, std::placeholders::_2)); + + if(!tcpclient->connect(_host.c_str(), _port)) { + connectFailedCb(); + delete tcpclient; + } +} + +#endif + +/** + * send heartbeat ping to server in set intervals + */ +void WebSocketsClient::handleHBPing() { + if(_client.pingInterval == 0) + return; + uint32_t pi = millis() - _client.lastPing; + if(pi > _client.pingInterval) { + DEBUG_WEBSOCKETS("[WS-Client] sending HB ping\n"); + if(sendPing()) { + _client.lastPing = millis(); + _client.pongReceived = false; + } else { + DEBUG_WEBSOCKETS("[WS-Client] sending HB ping failed\n"); + WebSockets::clientDisconnect(&_client, 1000); + } + } +} + +/** + * enable ping/pong heartbeat process + * @param pingInterval uint32_t how often ping will be sent + * @param pongTimeout uint32_t millis after which pong should timout if not received + * @param disconnectTimeoutCount uint8_t how many timeouts before disconnect, 0=> do not disconnect + */ +void WebSocketsClient::enableHeartbeat(uint32_t pingInterval, uint32_t pongTimeout, uint8_t disconnectTimeoutCount) { + WebSockets::enableHeartbeat(&_client, pingInterval, pongTimeout, disconnectTimeoutCount); +} + +/** + * disable ping/pong heartbeat process + */ +void WebSocketsClient::disableHeartbeat() { + _client.pingInterval = 0; +} diff --git a/lib/WebSockets/src/WebSocketsClient.h b/lib/WebSockets/src/WebSocketsClient.h new file mode 100644 index 00000000..33028b98 --- /dev/null +++ b/lib/WebSockets/src/WebSocketsClient.h @@ -0,0 +1,169 @@ +/** + * @file WebSocketsClient.h + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETSCLIENT_H_ +#define WEBSOCKETSCLIENT_H_ + +#include "WebSockets.h" + +class WebSocketsClient : protected WebSockets { + public: +#ifdef __AVR__ + typedef void (*WebSocketClientEvent)(WStype_t type, uint8_t * payload, size_t length); +#else + typedef std::function WebSocketClientEvent; +#endif + + WebSocketsClient(void); + virtual ~WebSocketsClient(void); + + void begin(const char * host, uint16_t port, const char * url = "/", const char * protocol = "arduino"); + void begin(String host, uint16_t port, String url = "/", String protocol = "arduino"); + void begin(IPAddress host, uint16_t port, const char * url = "/", const char * protocol = "arduino"); + +#if defined(HAS_SSL) +#ifdef SSL_AXTLS + void beginSSL(const char * host, uint16_t port, const char * url = "/", const char * fingerprint = "", const char * protocol = "arduino"); + void beginSSL(String host, uint16_t port, String url = "/", String fingerprint = "", String protocol = "arduino"); +#else + void beginSSL(const char * host, uint16_t port, const char * url = "/", const uint8_t * fingerprint = NULL, const char * protocol = "arduino"); + void beginSslWithCA(const char * host, uint16_t port, const char * url = "/", BearSSL::X509List * CA_cert = NULL, const char * protocol = "arduino"); + void setSSLClientCertKey(BearSSL::X509List * clientCert = NULL, BearSSL::PrivateKey * clientPrivateKey = NULL); + void setSSLClientCertKey(const char * clientCert = NULL, const char * clientPrivateKey = NULL); +#endif + void beginSslWithCA(const char * host, uint16_t port, const char * url = "/", const char * CA_cert = NULL, const char * protocol = "arduino"); +#endif + + void beginSocketIO(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * protocol = "arduino"); + void beginSocketIO(String host, uint16_t port, String url = "/socket.io/?EIO=3", String protocol = "arduino"); + +#if defined(HAS_SSL) + void beginSocketIOSSL(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * protocol = "arduino"); + void beginSocketIOSSL(String host, uint16_t port, String url = "/socket.io/?EIO=3", String protocol = "arduino"); + + void beginSocketIOSSLWithCA(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", const char * CA_cert = NULL, const char * protocol = "arduino"); +#if defined(SSL_BARESSL) + void beginSocketIOSSLWithCA(const char * host, uint16_t port, const char * url = "/socket.io/?EIO=3", BearSSL::X509List * CA_cert = NULL, const char * protocol = "arduino"); +#endif +#endif + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void loop(void); +#else + // Async interface not need a loop call + void loop(void) __attribute__((deprecated)) {} +#endif + + void onEvent(WebSocketClientEvent cbEvent); + + bool sendTXT(uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(const uint8_t * payload, size_t length = 0); + bool sendTXT(char * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(const char * payload, size_t length = 0); + bool sendTXT(String & payload); + bool sendTXT(char payload); + + bool sendBIN(uint8_t * payload, size_t length, bool headerToPayload = false); + bool sendBIN(const uint8_t * payload, size_t length); + + bool sendPing(uint8_t * payload = NULL, size_t length = 0); + bool sendPing(String & payload); + + void disconnect(void); + + void setAuthorization(const char * user, const char * password); + void setAuthorization(const char * auth); + + void setExtraHeaders(const char * extraHeaders = NULL); + + void setReconnectInterval(unsigned long time); + + void enableHeartbeat(uint32_t pingInterval, uint32_t pongTimeout, uint8_t disconnectTimeoutCount); + void disableHeartbeat(); + + bool isConnected(void); + + protected: + String _host; + uint16_t _port; + +#if defined(HAS_SSL) +#ifdef SSL_AXTLS + String _fingerprint; + const char * _CA_cert; +#define SSL_FINGERPRINT_IS_SET (_fingerprint.length()) +#define SSL_FINGERPRINT_NULL "" +#else + const uint8_t * _fingerprint; + BearSSL::X509List * _CA_cert; + BearSSL::X509List * _client_cert; + BearSSL::PrivateKey * _client_key; +#define SSL_FINGERPRINT_IS_SET (_fingerprint != NULL) +#define SSL_FINGERPRINT_NULL NULL +#endif + +#endif + WSclient_t _client; + + WebSocketClientEvent _cbEvent; + + unsigned long _lastConnectionFail; + unsigned long _reconnectInterval; + unsigned long _lastHeaderSent; + + void messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin); + + void clientDisconnect(WSclient_t * client); + bool clientIsConnected(WSclient_t * client); + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void handleClientData(void); +#endif + + void sendHeader(WSclient_t * client); + void handleHeader(WSclient_t * client, String * headerLine); + + void connectedCb(); + void connectFailedCb(); + + void handleHBPing(); // send ping in specified intervals + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + void asyncConnect(); +#endif + + /** + * called for sending a Event to the app + * @param type WStype_t + * @param payload uint8_t * + * @param length size_t + */ + virtual void runCbEvent(WStype_t type, uint8_t * payload, size_t length) { + if(_cbEvent) { + _cbEvent(type, payload, length); + } + } +}; + +#endif /* WEBSOCKETSCLIENT_H_ */ diff --git a/lib/WebSockets/src/WebSocketsServer.cpp b/lib/WebSockets/src/WebSocketsServer.cpp new file mode 100644 index 00000000..ea765633 --- /dev/null +++ b/lib/WebSockets/src/WebSocketsServer.cpp @@ -0,0 +1,955 @@ +/** + * @file WebSocketsServer.cpp + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "WebSockets.h" +#include "WebSocketsServer.h" + +WebSocketsServerCore::WebSocketsServerCore(const String & origin, const String & protocol) { + _origin = origin; + _protocol = protocol; + _runnning = false; + _pingInterval = 0; + _pongTimeout = 0; + _disconnectTimeoutCount = 0; + + _cbEvent = NULL; + + _httpHeaderValidationFunc = NULL; + _mandatoryHttpHeaders = NULL; + _mandatoryHttpHeaderCount = 0; +} + +WebSocketsServer::WebSocketsServer(uint16_t port, const String & origin, const String & protocol) + : WebSocketsServerCore(origin, protocol) { + _port = port; + + _server = new WEBSOCKETS_NETWORK_SERVER_CLASS(port); + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + _server->onClient([](void * s, AsyncClient * c) { + ((WebSocketsServerCore *)s)->newClient(new AsyncTCPbuffer(c)); + }, + this); +#endif +} + +WebSocketsServerCore::~WebSocketsServerCore() { + // disconnect all clients + close(); + + if(_mandatoryHttpHeaders) + delete[] _mandatoryHttpHeaders; + + _mandatoryHttpHeaderCount = 0; +} + +WebSocketsServer::~WebSocketsServer() { +} + +/** + * called to initialize the Websocket server + */ +void WebSocketsServerCore::begin(void) { + // adjust clients storage: + // _clients[i]'s constructor are already called, + // all its members are initialized to their default value, + // except the ones explicitly detailed in WSclient_t() constructor. + // Then we need to initialize some members to non-trivial values: + for(int i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + _clients[i].init(i, _pingInterval, _pongTimeout, _disconnectTimeoutCount); + } + +#ifdef ESP8266 + randomSeed(RANDOM_REG32); +#elif defined(ESP32) +#define DR_REG_RNG_BASE 0x3ff75144 + randomSeed(READ_PERI_REG(DR_REG_RNG_BASE)); +#else + // TODO find better seed + randomSeed(millis()); +#endif + + _runnning = true; + + DEBUG_WEBSOCKETS("[WS-Server] Websocket Version: " WEBSOCKETS_VERSION "\n"); +} + +void WebSocketsServerCore::close(void) { + _runnning = false; + disconnect(); + + // restore _clients[] to their initial state + // before next call to ::begin() + for(int i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + _clients[i] = WSclient_t(); + } +} + +/** + * set callback function + * @param cbEvent WebSocketServerEvent + */ +void WebSocketsServerCore::onEvent(WebSocketServerEvent cbEvent) { + _cbEvent = cbEvent; +} + +/* + * Sets the custom http header validator function + * @param httpHeaderValidationFunc WebSocketServerHttpHeaderValFunc ///< pointer to the custom http header validation function + * @param mandatoryHttpHeaders[] const char* ///< the array of named http headers considered to be mandatory / must be present in order for websocket upgrade to succeed + * @param mandatoryHttpHeaderCount size_t ///< the number of items in the mandatoryHttpHeaders array + */ +void WebSocketsServerCore::onValidateHttpHeader( + WebSocketServerHttpHeaderValFunc validationFunc, + const char * mandatoryHttpHeaders[], + size_t mandatoryHttpHeaderCount) { + _httpHeaderValidationFunc = validationFunc; + + if(_mandatoryHttpHeaders) + delete[] _mandatoryHttpHeaders; + + _mandatoryHttpHeaderCount = mandatoryHttpHeaderCount; + _mandatoryHttpHeaders = new String[_mandatoryHttpHeaderCount]; + + for(size_t i = 0; i < _mandatoryHttpHeaderCount; i++) { + _mandatoryHttpHeaders[i] = mandatoryHttpHeaders[i]; + } +} + +/* + * send text data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServerCore::sendTXT(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return false; + } + if(length == 0) { + length = strlen((const char *)payload); + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return sendFrame(client, WSop_text, payload, length, true, headerToPayload); + } + return false; +} + +bool WebSocketsServerCore::sendTXT(uint8_t num, const uint8_t * payload, size_t length) { + return sendTXT(num, (uint8_t *)payload, length); +} + +bool WebSocketsServerCore::sendTXT(uint8_t num, char * payload, size_t length, bool headerToPayload) { + return sendTXT(num, (uint8_t *)payload, length, headerToPayload); +} + +bool WebSocketsServerCore::sendTXT(uint8_t num, const char * payload, size_t length) { + return sendTXT(num, (uint8_t *)payload, length); +} + +bool WebSocketsServerCore::sendTXT(uint8_t num, String & payload) { + return sendTXT(num, (uint8_t *)payload.c_str(), payload.length()); +} + +/** + * send text data to client all + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServerCore::broadcastTXT(uint8_t * payload, size_t length, bool headerToPayload) { + WSclient_t * client; + bool ret = true; + if(length == 0) { + length = strlen((const char *)payload); + } + + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + if(!sendFrame(client, WSop_text, payload, length, true, headerToPayload)) { + ret = false; + } + } + WEBSOCKETS_YIELD(); + } + return ret; +} + +bool WebSocketsServerCore::broadcastTXT(const uint8_t * payload, size_t length) { + return broadcastTXT((uint8_t *)payload, length); +} + +bool WebSocketsServerCore::broadcastTXT(char * payload, size_t length, bool headerToPayload) { + return broadcastTXT((uint8_t *)payload, length, headerToPayload); +} + +bool WebSocketsServerCore::broadcastTXT(const char * payload, size_t length) { + return broadcastTXT((uint8_t *)payload, length); +} + +bool WebSocketsServerCore::broadcastTXT(String & payload) { + return broadcastTXT((uint8_t *)payload.c_str(), payload.length()); +} + +/** + * send binary data to client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServerCore::sendBIN(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return false; + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return sendFrame(client, WSop_binary, payload, length, true, headerToPayload); + } + return false; +} + +bool WebSocketsServerCore::sendBIN(uint8_t num, const uint8_t * payload, size_t length) { + return sendBIN(num, (uint8_t *)payload, length); +} + +/** + * send binary data to client all + * @param payload uint8_t * + * @param length size_t + * @param headerToPayload bool (see sendFrame for more details) + * @return true if ok + */ +bool WebSocketsServerCore::broadcastBIN(uint8_t * payload, size_t length, bool headerToPayload) { + WSclient_t * client; + bool ret = true; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + if(!sendFrame(client, WSop_binary, payload, length, true, headerToPayload)) { + ret = false; + } + } + WEBSOCKETS_YIELD(); + } + return ret; +} + +bool WebSocketsServerCore::broadcastBIN(const uint8_t * payload, size_t length) { + return broadcastBIN((uint8_t *)payload, length); +} + +/** + * sends a WS ping to Client + * @param num uint8_t client id + * @param payload uint8_t * + * @param length size_t + * @return true if ping is send out + */ +bool WebSocketsServerCore::sendPing(uint8_t num, uint8_t * payload, size_t length) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return false; + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return sendFrame(client, WSop_ping, payload, length); + } + return false; +} + +bool WebSocketsServerCore::sendPing(uint8_t num, String & payload) { + return sendPing(num, (uint8_t *)payload.c_str(), payload.length()); +} + +/** + * sends a WS ping to all Client + * @param payload uint8_t * + * @param length size_t + * @return true if ping is send out + */ +bool WebSocketsServerCore::broadcastPing(uint8_t * payload, size_t length) { + WSclient_t * client; + bool ret = true; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + if(!sendFrame(client, WSop_ping, payload, length)) { + ret = false; + } + } + WEBSOCKETS_YIELD(); + } + return ret; +} + +bool WebSocketsServerCore::broadcastPing(String & payload) { + return broadcastPing((uint8_t *)payload.c_str(), payload.length()); +} + +/** + * disconnect all clients + */ +void WebSocketsServerCore::disconnect(void) { + WSclient_t * client; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + WebSockets::clientDisconnect(client, 1000); + } + } +} + +/** + * disconnect one client + * @param num uint8_t client id + */ +void WebSocketsServerCore::disconnect(uint8_t num) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return; + } + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + WebSockets::clientDisconnect(client, 1000); + } +} + +/* + * set the Authorization for the http request + * @param user const char * + * @param password const char * + */ +void WebSocketsServerCore::setAuthorization(const char * user, const char * password) { + if(user && password) { + String auth = user; + auth += ":"; + auth += password; + _base64Authorization = base64_encode((uint8_t *)auth.c_str(), auth.length()); + } +} + +/** + * set the Authorizatio for the http request + * @param auth const char * base64 + */ +void WebSocketsServerCore::setAuthorization(const char * auth) { + if(auth) { + _base64Authorization = auth; + } +} + +/** + * count the connected clients (optional ping them) + * @param ping bool ping the connected clients + */ +int WebSocketsServerCore::connectedClients(bool ping) { + WSclient_t * client; + int count = 0; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(client->status == WSC_CONNECTED) { + if(ping != true || sendPing(i)) { + count++; + } + } + } + return count; +} + +/** + * see if one client is connected + * @param num uint8_t client id + */ +bool WebSocketsServerCore::clientIsConnected(uint8_t num) { + if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { + return false; + } + WSclient_t * client = &_clients[num]; + return clientIsConnected(client); +} + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) +/** + * get an IP for a client + * @param num uint8_t client id + * @return IPAddress + */ +IPAddress WebSocketsServerCore::remoteIP(uint8_t num) { + if(num < WEBSOCKETS_SERVER_CLIENT_MAX) { + WSclient_t * client = &_clients[num]; + if(clientIsConnected(client)) { + return client->tcp->remoteIP(); + } + } + + return IPAddress(); +} +#endif + +//################################################################################# +//################################################################################# +//################################################################################# + +/** + * handle new client connection + * @param client + */ +WSclient_t * WebSocketsServerCore::newClient(WEBSOCKETS_NETWORK_CLASS * TCPclient) { + WSclient_t * client; + // search free list entry for client + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + + // state is not connected or tcp connection is lost + if(!clientIsConnected(client)) { + client->tcp = TCPclient; + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + client->isSSL = false; + client->tcp->setNoDelay(true); +#endif +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + // set Timeout for readBytesUntil and readStringUntil + client->tcp->setTimeout(WEBSOCKETS_TCP_TIMEOUT); +#endif + client->status = WSC_HEADER; +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) +#ifndef NODEBUG_WEBSOCKETS + IPAddress ip = client->tcp->remoteIP(); +#endif + DEBUG_WEBSOCKETS("[WS-Server][%d] new client from %d.%d.%d.%d\n", client->num, ip[0], ip[1], ip[2], ip[3]); +#else + DEBUG_WEBSOCKETS("[WS-Server][%d] new client\n", client->num); +#endif + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->onDisconnect(std::bind([](WebSocketsServerCore * server, AsyncTCPbuffer * obj, WSclient_t * client) -> bool { + DEBUG_WEBSOCKETS("[WS-Server][%d] Disconnect client\n", client->num); + + AsyncTCPbuffer ** sl = &server->_clients[client->num].tcp; + if(*sl == obj) { + client->status = WSC_NOT_CONNECTED; + *sl = NULL; + } + return true; + }, + this, std::placeholders::_1, client)); + + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsServerCore::handleHeader, this, client, &(client->cHttpLine))); +#endif + + client->pingInterval = _pingInterval; + client->pongTimeout = _pongTimeout; + client->disconnectTimeoutCount = _disconnectTimeoutCount; + client->lastPing = millis(); + client->pongReceived = false; + + return client; + break; + } + } + return nullptr; +} + +/** + * + * @param client WSclient_t * ptr to the client struct + * @param opcode WSopcode_t + * @param payload uint8_t * + * @param length size_t + */ +void WebSocketsServerCore::messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin) { + WStype_t type = WStype_ERROR; + + switch(opcode) { + case WSop_text: + type = fin ? WStype_TEXT : WStype_FRAGMENT_TEXT_START; + break; + case WSop_binary: + type = fin ? WStype_BIN : WStype_FRAGMENT_BIN_START; + break; + case WSop_continuation: + type = fin ? WStype_FRAGMENT_FIN : WStype_FRAGMENT; + break; + case WSop_ping: + type = WStype_PING; + break; + case WSop_pong: + type = WStype_PONG; + break; + case WSop_close: + default: + break; + } + + runCbEvent(client->num, type, payload, length); +} + +/** + * Discard a native client + * @param client WSclient_t * ptr to the client struct contaning the native client "->tcp" + */ +void WebSocketsServerCore::dropNativeClient(WSclient_t * client) { + if(!client) { + return; + } + if(client->tcp) { + if(client->tcp->connected()) { +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) && (WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP32) + client->tcp->flush(); +#endif + client->tcp->stop(); + } +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->status = WSC_NOT_CONNECTED; +#else + delete client->tcp; +#endif + client->tcp = NULL; + } +} + +/** + * Disconnect an client + * @param client WSclient_t * ptr to the client struct + */ +void WebSocketsServerCore::clientDisconnect(WSclient_t * client) { +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + if(client->isSSL && client->ssl) { + if(client->ssl->connected()) { + client->ssl->flush(); + client->ssl->stop(); + } + delete client->ssl; + client->ssl = NULL; + client->tcp = NULL; + } +#endif + + dropNativeClient(client); + + client->cUrl = ""; + client->cKey = ""; + client->cProtocol = ""; + client->cVersion = 0; + client->cIsUpgrade = false; + client->cIsWebsocket = false; + + client->cWsRXsize = 0; + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->cHttpLine = ""; +#endif + + client->status = WSC_NOT_CONNECTED; + + DEBUG_WEBSOCKETS("[WS-Server][%d] client disconnected.\n", client->num); + + runCbEvent(client->num, WStype_DISCONNECTED, NULL, 0); +} + +/** + * get client state + * @param client WSclient_t * ptr to the client struct + * @return true = connected + */ +bool WebSocketsServerCore::clientIsConnected(WSclient_t * client) { + if(!client->tcp) { + return false; + } + + if(client->tcp->connected()) { + if(client->status != WSC_NOT_CONNECTED) { + return true; + } + } else { + // client lost + if(client->status != WSC_NOT_CONNECTED) { + DEBUG_WEBSOCKETS("[WS-Server][%d] client connection lost.\n", client->num); + // do cleanup + clientDisconnect(client); + } + } + + if(client->tcp) { + // do cleanup + DEBUG_WEBSOCKETS("[WS-Server][%d] client list cleanup.\n", client->num); + clientDisconnect(client); + } + + return false; +} +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * Handle incoming Connection Request + */ +WSclient_t * WebSocketsServerCore::handleNewClient(WEBSOCKETS_NETWORK_CLASS * tcpClient) { + WSclient_t * client = newClient(tcpClient); + + if(!client) { + // no free space to handle client +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) +#ifndef NODEBUG_WEBSOCKETS + IPAddress ip = tcpClient->remoteIP(); +#endif + DEBUG_WEBSOCKETS("[WS-Server] no free space new client from %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); +#else + DEBUG_WEBSOCKETS("[WS-Server] no free space new client\n"); +#endif + // no client! => create dummy! + WSclient_t dummy = WSclient_t(); + client = &dummy; + client->tcp = tcpClient; + dropNativeClient(client); + } + + WEBSOCKETS_YIELD(); + + return client; +} + +/** + * Handle incoming Connection Request + */ +void WebSocketsServer::handleNewClients(void) { +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + while(_server->hasClient()) { +#endif + + // store new connection + WEBSOCKETS_NETWORK_CLASS * tcpClient = new WEBSOCKETS_NETWORK_CLASS(_server->available()); + if(!tcpClient) { + DEBUG_WEBSOCKETS("[WS-Client] creating Network class failed!"); + return; + } + + handleNewClient(tcpClient); + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + } +#endif +} + +/** + * Handel incomming data from Client + */ +void WebSocketsServerCore::handleClientData(void) { + WSclient_t * client; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + if(clientIsConnected(client)) { + int len = client->tcp->available(); + if(len > 0) { + // DEBUG_WEBSOCKETS("[WS-Server][%d][handleClientData] len: %d\n", client->num, len); + switch(client->status) { + case WSC_HEADER: { + String headerLine = client->tcp->readStringUntil('\n'); + handleHeader(client, &headerLine); + } break; + case WSC_CONNECTED: + WebSockets::handleWebsocket(client); + break; + default: + DEBUG_WEBSOCKETS("[WS-Server][%d][handleClientData] unknown client status %d\n", client->num, client->status); + WebSockets::clientDisconnect(client, 1002); + break; + } + } + + handleHBPing(client); + handleHBTimeout(client); + } + WEBSOCKETS_YIELD(); + } +} +#endif + +/* + * returns an indicator whether the given named header exists in the configured _mandatoryHttpHeaders collection + * @param headerName String ///< the name of the header being checked + */ +bool WebSocketsServerCore::hasMandatoryHeader(String headerName) { + for(size_t i = 0; i < _mandatoryHttpHeaderCount; i++) { + if(_mandatoryHttpHeaders[i].equalsIgnoreCase(headerName)) + return true; + } + return false; +} + +/** + * handles http header reading for WebSocket upgrade + * @param client WSclient_t * ///< pointer to the client struct + * @param headerLine String ///< the header being read / processed + */ +void WebSocketsServerCore::handleHeader(WSclient_t * client, String * headerLine) { + static const char * NEW_LINE = "\r\n"; + + headerLine->trim(); // remove \r + + if(headerLine->length() > 0) { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] RX: %s\n", client->num, headerLine->c_str()); + + // websocket requests always start with GET see rfc6455 + if(headerLine->startsWith("GET ")) { + // cut URL out + client->cUrl = headerLine->substring(4, headerLine->indexOf(' ', 4)); + + // reset non-websocket http header validation state for this client + client->cHttpHeadersValid = true; + client->cMandatoryHeadersCount = 0; + + } else if(headerLine->indexOf(':') >= 0) { + String headerName = headerLine->substring(0, headerLine->indexOf(':')); + String headerValue = headerLine->substring(headerLine->indexOf(':') + 1); + + // remove space in the beginning (RFC2616) + if(headerValue[0] == ' ') { + headerValue.remove(0, 1); + } + + if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Connection"))) { + headerValue.toLowerCase(); + if(headerValue.indexOf(WEBSOCKETS_STRING("upgrade")) >= 0) { + client->cIsUpgrade = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Upgrade"))) { + if(headerValue.equalsIgnoreCase(WEBSOCKETS_STRING("websocket"))) { + client->cIsWebsocket = true; + } + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Version"))) { + client->cVersion = headerValue.toInt(); + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Key"))) { + client->cKey = headerValue; + client->cKey.trim(); // see rfc6455 + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Protocol"))) { + client->cProtocol = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Sec-WebSocket-Extensions"))) { + client->cExtensions = headerValue; + } else if(headerName.equalsIgnoreCase(WEBSOCKETS_STRING("Authorization"))) { + client->base64Authorization = headerValue; + } else { + client->cHttpHeadersValid &= execHttpHeaderValidation(headerName, headerValue); + if(_mandatoryHttpHeaderCount > 0 && hasMandatoryHeader(headerName)) { + client->cMandatoryHeadersCount++; + } + } + + } else { + DEBUG_WEBSOCKETS("[WS-Client][handleHeader] Header error (%s)\n", headerLine->c_str()); + } + + (*headerLine) = ""; +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + client->tcp->readStringUntil('\n', &(client->cHttpLine), std::bind(&WebSocketsServerCore::handleHeader, this, client, &(client->cHttpLine))); +#endif + } else { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] Header read fin.\n", client->num); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cURL: %s\n", client->num, client->cUrl.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cIsUpgrade: %d\n", client->num, client->cIsUpgrade); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cIsWebsocket: %d\n", client->num, client->cIsWebsocket); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cKey: %s\n", client->num, client->cKey.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cProtocol: %s\n", client->num, client->cProtocol.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cExtensions: %s\n", client->num, client->cExtensions.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cVersion: %d\n", client->num, client->cVersion); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - base64Authorization: %s\n", client->num, client->base64Authorization.c_str()); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cHttpHeadersValid: %d\n", client->num, client->cHttpHeadersValid); + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - cMandatoryHeadersCount: %d\n", client->num, client->cMandatoryHeadersCount); + + bool ok = (client->cIsUpgrade && client->cIsWebsocket); + + if(ok) { + if(client->cUrl.length() == 0) { + ok = false; + } + if(client->cKey.length() == 0) { + ok = false; + } + if(client->cVersion != 13) { + ok = false; + } + if(!client->cHttpHeadersValid) { + ok = false; + } + if(client->cMandatoryHeadersCount != _mandatoryHttpHeaderCount) { + ok = false; + } + } + + if(_base64Authorization.length() > 0) { + String auth = WEBSOCKETS_STRING("Basic "); + auth += _base64Authorization; + if(auth != client->base64Authorization) { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] HTTP Authorization failed!\n", client->num); + handleAuthorizationFailed(client); + return; + } + } + + if(ok) { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] Websocket connection incoming.\n", client->num); + + // generate Sec-WebSocket-Accept key + String sKey = acceptKey(client->cKey); + + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] - sKey: %s\n", client->num, sKey.c_str()); + + client->status = WSC_CONNECTED; + + String handshake = WEBSOCKETS_STRING( + "HTTP/1.1 101 Switching Protocols\r\n" + "Server: arduino-WebSocketsServer\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Version: 13\r\n" + "Sec-WebSocket-Accept: "); + handshake += sKey + NEW_LINE; + + if(_origin.length() > 0) { + handshake += WEBSOCKETS_STRING("Access-Control-Allow-Origin: "); + handshake += _origin + NEW_LINE; + } + + if(client->cProtocol.length() > 0) { + handshake += WEBSOCKETS_STRING("Sec-WebSocket-Protocol: "); + handshake += _protocol + NEW_LINE; + } + + // header end + handshake += NEW_LINE; + + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] handshake %s", client->num, (uint8_t *)handshake.c_str()); + + write(client, (uint8_t *)handshake.c_str(), handshake.length()); + + headerDone(client); + + // send ping + WebSockets::sendFrame(client, WSop_ping); + + runCbEvent(client->num, WStype_CONNECTED, (uint8_t *)client->cUrl.c_str(), client->cUrl.length()); + + } else { + handleNonWebsocketConnection(client); + } + } +} + +/** + * send heartbeat ping to server in set intervals + */ +void WebSocketsServerCore::handleHBPing(WSclient_t * client) { + if(client->pingInterval == 0) + return; + uint32_t pi = millis() - client->lastPing; + if(pi > client->pingInterval) { + DEBUG_WEBSOCKETS("[WS-Server][%d] sending HB ping\n", client->num); + if(sendPing(client->num)) { + client->lastPing = millis(); + client->pongReceived = false; + } + } +} + +/** + * enable ping/pong heartbeat process + * @param pingInterval uint32_t how often ping will be sent + * @param pongTimeout uint32_t millis after which pong should timout if not received + * @param disconnectTimeoutCount uint8_t how many timeouts before disconnect, 0=> do not disconnect + */ +void WebSocketsServerCore::enableHeartbeat(uint32_t pingInterval, uint32_t pongTimeout, uint8_t disconnectTimeoutCount) { + _pingInterval = pingInterval; + _pongTimeout = pongTimeout; + _disconnectTimeoutCount = disconnectTimeoutCount; + + WSclient_t * client; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + WebSockets::enableHeartbeat(client, pingInterval, pongTimeout, disconnectTimeoutCount); + } +} + +/** + * disable ping/pong heartbeat process + */ +void WebSocketsServerCore::disableHeartbeat() { + _pingInterval = 0; + + WSclient_t * client; + for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { + client = &_clients[i]; + client->pingInterval = 0; + } +} + +//////////////////// +// WebSocketServer + +/** + * called to initialize the Websocket server + */ +void WebSocketsServer::begin(void) { + WebSocketsServerCore::begin(); + _server->begin(); + + DEBUG_WEBSOCKETS("[WS-Server] Server Started.\n"); +} + +void WebSocketsServer::close(void) { + WebSocketsServerCore::close(); +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) + _server->close(); +#elif(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) + _server->end(); +#else + // TODO how to close server? +#endif +} + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) +/** + * called in arduino loop + */ +void WebSocketsServerCore::loop(void) { + if(_runnning) { + WEBSOCKETS_YIELD(); + handleClientData(); + } +} + +/** + * called in arduino loop + */ +void WebSocketsServer::loop(void) { + if(_runnning) { + WEBSOCKETS_YIELD(); + handleNewClients(); + WebSocketsServerCore::loop(); + } +} +#endif diff --git a/lib/WebSockets/src/WebSocketsServer.h b/lib/WebSockets/src/WebSocketsServer.h new file mode 100644 index 00000000..28ef17c6 --- /dev/null +++ b/lib/WebSockets/src/WebSocketsServer.h @@ -0,0 +1,243 @@ +/** + * @file WebSocketsServer.h + * @date 20.05.2015 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETSSERVER_H_ +#define WEBSOCKETSSERVER_H_ + +#include "WebSockets.h" + +#ifndef WEBSOCKETS_SERVER_CLIENT_MAX +#define WEBSOCKETS_SERVER_CLIENT_MAX (5) +#endif + +class WebSocketsServerCore : protected WebSockets { + public: + WebSocketsServerCore(const String & origin = "", const String & protocol = "arduino"); + virtual ~WebSocketsServerCore(void); + + void begin(void); + void close(void); + +#ifdef __AVR__ + typedef void (*WebSocketServerEvent)(uint8_t num, WStype_t type, uint8_t * payload, size_t length); + typedef bool (*WebSocketServerHttpHeaderValFunc)(String headerName, String headerValue); +#else + typedef std::function WebSocketServerEvent; + typedef std::function WebSocketServerHttpHeaderValFunc; +#endif + + void onEvent(WebSocketServerEvent cbEvent); + void onValidateHttpHeader( + WebSocketServerHttpHeaderValFunc validationFunc, + const char * mandatoryHttpHeaders[], + size_t mandatoryHttpHeaderCount); + + bool sendTXT(uint8_t num, uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(uint8_t num, const uint8_t * payload, size_t length = 0); + bool sendTXT(uint8_t num, char * payload, size_t length = 0, bool headerToPayload = false); + bool sendTXT(uint8_t num, const char * payload, size_t length = 0); + bool sendTXT(uint8_t num, String & payload); + + bool broadcastTXT(uint8_t * payload, size_t length = 0, bool headerToPayload = false); + bool broadcastTXT(const uint8_t * payload, size_t length = 0); + bool broadcastTXT(char * payload, size_t length = 0, bool headerToPayload = false); + bool broadcastTXT(const char * payload, size_t length = 0); + bool broadcastTXT(String & payload); + + bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload = false); + bool sendBIN(uint8_t num, const uint8_t * payload, size_t length); + + bool broadcastBIN(uint8_t * payload, size_t length, bool headerToPayload = false); + bool broadcastBIN(const uint8_t * payload, size_t length); + + bool sendPing(uint8_t num, uint8_t * payload = NULL, size_t length = 0); + bool sendPing(uint8_t num, String & payload); + + bool broadcastPing(uint8_t * payload = NULL, size_t length = 0); + bool broadcastPing(String & payload); + + void disconnect(void); + void disconnect(uint8_t num); + + void setAuthorization(const char * user, const char * password); + void setAuthorization(const char * auth); + + int connectedClients(bool ping = false); + + bool clientIsConnected(uint8_t num); + + void enableHeartbeat(uint32_t pingInterval, uint32_t pongTimeout, uint8_t disconnectTimeoutCount); + void disableHeartbeat(); + +#if(WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP8266_ASYNC) || (WEBSOCKETS_NETWORK_TYPE == NETWORK_ESP32) + IPAddress remoteIP(uint8_t num); +#endif + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void loop(void); // handle client data only +#endif + + WSclient_t * newClient(WEBSOCKETS_NETWORK_CLASS * TCPclient); + + protected: + String _origin; + String _protocol; + String _base64Authorization; ///< Base64 encoded Auth request + String * _mandatoryHttpHeaders; + size_t _mandatoryHttpHeaderCount; + + WSclient_t _clients[WEBSOCKETS_SERVER_CLIENT_MAX]; + + WebSocketServerEvent _cbEvent; + WebSocketServerHttpHeaderValFunc _httpHeaderValidationFunc; + + bool _runnning; + + uint32_t _pingInterval; + uint32_t _pongTimeout; + uint8_t _disconnectTimeoutCount; + + void messageReceived(WSclient_t * client, WSopcode_t opcode, uint8_t * payload, size_t length, bool fin); + + void clientDisconnect(WSclient_t * client); + bool clientIsConnected(WSclient_t * client); + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void handleClientData(void); +#endif + + void handleHeader(WSclient_t * client, String * headerLine); + + void handleHBPing(WSclient_t * client); // send ping in specified intervals + + /** + * called if a non Websocket connection is coming in. + * Note: can be override + * @param client WSclient_t * ptr to the client struct + */ + virtual void handleNonWebsocketConnection(WSclient_t * client) { + DEBUG_WEBSOCKETS("[WS-Server][%d][handleHeader] no Websocket connection close.\n", client->num); + client->tcp->write( + "HTTP/1.1 400 Bad Request\r\n" + "Server: arduino-WebSocket-Server\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 32\r\n" + "Connection: close\r\n" + "Sec-WebSocket-Version: 13\r\n" + "\r\n" + "This is a Websocket server only!"); + clientDisconnect(client); + } + + /** + * called if a non Authorization connection is coming in. + * Note: can be override + * @param client WSclient_t * ptr to the client struct + */ + virtual void handleAuthorizationFailed(WSclient_t * client) { + client->tcp->write( + "HTTP/1.1 401 Unauthorized\r\n" + "Server: arduino-WebSocket-Server\r\n" + "Content-Type: text/plain\r\n" + "Content-Length: 45\r\n" + "Connection: close\r\n" + "Sec-WebSocket-Version: 13\r\n" + "WWW-Authenticate: Basic realm=\"WebSocket Server\"" + "\r\n" + "This Websocket server requires Authorization!"); + clientDisconnect(client); + } + + /** + * called for sending a Event to the app + * @param num uint8_t + * @param type WStype_t + * @param payload uint8_t * + * @param length size_t + */ + virtual void runCbEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { + if(_cbEvent) { + _cbEvent(num, type, payload, length); + } + } + + /* + * Called at client socket connect handshake negotiation time for each http header that is not + * a websocket specific http header (not Connection, Upgrade, Sec-WebSocket-*) + * If the custom httpHeaderValidationFunc returns false for any headerName / headerValue passed, the + * socket negotiation is considered invalid and the upgrade to websockets request is denied / rejected + * This mechanism can be used to enable custom authentication schemes e.g. test the value + * of a session cookie to determine if a user is logged on / authenticated + */ + virtual bool execHttpHeaderValidation(String headerName, String headerValue) { + if(_httpHeaderValidationFunc) { + // return the value of the custom http header validation function + return _httpHeaderValidationFunc(headerName, headerValue); + } + // no custom http header validation so just assume all is good + return true; + } + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + WSclient_t * handleNewClient(WEBSOCKETS_NETWORK_CLASS * tcpClient); +#endif + + /** + * drop native tcp connection (client->tcp) + */ + void dropNativeClient(WSclient_t * client); + + private: + /* + * returns an indicator whether the given named header exists in the configured _mandatoryHttpHeaders collection + * @param headerName String ///< the name of the header being checked + */ + bool hasMandatoryHeader(String headerName); +}; + +class WebSocketsServer : public WebSocketsServerCore { + public: + WebSocketsServer(uint16_t port, const String & origin = "", const String & protocol = "arduino"); + virtual ~WebSocketsServer(void); + + void begin(void); + void close(void); + +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void loop(void); // handle incoming client and client data +#else + // Async interface not need a loop call + void loop(void) __attribute__((deprecated)) {} +#endif + + protected: +#if(WEBSOCKETS_NETWORK_TYPE != NETWORK_ESP8266_ASYNC) + void handleNewClients(void); +#endif + + uint16_t _port; + WEBSOCKETS_NETWORK_SERVER_CLASS * _server; +}; + +#endif /* WEBSOCKETSSERVER_H_ */ diff --git a/lib/WebSockets/src/WebSocketsVersion.h b/lib/WebSockets/src/WebSocketsVersion.h new file mode 100644 index 00000000..bf2526c1 --- /dev/null +++ b/lib/WebSockets/src/WebSocketsVersion.h @@ -0,0 +1,36 @@ +/** + * @file WebSocketsVersion.h + * @date 05.04.2022 + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETSVERSION_H_ +#define WEBSOCKETSVERSION_H_ + +#define WEBSOCKETS_VERSION "2.3.7" + +#define WEBSOCKETS_VERSION_MAJOR 2 +#define WEBSOCKETS_VERSION_MINOR 3 +#define WEBSOCKETS_VERSION_PATCH 7 + +#define WEBSOCKETS_VERSION_INT 2003007 + +#endif /* WEBSOCKETSVERSION_H_ */ diff --git a/lib/WebSockets/src/libb64/AUTHORS b/lib/WebSockets/src/libb64/AUTHORS new file mode 100644 index 00000000..af687375 --- /dev/null +++ b/lib/WebSockets/src/libb64/AUTHORS @@ -0,0 +1,7 @@ +libb64: Base64 Encoding/Decoding Routines +====================================== + +Authors: +------- + +Chris Venter chris.venter@gmail.com http://rocketpod.blogspot.com diff --git a/lib/WebSockets/src/libb64/LICENSE b/lib/WebSockets/src/libb64/LICENSE new file mode 100644 index 00000000..a6b56069 --- /dev/null +++ b/lib/WebSockets/src/libb64/LICENSE @@ -0,0 +1,29 @@ +Copyright-Only Dedication (based on United States law) +or Public Domain Certification + +The person or persons who have associated work with this document (the +"Dedicator" or "Certifier") hereby either (a) certifies that, to the best of +his knowledge, the work of authorship identified is in the public domain of the +country from which the work is published, or (b) hereby dedicates whatever +copyright the dedicators holds in the work of authorship identified below (the +"Work") to the public domain. A certifier, moreover, dedicates any copyright +interest he may have in the associated work, and for these purposes, is +described as a "dedicator" below. + +A certifier has taken reasonable steps to verify the copyright status of this +work. Certifier recognizes that his good faith efforts may not shield him from +liability if in fact the work certified is not in the public domain. + +Dedicator makes this dedication for the benefit of the public at large and to +the detriment of the Dedicator's heirs and successors. Dedicator intends this +dedication to be an overt act of relinquishment in perpetuity of all present +and future rights under copyright law, whether vested or contingent, in the +Work. Dedicator understands that such relinquishment of all rights includes +the relinquishment of all rights to enforce (by lawsuit or otherwise) those +copyrights in the Work. + +Dedicator recognizes that, once placed in the public domain, the Work may be +freely reproduced, distributed, transmitted, used, modified, built upon, or +otherwise exploited by anyone for any purpose, commercial or non-commercial, +and in any way, including by methods that have not yet been invented or +conceived. \ No newline at end of file diff --git a/lib/WebSockets/src/libb64/cdecode.c b/lib/WebSockets/src/libb64/cdecode.c new file mode 100644 index 00000000..e135da24 --- /dev/null +++ b/lib/WebSockets/src/libb64/cdecode.c @@ -0,0 +1,98 @@ +/* +cdecoder.c - c source to a base64 decoding algorithm implementation + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifdef ESP8266 +#include +#endif + +#if defined(ESP32) +#define CORE_HAS_LIBB64 +#endif + +#ifndef CORE_HAS_LIBB64 +#include "cdecode_inc.h" + +int base64_decode_value(char value_in) +{ + static const char decoding[] = {62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-2,-1,-1,-1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51}; + static const char decoding_size = sizeof(decoding); + value_in -= 43; + if (value_in < 0 || value_in > decoding_size) return -1; + return decoding[(int)value_in]; +} + +void base64_init_decodestate(base64_decodestate* state_in) +{ + state_in->step = step_a; + state_in->plainchar = 0; +} + +int base64_decode_block(const char* code_in, const int length_in, char* plaintext_out, base64_decodestate* state_in) +{ + const char* codechar = code_in; + char* plainchar = plaintext_out; + char fragment; + + *plainchar = state_in->plainchar; + + switch (state_in->step) + { + while (1) + { + case step_a: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_a; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar = (fragment & 0x03f) << 2; + case step_b: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_b; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar++ |= (fragment & 0x030) >> 4; + *plainchar = (fragment & 0x00f) << 4; + case step_c: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_c; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar++ |= (fragment & 0x03c) >> 2; + *plainchar = (fragment & 0x003) << 6; + case step_d: + do { + if (codechar == code_in+length_in) + { + state_in->step = step_d; + state_in->plainchar = *plainchar; + return plainchar - plaintext_out; + } + fragment = (char)base64_decode_value(*codechar++); + } while (fragment < 0); + *plainchar++ |= (fragment & 0x03f); + } + } + /* control should not reach here */ + return plainchar - plaintext_out; +} + +#endif diff --git a/lib/WebSockets/src/libb64/cdecode_inc.h b/lib/WebSockets/src/libb64/cdecode_inc.h new file mode 100644 index 00000000..d0d7f489 --- /dev/null +++ b/lib/WebSockets/src/libb64/cdecode_inc.h @@ -0,0 +1,28 @@ +/* +cdecode.h - c header for a base64 decoding algorithm + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifndef BASE64_CDECODE_H +#define BASE64_CDECODE_H + +typedef enum +{ + step_a, step_b, step_c, step_d +} base64_decodestep; + +typedef struct +{ + base64_decodestep step; + char plainchar; +} base64_decodestate; + +void base64_init_decodestate(base64_decodestate* state_in); + +int base64_decode_value(char value_in); + +int base64_decode_block(const char* code_in, const int length_in, char* plaintext_out, base64_decodestate* state_in); + +#endif /* BASE64_CDECODE_H */ diff --git a/lib/WebSockets/src/libb64/cencode.c b/lib/WebSockets/src/libb64/cencode.c new file mode 100644 index 00000000..afe1463c --- /dev/null +++ b/lib/WebSockets/src/libb64/cencode.c @@ -0,0 +1,119 @@ +/* +cencoder.c - c source to a base64 encoding algorithm implementation + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifdef ESP8266 +#include +#endif + +#if defined(ESP32) +#define CORE_HAS_LIBB64 +#endif + +#ifndef CORE_HAS_LIBB64 +#include "cencode_inc.h" + +const int CHARS_PER_LINE = 72; + +void base64_init_encodestate(base64_encodestate* state_in) +{ + state_in->step = step_A; + state_in->result = 0; + state_in->stepcount = 0; +} + +char base64_encode_value(char value_in) +{ + static const char* encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + if (value_in > 63) return '='; + return encoding[(int)value_in]; +} + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in) +{ + const char* plainchar = plaintext_in; + const char* const plaintextend = plaintext_in + length_in; + char* codechar = code_out; + char result; + char fragment; + + result = state_in->result; + + switch (state_in->step) + { + while (1) + { + case step_A: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_A; + return codechar - code_out; + } + fragment = *plainchar++; + result = (fragment & 0x0fc) >> 2; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x003) << 4; + case step_B: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_B; + return codechar - code_out; + } + fragment = *plainchar++; + result |= (fragment & 0x0f0) >> 4; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x00f) << 2; + case step_C: + if (plainchar == plaintextend) + { + state_in->result = result; + state_in->step = step_C; + return codechar - code_out; + } + fragment = *plainchar++; + result |= (fragment & 0x0c0) >> 6; + *codechar++ = base64_encode_value(result); + result = (fragment & 0x03f) >> 0; + *codechar++ = base64_encode_value(result); + + ++(state_in->stepcount); + if (state_in->stepcount == CHARS_PER_LINE/4) + { + *codechar++ = '\n'; + state_in->stepcount = 0; + } + } + } + /* control should not reach here */ + return codechar - code_out; +} + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in) +{ + char* codechar = code_out; + + switch (state_in->step) + { + case step_B: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + *codechar++ = '='; + break; + case step_C: + *codechar++ = base64_encode_value(state_in->result); + *codechar++ = '='; + break; + case step_A: + break; + } + *codechar++ = 0x00; + + return codechar - code_out; +} + +#endif diff --git a/lib/WebSockets/src/libb64/cencode_inc.h b/lib/WebSockets/src/libb64/cencode_inc.h new file mode 100644 index 00000000..c1e3464a --- /dev/null +++ b/lib/WebSockets/src/libb64/cencode_inc.h @@ -0,0 +1,31 @@ +/* +cencode.h - c header for a base64 encoding algorithm + +This is part of the libb64 project, and has been placed in the public domain. +For details, see http://sourceforge.net/projects/libb64 +*/ + +#ifndef BASE64_CENCODE_H +#define BASE64_CENCODE_H + +typedef enum +{ + step_A, step_B, step_C +} base64_encodestep; + +typedef struct +{ + base64_encodestep step; + char result; + int stepcount; +} base64_encodestate; + +void base64_init_encodestate(base64_encodestate* state_in); + +char base64_encode_value(char value_in); + +int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in); + +int base64_encode_blockend(char* code_out, base64_encodestate* state_in); + +#endif /* BASE64_CENCODE_H */ diff --git a/lib/WebSockets/src/libsha1/libsha1.c b/lib/WebSockets/src/libsha1/libsha1.c new file mode 100644 index 00000000..48f4df5a --- /dev/null +++ b/lib/WebSockets/src/libsha1/libsha1.c @@ -0,0 +1,202 @@ +/* from valgrind tests */ + +/* ================ sha1.c ================ */ +/* +SHA-1 in C +By Steve Reid +100% Public Domain + +Test Vectors (from FIPS PUB 180-1) +"abc" + A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D +"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" + 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1 +A million repetitions of "a" + 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F +*/ + +/* #define LITTLE_ENDIAN * This should be #define'd already, if true. */ +/* #define SHA1HANDSOFF * Copies data before messing with it. */ + +#if !defined(ESP8266) && !defined(ESP32) + +#define SHA1HANDSOFF + +#include +#include +#include + +#include "libsha1.h" + + +#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits)))) + +/* blk0() and blk() perform the initial expand. */ +/* I got the idea of expanding during the round function from SSLeay */ +#if BYTE_ORDER == LITTLE_ENDIAN +#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \ + |(rol(block->l[i],8)&0x00FF00FF)) +#elif BYTE_ORDER == BIG_ENDIAN +#define blk0(i) block->l[i] +#else +#error "Endianness not defined!" +#endif +#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \ + ^block->l[(i+2)&15]^block->l[i&15],1)) + +/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */ +#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30); +#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30); +#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30); +#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30); +#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30); + + +/* Hash a single 512-bit block. This is the core of the algorithm. */ + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]) +{ + uint32_t a, b, c, d, e; + typedef union { + unsigned char c[64]; + uint32_t l[16]; + } CHAR64LONG16; +#ifdef SHA1HANDSOFF + CHAR64LONG16 block[1]; /* use array to appear as a pointer */ + memcpy(block, buffer, 64); +#else + /* The following had better never be used because it causes the + * pointer-to-const buffer to be cast into a pointer to non-const. + * And the result is written through. I threw a "const" in, hoping + * this will cause a diagnostic. + */ + CHAR64LONG16* block = (const CHAR64LONG16*)buffer; +#endif + /* Copy context->state[] to working vars */ + a = state[0]; + b = state[1]; + c = state[2]; + d = state[3]; + e = state[4]; + /* 4 rounds of 20 operations each. Loop unrolled. */ + R0(a,b,c,d,e, 0); R0(e,a,b,c,d, 1); R0(d,e,a,b,c, 2); R0(c,d,e,a,b, 3); + R0(b,c,d,e,a, 4); R0(a,b,c,d,e, 5); R0(e,a,b,c,d, 6); R0(d,e,a,b,c, 7); + R0(c,d,e,a,b, 8); R0(b,c,d,e,a, 9); R0(a,b,c,d,e,10); R0(e,a,b,c,d,11); + R0(d,e,a,b,c,12); R0(c,d,e,a,b,13); R0(b,c,d,e,a,14); R0(a,b,c,d,e,15); + R1(e,a,b,c,d,16); R1(d,e,a,b,c,17); R1(c,d,e,a,b,18); R1(b,c,d,e,a,19); + R2(a,b,c,d,e,20); R2(e,a,b,c,d,21); R2(d,e,a,b,c,22); R2(c,d,e,a,b,23); + R2(b,c,d,e,a,24); R2(a,b,c,d,e,25); R2(e,a,b,c,d,26); R2(d,e,a,b,c,27); + R2(c,d,e,a,b,28); R2(b,c,d,e,a,29); R2(a,b,c,d,e,30); R2(e,a,b,c,d,31); + R2(d,e,a,b,c,32); R2(c,d,e,a,b,33); R2(b,c,d,e,a,34); R2(a,b,c,d,e,35); + R2(e,a,b,c,d,36); R2(d,e,a,b,c,37); R2(c,d,e,a,b,38); R2(b,c,d,e,a,39); + R3(a,b,c,d,e,40); R3(e,a,b,c,d,41); R3(d,e,a,b,c,42); R3(c,d,e,a,b,43); + R3(b,c,d,e,a,44); R3(a,b,c,d,e,45); R3(e,a,b,c,d,46); R3(d,e,a,b,c,47); + R3(c,d,e,a,b,48); R3(b,c,d,e,a,49); R3(a,b,c,d,e,50); R3(e,a,b,c,d,51); + R3(d,e,a,b,c,52); R3(c,d,e,a,b,53); R3(b,c,d,e,a,54); R3(a,b,c,d,e,55); + R3(e,a,b,c,d,56); R3(d,e,a,b,c,57); R3(c,d,e,a,b,58); R3(b,c,d,e,a,59); + R4(a,b,c,d,e,60); R4(e,a,b,c,d,61); R4(d,e,a,b,c,62); R4(c,d,e,a,b,63); + R4(b,c,d,e,a,64); R4(a,b,c,d,e,65); R4(e,a,b,c,d,66); R4(d,e,a,b,c,67); + R4(c,d,e,a,b,68); R4(b,c,d,e,a,69); R4(a,b,c,d,e,70); R4(e,a,b,c,d,71); + R4(d,e,a,b,c,72); R4(c,d,e,a,b,73); R4(b,c,d,e,a,74); R4(a,b,c,d,e,75); + R4(e,a,b,c,d,76); R4(d,e,a,b,c,77); R4(c,d,e,a,b,78); R4(b,c,d,e,a,79); + /* Add the working vars back into context.state[] */ + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; + /* Wipe variables */ + a = b = c = d = e = 0; +#ifdef SHA1HANDSOFF + memset(block, '\0', sizeof(block)); +#endif +} + + +/* SHA1Init - Initialize new context */ + +void SHA1Init(SHA1_CTX* context) +{ + /* SHA1 initialization constants */ + context->state[0] = 0x67452301; + context->state[1] = 0xEFCDAB89; + context->state[2] = 0x98BADCFE; + context->state[3] = 0x10325476; + context->state[4] = 0xC3D2E1F0; + context->count[0] = context->count[1] = 0; +} + + +/* Run your data through this. */ + +void SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len) +{ + uint32_t i, j; + + j = context->count[0]; + if ((context->count[0] += len << 3) < j) + context->count[1]++; + context->count[1] += (len>>29); + j = (j >> 3) & 63; + if ((j + len) > 63) { + memcpy(&context->buffer[j], data, (i = 64-j)); + SHA1Transform(context->state, context->buffer); + for ( ; i + 63 < len; i += 64) { + SHA1Transform(context->state, &data[i]); + } + j = 0; + } + else i = 0; + memcpy(&context->buffer[j], &data[i], len - i); +} + + +/* Add padding and return the message digest. */ + +void SHA1Final(unsigned char digest[20], SHA1_CTX* context) +{ + unsigned i; + unsigned char finalcount[8]; + unsigned char c; + +#if 0 /* untested "improvement" by DHR */ + /* Convert context->count to a sequence of bytes + * in finalcount. Second element first, but + * big-endian order within element. + * But we do it all backwards. + */ + unsigned char *fcp = &finalcount[8]; + + for (i = 0; i < 2; i++) + { + uint32_t t = context->count[i]; + int j; + + for (j = 0; j < 4; t >>= 8, j++) + *--fcp = (unsigned char) t; + } +#else + for (i = 0; i < 8; i++) { + finalcount[i] = (unsigned char)((context->count[(i >= 4 ? 0 : 1)] + >> ((3-(i & 3)) * 8) ) & 255); /* Endian independent */ + } +#endif + c = 0200; + SHA1Update(context, &c, 1); + while ((context->count[0] & 504) != 448) { + c = 0000; + SHA1Update(context, &c, 1); + } + SHA1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */ + for (i = 0; i < 20; i++) { + digest[i] = (unsigned char) + ((context->state[i>>2] >> ((3-(i & 3)) * 8) ) & 255); + } + /* Wipe variables */ + memset(context, '\0', sizeof(*context)); + memset(&finalcount, '\0', sizeof(finalcount)); +} +/* ================ end of sha1.c ================ */ + + +#endif diff --git a/lib/WebSockets/src/libsha1/libsha1.h b/lib/WebSockets/src/libsha1/libsha1.h new file mode 100644 index 00000000..ee3718e1 --- /dev/null +++ b/lib/WebSockets/src/libsha1/libsha1.h @@ -0,0 +1,21 @@ +/* ================ sha1.h ================ */ +/* +SHA-1 in C +By Steve Reid +100% Public Domain +*/ + +#if !defined(ESP8266) && !defined(ESP32) + +typedef struct { + uint32_t state[5]; + uint32_t count[2]; + unsigned char buffer[64]; +} SHA1_CTX; + +void SHA1Transform(uint32_t state[5], const unsigned char buffer[64]); +void SHA1Init(SHA1_CTX* context); +void SHA1Update(SHA1_CTX* context, const unsigned char* data, uint32_t len); +void SHA1Final(unsigned char digest[20], SHA1_CTX* context); + +#endif diff --git a/lib/WebSockets/travis/common.sh b/lib/WebSockets/travis/common.sh new file mode 100644 index 00000000..c1c23285 --- /dev/null +++ b/lib/WebSockets/travis/common.sh @@ -0,0 +1,134 @@ +#!/bin/bash + +set -ex + +function build_sketches() +{ + local arduino=$1 + local srcpath=$2 + local platform=$3 + local sketches=$(find $srcpath -name *.ino) + for sketch in $sketches; do + local sketchdir=$(dirname $sketch) + if [[ -f "$sketchdir/.$platform.skip" ]]; then + echo -e "\n\n ------------ Skipping $sketch ------------ \n\n"; + continue + fi + echo -e "\n\n ------------ Building $sketch ------------ \n\n"; + $arduino --verify $sketch; + local result=$? + if [ $result -ne 0 ]; then + echo "Build failed ($sketch) build verbose..." + $arduino --verify --verbose --preserve-temp-files $sketch + result=$? + fi + if [ $result -ne 0 ]; then + echo "Build failed ($1) $sketch" + return $result + fi + done +} + +function build_sketch() +{ + local arduino=$1 + local sketch=$2 + $arduino --verify $sketch; + local result=$? + if [ $result -ne 0 ]; then + echo "Build failed ($sketch) build verbose..." + $arduino --verify --verbose --preserve-temp-files $sketch + result=$? + fi + if [ $result -ne 0 ]; then + echo "Build failed ($1) $sketch" + return $result + fi +} + +function get_sketches_json() +{ + local arduino=$1 + local srcpath=$2 + local platform=$3 + local sketches=($(find $srcpath -name *.ino)) + echo -en "[" + for sketch in "${sketches[@]}" ; do + local sketchdir=$(dirname $sketch) + if [[ -f "$sketchdir/.$platform.skip" ]]; then + continue + fi + echo -en "\"$sketch\"" + if [[ $sketch != ${sketches[-1]} ]] ; then + echo -en "," + fi + + done + echo -en "]" +} + +function get_sketches_json_matrix() +{ + local arduino=$1 + local srcpath=$2 + local platform=$3 + local ideversion=$4 + local board=$5 + local sketches=($(find $srcpath -name *.ino)) + for sketch in "${sketches[@]}" ; do + local sketchdir=$(dirname $sketch) + local sketchname=$(basename $sketch) + if [[ -f "$sketchdir/.$platform.skip" ]]; then + continue + fi + echo -en "{\"name\":\"$sketchname\",\"board\":\"$board\",\"ideversion\":\"$ideversion\",\"cpu\":\"$platform\",\"sketch\":\"$sketch\"}" + if [[ $sketch != ${sketches[-1]} ]] ; then + echo -en "," + fi + done +} + +function get_core() +{ + echo Setup core for $1 + + cd $HOME/arduino_ide/hardware + + if [ "$1" = "esp8266" ] ; then + mkdir esp8266com + cd esp8266com + git clone --depth 1 https://github.com/esp8266/Arduino.git esp8266 + cd esp8266/ + git submodule update --init + rm -rf .git + cd tools + python get.py + fi + + if [ "$1" = "esp32" ] ; then + mkdir espressif + cd espressif + git clone --depth 1 https://github.com/espressif/arduino-esp32.git esp32 + cd esp32/ + rm -rf .git + cd tools + python get.py + fi + +} + +function clone_library() { + local url=$1 + echo clone $(basename $url) + mkdir -p $HOME/Arduino/libraries + cd $HOME/Arduino/libraries + git clone --depth 1 $url + rm -rf */.git + rm -rf */.github + rm -rf */examples +} + +function hash_library_names() { + cd $HOME/Arduino/libraries + ls | sha1sum -z | cut -c1-5 +} \ No newline at end of file diff --git a/lib/WebSockets/travis/version.py b/lib/WebSockets/travis/version.py new file mode 100644 index 00000000..71454abb --- /dev/null +++ b/lib/WebSockets/travis/version.py @@ -0,0 +1,132 @@ +#!/usr/bin/python3 + +import json +import configparser +import argparse +import re +import os +import datetime + +travis_dir = os.path.dirname(os.path.abspath(__file__)) +base_dir = os.path.abspath(travis_dir + "/../") + +def write_header_file(version): + hvs = version.split('.') + intversion = int(hvs[0]) * 1000000 + int(hvs[1]) * 1000 + int(hvs[2]) + now = datetime.datetime.now() + + text = f'''/** + * @file WebSocketsVersion.h + * @date {now.strftime("%d.%m.%Y")} + * @author Markus Sattler + * + * Copyright (c) 2015 Markus Sattler. All rights reserved. + * This file is part of the WebSockets for Arduino. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef WEBSOCKETSVERSION_H_ +#define WEBSOCKETSVERSION_H_ + +#define WEBSOCKETS_VERSION "{version}" + +#define WEBSOCKETS_VERSION_MAJOR {hvs[0]} +#define WEBSOCKETS_VERSION_MINOR {hvs[1]} +#define WEBSOCKETS_VERSION_PATCH {hvs[2]} + +#define WEBSOCKETS_VERSION_INT {intversion} + +#endif /* WEBSOCKETSVERSION_H_ */ +''' + with open(f'{base_dir}/src/WebSocketsVersion.h', 'w') as f: + f.write(text) + + +def get_library_properties_version(): + library_properties = {} + with open(f'{base_dir}/library.properties', 'r') as f: + library_properties = configparser.ConfigParser() + library_properties.read_string('[root]\n' + f.read()) + return library_properties['root']['version'] + + +def get_library_json_version(): + library_json = {} + with open(f'{base_dir}/library.json', 'r') as f: + library_json = json.load(f) + return library_json['version'] + + +def get_header_versions(): + data = {} + define = re.compile('^#define WEBSOCKETS_VERSION_?(.*) "?([0-9\.]*)"?$') + with open(f'{base_dir}/src/WebSocketsVersion.h', 'r') as f: + for line in f: + m = define.match(line) + if m: + name = m[1] + if name == "": + name = "VERSION" + data[name] = m[2] + return data + + +parser = argparse.ArgumentParser(description='Checks and update Version files') +parser.add_argument( + '--update', action='store_true', default=False) +parser.add_argument( + '--check', action='store_true', default=True) + +args = parser.parse_args() + +if args.update: + library_properties_version = get_library_properties_version() + + with open(f'{base_dir}/library.json', 'r') as f: + library_json = json.load(f) + + library_json['version'] = library_properties_version + + with open(f'{base_dir}/library.json', 'w') as f: + json.dump(library_json, f, indent=4, sort_keys=True) + + write_header_file(library_properties_version) + + +library_json_version = get_library_json_version() +library_properties_version = get_library_properties_version() +header_version = get_header_versions() + +print("WebSocketsVersion.h", header_version) +print(f"library.json: {library_json_version}") +print(f"library.properties: {library_properties_version}") + +if args.check: + if library_json_version != library_properties_version or header_version['VERSION'] != library_properties_version: + raise Exception('versions did not match!') + + hvs = header_version['VERSION'].split('.') + if header_version['MAJOR'] != hvs[0]: + raise Exception('header MAJOR version wrong!') + if header_version['MINOR'] != hvs[1]: + raise Exception('header MINOR version wrong!') + if header_version['PATCH'] != hvs[2]: + raise Exception('header PATCH version wrong!') + + intversion = int(hvs[0]) * 1000000 + int(hvs[1]) * 1000 + int(hvs[2]) + if int(header_version['INT']) != intversion: + raise Exception('header INT version wrong!') diff --git a/platformio.ini b/platformio.ini index b38209a2..4590a0cc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -48,7 +48,6 @@ data_dir = data_svelte upload_port = COM4 lib_deps_external = bblanchon/ArduinoJson @6.18.0 - Links2004/WebSockets knolleary/PubSubClient [env:esp8266_4mb_fromitems] diff --git a/src/Main.cpp b/src/Main.cpp index dd750d81..16448d34 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -72,7 +72,6 @@ void setup() { Wire.setClock(i2cFreq); #endif } - //настраиваем микроконтроллер configure("/config.json"); diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 79611e91..5a4ad354 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -220,6 +220,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) generateOrder(key, value); SerialPrint("i", F("=>WS"), "Msg from svelte web, WS No: " + String(num) + ", msg: " + msg); } + + if (headerStr == "/test|") { + //sendBlobToWsStrHeader("/layout.json", "header", num, 1024); + } } break; case WStype_BIN: { @@ -396,6 +400,44 @@ void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, } } +// 6 4 +// layout|0120|{status:12}|...from file... +// layout|0000|...from file... +// layout|0000|...from file... + +void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t client_id, size_t frameSize) { + // откроем файл + auto path = filepath(filename); + auto file = FileFS.open(path, "r"); + if (!file) { + SerialPrint("E", "FS", F("reed file error")); + return; + } + + size_t totalSize = file.size(); + SerialPrint("I", "FS", "Send file '" + String(filename) + "', file size: " + String(totalSize)); + + // размер заголовка + auto headerSize = header.length(); + // выделим буфер размером с фрейм + auto frameBuf = new uint8_t[frameSize]; + // заголовок у нас не меняется, запием его в начало буфера + header.toCharArray((char*)frameBuf, frameSize); + // указатель на начало полезной нагрузки + auto payloadBuf = &frameBuf[headerSize]; + // и сколько осталось места для нее + auto maxPayloadSize = frameSize - headerSize; + + while (file.available()) { + // прочитаем кусок в буфер + size_t payloadSize = file.read(payloadBuf, maxPayloadSize); + if (payloadSize) { + // отправим фрейм + standWebSocket.sendBIN(client_id, frameBuf, headerSize + payloadSize, true); + } + } +} + // void sendMark(const char* filename, const char* mark, uint8_t num) { // char outChar[strlen(filename) + strlen(mark) + 1]; // strcpy(outChar, mark); diff --git a/src/modules/sensors/Pzem004t/modinfo.json b/src/modules/sensors/Pzem004t/modinfo.json index 2a25464f..3d75cce0 100644 --- a/src/modules/sensors/Pzem004t/modinfo.json +++ b/src/modules/sensors/Pzem004t/modinfo.json @@ -102,11 +102,7 @@ }, "defActive": true, "usedLibs": { - "esp32_4mb": [ - "https://github.com/mandulaj/PZEM-004T-v30" - ], - "esp8266_4mb": [ - "https://github.com/mandulaj/PZEM-004T-v30" - ] + "esp32_4mb": [], + "esp8266_4mb": [] } } \ No newline at end of file diff --git a/src/utils/FileUtils.cpp b/src/utils/FileUtils.cpp index f6c128db..d40ab4d2 100644 --- a/src/utils/FileUtils.cpp +++ b/src/utils/FileUtils.cpp @@ -329,10 +329,10 @@ String createDataBaseSting() { for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getSubtype() == "LogingDaily") { String id = (*it)->getID(); - id = "/lgd/" + id + "/" + id + ".txt"; - String fileContent = readFile(id, 10000); + String path = "/lgd/" + id + "/" + id + ".txt"; + String fileContent = readFile(path, 10000); if (fileContent == "failed") { - SerialPrint("i", "Export", "file not exist " + id); + SerialPrint("i", "Export", "file not exist " + path); } else { out += "=>" + fileContent + "\r\n"; } From b44e7c3ae061ad00085d3636b51268d327d3ce00 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Fri, 7 Oct 2022 18:53:10 +0200 Subject: [PATCH 002/107] =?UTF-8?q?=D0=BD=D0=B0=D0=B4=D0=B5=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82=D0=B5?= =?UTF-8?q?=D0=BA=D1=83=20=D1=81=D0=BE=D0=BA=D0=B5=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=D1=8E=20=D0=BF=D0=BE=D1=81=D1=8B=D0=BB=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20=D1=84=D1=80=D0=B5=D0=B9=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/Const.h | 2 +- lib/WebSockets/src/WebSocketsServer.cpp | 4 ++-- lib/WebSockets/src/WebSocketsServer.h | 5 +++-- platformio.ini | 1 - src/WsServer.cpp | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/include/Const.h b/include/Const.h index bb82f3e8..d93e4c43 100644 --- a/include/Const.h +++ b/include/Const.h @@ -1,7 +1,7 @@ #pragma once //Версия прошивки -#define FIRMWARE_VERSION 430 +#define FIRMWARE_VERSION 431 #ifdef esp8266_4mb #define FIRMWARE_NAME "esp8266_4mb" diff --git a/lib/WebSockets/src/WebSocketsServer.cpp b/lib/WebSockets/src/WebSocketsServer.cpp index ea765633..e8b7f223 100644 --- a/lib/WebSockets/src/WebSocketsServer.cpp +++ b/lib/WebSockets/src/WebSocketsServer.cpp @@ -225,13 +225,13 @@ bool WebSocketsServerCore::broadcastTXT(String & payload) { * @param headerToPayload bool (see sendFrame for more details) * @return true if ok */ -bool WebSocketsServerCore::sendBIN(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload) { +bool WebSocketsServerCore::sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin, bool headerToPayload) { if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { return false; } WSclient_t * client = &_clients[num]; if(clientIsConnected(client)) { - return sendFrame(client, WSop_binary, payload, length, true, headerToPayload); + return sendFrame(client, WSop_binary, payload, length, fin, headerToPayload); } return false; } diff --git a/lib/WebSockets/src/WebSocketsServer.h b/lib/WebSockets/src/WebSocketsServer.h index 28ef17c6..88b3b417 100644 --- a/lib/WebSockets/src/WebSocketsServer.h +++ b/lib/WebSockets/src/WebSocketsServer.h @@ -65,7 +65,7 @@ class WebSocketsServerCore : protected WebSockets { bool broadcastTXT(const char * payload, size_t length = 0); bool broadcastTXT(String & payload); - bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool headerToPayload = false); + bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin = true, bool headerToPayload = false); bool sendBIN(uint8_t num, const uint8_t * payload, size_t length); bool broadcastBIN(uint8_t * payload, size_t length, bool headerToPayload = false); @@ -228,7 +228,8 @@ class WebSocketsServer : public WebSocketsServerCore { void loop(void); // handle incoming client and client data #else // Async interface not need a loop call - void loop(void) __attribute__((deprecated)) {} + void loop(void) __attribute__((deprecated)) { + } #endif protected: diff --git a/platformio.ini b/platformio.ini index 4590a0cc..2a704efe 100644 --- a/platformio.ini +++ b/platformio.ini @@ -29,7 +29,6 @@ framework = arduino board = esp32dev platform = espressif32 @5.1.1 monitor_filters = esp32_exception_decoder -upload_speed = 115200 monitor_speed = 115200 debug_tool = esp-prog board_build.filesystem = littlefs diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 5a4ad354..70be5600 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -222,7 +222,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) } if (headerStr == "/test|") { - //sendBlobToWsStrHeader("/layout.json", "header", num, 1024); + sendBlobToWsStrHeader("/layout.json", "layout|0000|", num, 1024); } } break; From 111486c7c3fd7f26e851a0d10de6f14a511166e3 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 7 Oct 2022 22:17:23 +0300 Subject: [PATCH 003/107] =?UTF-8?q?=D0=A7=D1=83=D1=82=D1=8C=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=82=D0=B8=D0=BC=D0=B8=D0=B7=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83=20LCD16XX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/display/Lcd2004/Lcd2004.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/modules/display/Lcd2004/Lcd2004.cpp b/src/modules/display/Lcd2004/Lcd2004.cpp index 3feee118..74f3080d 100644 --- a/src/modules/display/Lcd2004/Lcd2004.cpp +++ b/src/modules/display/Lcd2004/Lcd2004.cpp @@ -36,9 +36,11 @@ class Lcd2004 : public IoTItem { LCDI2C = new LiquidCrystal_I2C(hexStringToUint8(addr), w, h); if (LCDI2C != nullptr) { LCDI2C->init(); - LCDI2C->backlight(); } } + + LCDI2C->clear(); + LCDI2C->backlight(); jsonRead(parameters, "coord", xy); _x = selectFromMarkerToMarker(xy, ",", 0).toInt(); @@ -52,14 +54,13 @@ class Lcd2004 : public IoTItem { if (LCDI2C != nullptr) { printBlankStr(_prevStrSize); - String tmpStr = ""; - if (_descr != "none") tmpStr = _descr + " " + getItemValue(_id2show); - else tmpStr = getItemValue(_id2show); + String tmpStr = getItemValue(_id2show); + if (_descr != "none") tmpStr = _descr + " " + tmpStr; LCDI2C->setCursor(_x, _y); LCDI2C->print(tmpStr); //LCDI2C->print("Helloy,Manager 404 !"); - + //Serial.printf("ffff %s\n", _id2show); _prevStrSize = tmpStr.length(); } } @@ -113,7 +114,10 @@ class Lcd2004 : public IoTItem { LCDI2C->print(tmpStr); } - ~Lcd2004(){}; + ~Lcd2004(){ + if (LCDI2C) delete LCDI2C; + LCDI2C = nullptr; + }; }; void *getAPI_Lcd2004(String subtype, String param) { From 9e4ebd2a5ede0cf787ca0090835b35ad0543ed47 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 7 Oct 2022 22:18:05 +0300 Subject: [PATCH 004/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20IoTItem=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BF=D1=80=D1=8F=D0=BC=D0=BE=D0=BC=20?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D1=89=D0=B5=D0=BD=D0=B8=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=B2=D0=BE=D0=B4=D0=B8=D0=BB=D0=BE=20=D0=BA=20?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8E=20=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 1 + src/classes/IoTItem.cpp | 35 ++++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index e16e536e..555c3a35 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -44,6 +44,7 @@ class IoTItem { virtual IoTGpio* getGpioDriver(); virtual void setValue(IoTValue Value); virtual void setValue(String valStr); + String getRoundValue(); //методы для графиков virtual void publishValue(); diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 68f88c96..4f3cb1a4 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -52,20 +52,7 @@ void IoTItem::loop() { //получить String IoTItem::getValue() { if (value.isDecimal) { - if (_multiply) value.valD = value.valD * _multiply; - if (_plus) value.valD = value.valD + _plus; - if (_round >= 0 && _round <= 6) { - int sot = _round ? pow(10, (int)_round) : 1; - value.valD = round(value.valD * sot) / sot; - } - if (_map1 != _map2) value.valD = map(value.valD, _map1, _map2, _map3, _map4); - - if (_round >= 0 && _round <= 6) { - char buf[15]; - sprintf(buf, ("%1." + (String)_round + "f").c_str(), value.valD); - return value.valS = buf; - } else - return (String)value.valD; + return getRoundValue(); } else return value.valS; } @@ -114,9 +101,27 @@ void IoTItem::regEvent(String value, String consoleInfo = "") { //======================================================================== } +String IoTItem::getRoundValue() { + if (_round >= 0 && _round <= 6) { + int sot = _round ? pow(10, (int)_round) : 1; + value.valD = round(value.valD * sot) / sot; + + char buf[15]; + sprintf(buf, ("%1." + (String)_round + "f").c_str(), value.valD); + return (String)buf; + } else { + return (String)value.valD; + } +} + void IoTItem::regEvent(float regvalue, String consoleInfo = "") { value.valD = regvalue; - regEvent(getValue(), consoleInfo); + + if (_multiply) value.valD = value.valD * _multiply; + if (_plus) value.valD = value.valD + _plus; + if (_map1 != _map2) value.valD = map(value.valD, _map1, _map2, _map3, _map4); + + regEvent(getRoundValue(), consoleInfo); } void IoTItem::doByInterval() {} From 5812d98ff9a69e7196aa36337906a22ea8379a23 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Fri, 7 Oct 2022 22:29:12 +0200 Subject: [PATCH 005/107] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 163 +++++++-------------- lib/WebSockets/src/WebSocketsServer.cpp | 2 +- lib/WebSockets/src/WebSocketsServer.h | 2 +- myProfile.json | 2 +- platformio.ini | 6 +- src/WsServer.cpp | 41 ++++-- src/modules/API.cpp | 2 - src/modules/sensors/FreqMeter/modinfo.json | 2 +- 8 files changed, 87 insertions(+), 133 deletions(-) diff --git a/data_svelte/items.json b/data_svelte/items.json index 74a8c083..01f1ca76 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -276,58 +276,7 @@ "num": 20 }, { - "name": "21. Частотомер на ADC, Частота", - "type": "Reading", - "subtype": "FreqMeterF", - "id": "freq", - "widget": "anydataHtz", - "page": "Частотомер", - "descr": "Частота", - "plus": 0, - "multiply": 1, - "round": 1, - "pin-Esp32": 33, - "samples": 512, - "samplingFrequency": 5000, - "int": 5, - "num": 21 - }, - { - "name": "22. Частотомер на ADC, Процент Пульсации", - "type": "Reading", - "subtype": "FreqMeterPcFl", - "id": "pctFlicker", - "widget": "anydataPct", - "page": "Частотомер", - "descr": "Процент Пульсации", - "plus": 0, - "multiply": 1, - "round": 1, - "pin-Esp32": 33, - "samples": 512, - "samplingFrequency": 5000, - "int": 5, - "num": 22 - }, - { - "name": "23. Частотомер на ADC, Индекс Пульсации", - "type": "Reading", - "subtype": "FreqMeterFlIn", - "id": "flickerIndex", - "widget": "anydataDef", - "page": "Частотомер", - "descr": "Индекс Пульсации", - "plus": 0, - "multiply": 1, - "round": 10, - "pin-Esp32": 33, - "samples": 512, - "samplingFrequency": 5000, - "int": 5, - "num": 23 - }, - { - "name": "24. GY21 Температура", + "name": "21. GY21 Температура", "type": "Reading", "subtype": "GY21t", "id": "tmp4", @@ -336,10 +285,10 @@ "descr": "Температура", "round": 1, "int": 15, - "num": 24 + "num": 21 }, { - "name": "25. GY21 Влажность", + "name": "22. GY21 Влажность", "type": "Reading", "subtype": "GY21h", "id": "Hum4", @@ -348,10 +297,10 @@ "descr": "Влажность", "round": 1, "int": 15, - "num": 25 + "num": 22 }, { - "name": "26. HDC1080 Температура", + "name": "23. HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", "id": "Temp1080", @@ -361,10 +310,10 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 26 + "num": 23 }, { - "name": "27. HDC1080 Влажность", + "name": "24. HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", "id": "Hum1080", @@ -374,10 +323,10 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 27 + "num": 24 }, { - "name": "28. MAX6675 Температура", + "name": "25. MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", "id": "maxtmp", @@ -388,10 +337,10 @@ "DO": 12, "CS": 13, "CLK": 14, - "num": 28 + "num": 25 }, { - "name": "29. PZEM 004t Напряжение", + "name": "26. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", "id": "v", @@ -401,10 +350,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 29 + "num": 26 }, { - "name": "30. PZEM 004t Сила тока", + "name": "27. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", "id": "a", @@ -414,10 +363,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 30 + "num": 27 }, { - "name": "31. PZEM 004t Мощность", + "name": "28. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", "id": "w", @@ -427,10 +376,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 31 + "num": 28 }, { - "name": "32. PZEM 004t Энергия", + "name": "29. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", "id": "wh", @@ -440,10 +389,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 32 + "num": 29 }, { - "name": "33. PZEM 004t Частота", + "name": "30. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", "id": "hz", @@ -453,10 +402,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 33 + "num": 30 }, { - "name": "34. PZEM 004t Косинус", + "name": "31. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", "id": "pf", @@ -466,11 +415,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 34 + "num": 31 }, { - "name": "35. Сканер кнопок 433 MHz", - "num": 35, + "name": "32. Сканер кнопок 433 MHz", + "num": 32, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -479,7 +428,7 @@ "pinTx": 12 }, { - "name": "36. Sht20 Температура", + "name": "33. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", "id": "tmp2", @@ -488,10 +437,10 @@ "descr": "Температура", "int": 15, "round": 1, - "num": 36 + "num": 33 }, { - "name": "37. Sht20 Влажность", + "name": "34. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", "id": "Hum2", @@ -500,10 +449,10 @@ "descr": "Влажность", "int": 15, "round": 1, - "num": 37 + "num": 34 }, { - "name": "38. Sht30 Температура", + "name": "35. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", "id": "tmp30", @@ -512,10 +461,10 @@ "descr": "SHT30 Температура", "int": 15, "round": 1, - "num": 38 + "num": 35 }, { - "name": "39. Sht30 Влажность", + "name": "36. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", "id": "Hum30", @@ -524,11 +473,11 @@ "descr": "SHT30 Влажность", "int": 15, "round": 1, - "num": 39 + "num": 36 }, { - "name": "40. HC-SR04 Ультразвуковой дальномер", - "num": 40, + "name": "37. HC-SR04 Ультразвуковой дальномер", + "num": 37, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -540,7 +489,7 @@ "int": 5 }, { - "name": "41. UART", + "name": "38. UART", "type": "Reading", "subtype": "UART", "page": "", @@ -550,13 +499,13 @@ "tx": 12, "rx": 13, "speed": 9600, - "num": 41 + "num": 38 }, { "header": "Исполнительные устройства" }, { - "name": "42. Кнопка подключенная к пину", + "name": "39. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -569,10 +518,10 @@ "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 42 + "num": 39 }, { - "name": "43. Управление пином", + "name": "40. Управление пином", "type": "Writing", "subtype": "ButtonOut", "id": "btn", @@ -582,10 +531,10 @@ "int": 0, "inv": 0, "pin": 2, - "num": 43 + "num": 40 }, { - "name": "44. Сервопривод", + "name": "41. Сервопривод", "type": "Writing", "subtype": "IoTServo", "id": "servo", @@ -596,10 +545,10 @@ "pin": 12, "apin": -1, "amap": "0, 4096, 0, 180", - "num": 44 + "num": 41 }, { - "name": "45. Расширитель портов Mcp23017", + "name": "42. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -609,10 +558,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 45 + "num": 42 }, { - "name": "46. MP3 плеер", + "name": "43. MP3 плеер", "type": "Reading", "subtype": "Mp3", "id": "mp3", @@ -622,10 +571,10 @@ "int": 1, "pins": "14,12", "volume": 20, - "num": 46 + "num": 43 }, { - "name": "47. Расширитель портов Pcf8574", + "name": "44. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -635,10 +584,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 47 + "num": 44 }, { - "name": "48. PWM ESP8266", + "name": "45. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -650,10 +599,10 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 48 + "num": 45 }, { - "name": "49. Телеграм-Лайт", + "name": "46. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -662,13 +611,13 @@ "descr": "", "token": "", "chatID": "", - "num": 49 + "num": 46 }, { "header": "Экраны" }, { - "name": "50. LCD экран 2004", + "name": "47. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -680,10 +629,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 50 + "num": 47 }, { - "name": "51. LCD экран 1602", + "name": "48. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -695,6 +644,6 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 51 + "num": 48 } ] \ No newline at end of file diff --git a/lib/WebSockets/src/WebSocketsServer.cpp b/lib/WebSockets/src/WebSocketsServer.cpp index e8b7f223..e8da8a3c 100644 --- a/lib/WebSockets/src/WebSocketsServer.cpp +++ b/lib/WebSockets/src/WebSocketsServer.cpp @@ -247,7 +247,7 @@ bool WebSocketsServerCore::sendBIN(uint8_t num, const uint8_t * payload, size_t * @param headerToPayload bool (see sendFrame for more details) * @return true if ok */ -bool WebSocketsServerCore::broadcastBIN(uint8_t * payload, size_t length, bool headerToPayload) { +bool WebSocketsServerCore::broadcastBIN(uint8_t * payload, size_t length, bool fin, bool headerToPayload) { WSclient_t * client; bool ret = true; for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { diff --git a/lib/WebSockets/src/WebSocketsServer.h b/lib/WebSockets/src/WebSocketsServer.h index 88b3b417..16461787 100644 --- a/lib/WebSockets/src/WebSocketsServer.h +++ b/lib/WebSockets/src/WebSocketsServer.h @@ -68,7 +68,7 @@ class WebSocketsServerCore : protected WebSockets { bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin = true, bool headerToPayload = false); bool sendBIN(uint8_t num, const uint8_t * payload, size_t length); - bool broadcastBIN(uint8_t * payload, size_t length, bool headerToPayload = false); + bool broadcastBIN(uint8_t * payload, size_t length, bool fin = true, bool headerToPayload = false); bool broadcastBIN(const uint8_t * payload, size_t length); bool sendPing(uint8_t num, uint8_t * payload = NULL, size_t length = 0); diff --git a/myProfile.json b/myProfile.json index 4e2a5610..18e1f5bc 100644 --- a/myProfile.json +++ b/myProfile.json @@ -90,7 +90,7 @@ }, { "path": "src/modules/sensors/FreqMeter", - "active": true + "active": false }, { "path": "src/modules/sensors/GY21", diff --git a/platformio.ini b/platformio.ini index 2a704efe..971984a1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,7 +9,7 @@ board = nodemcuv2 board_build.ldscript = eagle.flash.4m1m.ld platform = espressif8266 @4.0.1 monitor_filters = esp8266_exception_decoder -upload_speed = 115200 +upload_speed = 921600 monitor_speed = 115200 board_build.filesystem = littlefs build_src_filter = @@ -29,6 +29,7 @@ framework = arduino board = esp32dev platform = espressif32 @5.1.1 monitor_filters = esp32_exception_decoder +upload_speed = 921600 monitor_speed = 115200 debug_tool = esp-prog board_build.filesystem = littlefs @@ -56,11 +57,9 @@ lib_deps = adafruit/Adafruit BMP280 Library beegee-tokyo/DHT sensor library for ESPx milesburton/DallasTemperature@^3.9.1 - kosme/arduinoFFT@^1.5.6 https://github.com/JonasGMorsch/GY-21.git ClosedCube HDC1080 adafruit/MAX6675 library - https://github.com/mandulaj/PZEM-004T-v30 rc-switch @ ^2.6.4 robtillaart/SHT2x@^0.1.1 WEMOS SHT3x@1.0.0 @@ -83,7 +82,6 @@ build_src_filter = + + + - + + + + diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 70be5600..d713b12e 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -181,10 +181,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) // Страница веб интерфейса dev //----------------------------------------------------------------------// if (headerStr == "/dev|") { - standWebSocket.sendTXT(num, errorsHeapJson); - standWebSocket.sendTXT(num, settingsFlashJson); - sendFileToWs("/config.json", num, 1024); - sendFileToWs("/items.json", num, 1024); + // standWebSocket.sendTXT(num, errorsHeapJson); + // standWebSocket.sendTXT(num, settingsFlashJson); + // sendFileToWs("/config.json", num, 1024); + // sendFileToWs("/items.json", num, 1024); } //----------------------------------------------------------------------// @@ -222,7 +222,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) } if (headerStr == "/test|") { - sendBlobToWsStrHeader("/layout.json", "layout|0000|", num, 1024); + sendBlobToWsStrHeader("/items.json", "layout|0000|", num, 2048); } } break; @@ -264,11 +264,11 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //публикация статус сообщений (недостаток в том что делаем бродкаст всем клиентам поднятым в свелте!!!) void publishStatusWs(const String& topic, const String& data) { - String path = mqttRootDevice + "/" + topic; - String json = "{}"; - jsonWriteStr(json, "status", data); - jsonWriteStr(json, "topic", path); - standWebSocket.broadcastTXT(json); + // String path = mqttRootDevice + "/" + topic; + // String json = "{}"; + // jsonWriteStr(json, "status", data); + // jsonWriteStr(json, "topic", path); + // standWebSocket.broadcastTXT(json); } //публикация статус сообщений @@ -292,9 +292,9 @@ void publishChartWs(int num, String& path) { //данные которые мы отправляем в сокеты переодически void periodicWsSend() { - standWebSocket.broadcastTXT(devListHeapJson); - standWebSocket.broadcastTXT(ssidListHeapJson); - standWebSocket.broadcastTXT(errorsHeapJson); + // standWebSocket.broadcastTXT(devListHeapJson); + // standWebSocket.broadcastTXT(ssidListHeapJson); + // standWebSocket.broadcastTXT(errorsHeapJson); } #ifdef ESP32 @@ -427,14 +427,23 @@ void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t auto payloadBuf = &frameBuf[headerSize]; // и сколько осталось места для нее auto maxPayloadSize = frameSize - headerSize; - + int i = 0; while (file.available()) { // прочитаем кусок в буфер size_t payloadSize = file.read(payloadBuf, maxPayloadSize); if (payloadSize) { - // отправим фрейм - standWebSocket.sendBIN(client_id, frameBuf, headerSize + payloadSize, true); + size_t size = headerSize + payloadSize; + bool fin = false; + if (i == 16) { + fin = true; + } else { + fin = false; + } + + SerialPrint("I", "FS", String(size) + " " + String(fin) + " " + String(i)); + standWebSocket.sendBIN(client_id, frameBuf, size, fin); } + i++; } } diff --git a/src/modules/API.cpp b/src/modules/API.cpp index 38b509e8..ab864765 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -12,7 +12,6 @@ void* getAPI_Bme280(String subtype, String params); void* getAPI_Bmp280(String subtype, String params); void* getAPI_Dht1122(String subtype, String params); void* getAPI_Ds18b20(String subtype, String params); -void* getAPI_FreqMeter(String subtype, String params); void* getAPI_GY21(String subtype, String params); void* getAPI_Hdc1080(String subtype, String params); void* getAPI_Max6675(String subtype, String params); @@ -46,7 +45,6 @@ if ((tmpAPI = getAPI_Bme280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Bmp280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Dht1122(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Ds18b20(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_FreqMeter(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_GY21(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Hdc1080(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Max6675(subtype, params)) != nullptr) return tmpAPI; diff --git a/src/modules/sensors/FreqMeter/modinfo.json b/src/modules/sensors/FreqMeter/modinfo.json index c581802c..2bc0ac8d 100644 --- a/src/modules/sensors/FreqMeter/modinfo.json +++ b/src/modules/sensors/FreqMeter/modinfo.json @@ -79,7 +79,7 @@ "int": "Количество секунд между опросами датчика." } }, - "defActive": true, + "defActive": false, "usedLibs": { "esp32_4mb": [ "kosme/arduinoFFT@^1.5.6" From 645e90379c6eb38202e7e7560d63bad248b25626 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Sat, 8 Oct 2022 00:28:45 +0200 Subject: [PATCH 006/107] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=83=20=D1=81=D0=BE=D0=BA=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20=D1=84=D1=80=D0=B5=D0=B9=D0=BC=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/WebSockets/src/WebSocketsServer.cpp | 8 ++++++-- lib/WebSockets/src/WebSocketsServer.h | 2 +- src/WsServer.cpp | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/WebSockets/src/WebSocketsServer.cpp b/lib/WebSockets/src/WebSocketsServer.cpp index e8da8a3c..5c5abea8 100644 --- a/lib/WebSockets/src/WebSocketsServer.cpp +++ b/lib/WebSockets/src/WebSocketsServer.cpp @@ -225,13 +225,17 @@ bool WebSocketsServerCore::broadcastTXT(String & payload) { * @param headerToPayload bool (see sendFrame for more details) * @return true if ok */ -bool WebSocketsServerCore::sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin, bool headerToPayload) { +bool WebSocketsServerCore::sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin, bool continuation, bool headerToPayload) { if(num >= WEBSOCKETS_SERVER_CLIENT_MAX) { return false; } WSclient_t * client = &_clients[num]; if(clientIsConnected(client)) { - return sendFrame(client, WSop_binary, payload, length, fin, headerToPayload); + if(continuation) { + return sendFrame(client, WSop_continuation, payload, length, fin, headerToPayload); + } else { + return sendFrame(client, WSop_binary, payload, length, fin, headerToPayload); + } } return false; } diff --git a/lib/WebSockets/src/WebSocketsServer.h b/lib/WebSockets/src/WebSocketsServer.h index 16461787..cd224dd2 100644 --- a/lib/WebSockets/src/WebSocketsServer.h +++ b/lib/WebSockets/src/WebSocketsServer.h @@ -65,7 +65,7 @@ class WebSocketsServerCore : protected WebSockets { bool broadcastTXT(const char * payload, size_t length = 0); bool broadcastTXT(String & payload); - bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin = true, bool headerToPayload = false); + bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin = true, bool continuation = false, bool headerToPayload = false); bool sendBIN(uint8_t num, const uint8_t * payload, size_t length); bool broadcastBIN(uint8_t * payload, size_t length, bool fin = true, bool headerToPayload = false); diff --git a/src/WsServer.cpp b/src/WsServer.cpp index d713b12e..7ed90a58 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -433,15 +433,23 @@ void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t size_t payloadSize = file.read(payloadBuf, maxPayloadSize); if (payloadSize) { size_t size = headerSize + payloadSize; + bool fin = false; - if (i == 16) { - fin = true; - } else { + if (size == frameSize) { fin = false; + } else { + fin = true; } - SerialPrint("I", "FS", String(size) + " " + String(fin) + " " + String(i)); - standWebSocket.sendBIN(client_id, frameBuf, size, fin); + bool continuation = false; + if (i == 0) { + continuation = false; + } else { + continuation = true; + } + + SerialPrint("I", "FS", String(i) + ") sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); + standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); } i++; } From 00665e7f2bf84a7a37f079adf218a5445eeb1a58 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Sun, 9 Oct 2022 10:55:37 +0200 Subject: [PATCH 007/107] =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B9=20=D0=B2=20=D0=BF=D1=80=D0=BE=D1=86?= =?UTF-8?q?=D0=B5=D1=81=D1=81=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/build/bundle.css | 40 ++++++ data_svelte/build/bundle.js | 2 + include/Const.h | 1 + include/WsServer.h | 6 +- src/StandWebServer.cpp | 6 +- src/WsServer.cpp | 154 +++++++++++++++++----- src/modules/virtual/Loging/Loging (1).cpp | 34 +++++ 7 files changed, 204 insertions(+), 39 deletions(-) create mode 100644 data_svelte/build/bundle.css create mode 100644 data_svelte/build/bundle.js create mode 100644 src/modules/virtual/Loging/Loging (1).cpp diff --git a/data_svelte/build/bundle.css b/data_svelte/build/bundle.css new file mode 100644 index 00000000..40e33a4a --- /dev/null +++ b/data_svelte/build/bundle.css @@ -0,0 +1,40 @@ +*,::before,::after{box-sizing:border-box}html{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui, + -apple-system, /* Firefox supports this but not yet `system-ui` */ + 'Segoe UI', + Roboto, + Helvetica, + Arial, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, + SFMono-Regular, + Consolas, + 'Liberation Mono', + Menlo, + monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type='button']{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}pre,code,kbd,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::before,::after{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.grd-1col1{display:grid;grid-template-columns:repeat(1, minmax(0, 1fr));justify-items:center}.grd-2col1{display:grid;grid-template-columns:repeat(1, minmax(0, 1fr));justify-items:center;gap:1rem}@media(min-width: 640px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1024px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1280px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1536px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}.grd-2col2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));justify-items:center;gap:1rem}.grd-3col1{display:grid;grid-template-columns:repeat(1, minmax(0, 1fr));justify-items:center;gap:1rem}@media(min-width: 640px){.grd-3col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1024px){.grd-3col1{grid-template-columns:repeat(3, minmax(0, 1fr))}}@media(min-width: 1280px){.grd-3col1{grid-template-columns:repeat(3, minmax(0, 1fr))}}@media(min-width: 1536px){.grd-3col1{grid-template-columns:repeat(3, minmax(0, 1fr))}}.crd-itm-psn{margin-bottom:0.5rem;display:flex;height:2rem;align-items:center}.wgt-dscr-stl{padding-right:1rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.wgt-adt-stl{text-align:center;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.tbl{margin-top:0.5rem;margin-bottom:0.5rem;width:100%;table-layout:fixed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tbl-hd{overflow-wrap:break-word;padding-left:0.25rem;padding-right:0.25rem;text-align:center;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.tbl-bdy-lg{overflow-wrap:break-word;padding-left:0.25rem;padding-right:0.25rem;text-align:center}.tbl-bdy-sm{overflow-wrap:break-word;padding-left:0.25rem;padding-right:0.25rem}.ipt-lg{margin-top:0.5rem;height:1rem;align-content:center;border-width:2px;--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.ipt-lg:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.ipt-lg{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-lg:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-lg{text-align:center;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-lg:focus{outline:2px solid transparent;outline-offset:2px}@media(min-width: 640px){.ipt-lg{height:1.75rem}}@media(min-width: 768px){.ipt-lg{height:1.75rem}}@media(min-width: 1024px){.ipt-lg{height:1.75rem}}@media(min-width: 1280px){.ipt-lg{height:1.75rem}}@media(min-width: 1536px){.ipt-lg{height:1.75rem}}.ipt-sm{height:0.75rem;align-content:center;border-radius:0.125rem;border-width:2px;--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.ipt-sm:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.ipt-sm{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-sm:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-sm{text-align:center;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-sm:focus{outline:2px solid transparent;outline-offset:2px}@media(min-width: 640px){.ipt-sm{height:1.5rem}}@media(min-width: 768px){.ipt-sm{height:1.5rem}}@media(min-width: 1024px){.ipt-sm{height:1.5rem}}@media(min-width: 1280px){.ipt-sm{height:1.5rem}}@media(min-width: 1536px){.ipt-sm{height:1.5rem}}.ipt-rnd{height:2rem;width:100%;align-content:center;border-radius:0.25rem;border-width:2px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-rnd:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-rnd{padding-left:0.5rem;padding-right:0.5rem;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-rnd:focus{outline:2px solid transparent;outline-offset:2px}.ipt-big{height:2rem;width:100%;align-content:center;border-radius:0.25rem;border-width:2px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.ipt-big:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.ipt-big{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-big:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-big{padding-left:0.5rem;padding-right:0.5rem;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-big:focus{outline:2px solid transparent;outline-offset:2px}.txt-ita{display:inline-block;text-align:right;vertical-align:top;font-style:italic;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.txt-pad{padding-left:0.5rem;padding-right:0.5rem;padding-top:0px;padding-bottom:0px}@media(min-width: 640px){.txt-pad{padding-top:0px;padding-bottom:0px}}@media(min-width: 768px){.txt-pad{padding-top:0px;padding-bottom:0px}}@media(min-width: 1024px){.txt-pad{padding-top:0.25rem;padding-bottom:0.25rem}}@media(min-width: 1280px){.txt-pad{padding-top:0.5rem;padding-bottom:0.5rem}}@media(min-width: 1536px){.txt-pad{padding-top:0.5rem;padding-bottom:0.5rem}}.txt-sz{font-size:.5rem}@media(min-width: 640px){.txt-sz{font-size:1rem}}@media(min-width: 768px){.txt-sz{font-size:1rem}}@media(min-width: 1024px){.txt-sz{font-size:1rem}}@media(min-width: 1280px){.txt-sz{font-size:1rem}}@media(min-width: 1536px){.txt-sz{font-size:1rem}}.btn-lg{margin-top:0px;display:flex;height:1.5rem;width:100%;align-content:center;justify-content:center;overflow-wrap:break-word;border-radius:0.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.btn-lg:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.btn-lg{font-size:.875rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}@media(min-width: 640px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 768px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 1024px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 1280px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 1536px){.btn-lg{height:2rem;font-size:1rem}}.slct-lg{margin-bottom:0px;display:flex;height:1.5rem;width:100%;align-content:center;justify-content:center;overflow-wrap:break-word;border-radius:0.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.slct-lg:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.slct-lg{font-size:.875rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}@media(min-width: 640px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 768px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 1024px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 1280px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 1536px){.slct-lg{height:2rem;font-size:1rem}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0px;right:0px;bottom:0px;left:0px}.top-1{top:0.25rem}.left-1{left:0.25rem}.z-10{z-index:10}.z-50{z-index:50}.m-auto{margin:auto}.mt-0{margin-top:0px}.mt-2{margin-top:0.5rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mb-0{margin-bottom:0px}.mb-2{margin-bottom:0.5rem}.ml-0{margin-left:0px}.ml-36{margin-left:9rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-2{height:0.5rem}.h-3{height:0.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-40{height:10rem}.h-80{height:20rem}.h-auto{height:auto}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-1\/12{width:8.333333%}.w-11\/12{width:91.666667%}.w-full{width:100%}.flex-1{flex:1 1 0%}.table-fixed{table-layout:fixed}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}@-webkit-keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}@keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.flex-col{flex-direction:column}.content-center{align-content:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:0.25rem}.rounded-md{border-radius:0.375rem}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.border-2{border-width:2px}.border-4{border-width:4px}.border{border-width:1px}.border-solid{border-style:solid}.border-transparent{border-color:transparent}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239, 68, 68, var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgba(96, 165, 250, var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243, 244, 246, var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgba(209, 213, 219, var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(107, 114, 128, var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75, 85, 99, var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgba(254, 242, 242, var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgba(252, 165, 165, var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220, 38, 38, var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgba(255, 251, 235, var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgba(236, 253, 245, var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgba(239, 246, 255, var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgba(37, 99, 235, var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgba(99, 102, 241, var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185, 28, 28, var(--tw-bg-opacity))}.hover\:bg-blue-200:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgba(67, 56, 202, var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-cover{background-size:cover}.p-0{padding:0px}.p-2{padding:0.5rem}.px-1{padding-left:0.25rem;padding-right:0.25rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0px;padding-bottom:0px}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.pt-0{padding-top:0px}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pb-20{padding-bottom:5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.text-xxs{font-size:.5rem}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgba(0, 0, 0, var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255, 255, 255, var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17, 24, 39, var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239, 68, 68, var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245, 158, 11, var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgba(52, 211, 153, var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16, 185, 129, var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgba(59, 130, 246, var(--tw-text-opacity))}*,::before,::after{--tw-shadow:0 0 #0000}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}*,::before,::after{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(239, 68, 68, var(--tw-ring-opacity))}.focus\:ring-indigo-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(129, 140, 248, var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(99, 102, 241, var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition{transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}#menu__toggle{position:relative;opacity:0}#menu__toggle:checked~.menu__btn>span{transform:rotate(45deg)}#menu__toggle:checked~.menu__btn>span::before{top:0;transform:rotate(0)}#menu__toggle:checked~.menu__btn>span::after{top:0;transform:rotate(90deg)}#menu__toggle:checked~.menu__box{visibility:visible;left:0}#menu__toggle:checked~.menu__main{margin-left:150px;transition-duration:0.25s}.menu__btn{display:flex;align-items:center;position:fixed;z-index:1;top:10px;left:20px;width:20px;height:20px;cursor:pointer}.menu__btn>span,.menu__btn>span::before,.menu__btn>span::after{display:block;position:absolute;width:100%;height:2px;background-color:#616161;transition-duration:0.25s}.menu__btn>span::before{content:"";top:-8px}.menu__btn>span::after{content:"";top:8px}.menu__box{display:block;position:fixed;visibility:hidden;top:0;left:-100%;width:150px;height:100%;margin:0;padding:80px 0;list-style:none;background-color:#eceff1;box-shadow:1px 0px 6px rgba(0, 0, 0, 0.2);transition-duration:0.25s}.menu__item{display:block;padding:12px 24px;color:rgba(51, 51, 51, 0.788);font-family:"Roboto", sans-serif;font-size:15px;font-weight:600;text-decoration:none;transition-duration:0.25s}.menu__item:hover{background-color:#cfd8dc}.upper__bar{background-color:rgba(51, 51, 51, 0.144);height:70px;position:fixed;z-index:-1;top:0px;left:0;width:100%;margin:0;padding:0;box-shadow:1px 0px 3px rgba(0, 0, 0, 0.2)}input[type="date"]::-webkit-calendar-picker-indicator{margin-left:5px;margin-right:-8px}input[type="time"]::-webkit-calendar-picker-indicator{margin-left:5px;margin-right:-8px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{margin-left:7px;margin-right:-6px;width:30px;height:30px;opacity:1}input:checked~.dot{transform:translateX(100%)}@media(min-width: 640px){.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:mt-0{margin-top:0px}.sm\:ml-3{margin-left:0.75rem}.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:h-6{height:1.5rem}.sm\:h-7{height:1.75rem}.sm\:h-8{height:2rem}.sm\:h-screen{height:100vh}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-start{align-items:flex-start}.sm\:p-0{padding:0px}.sm\:p-2{padding:0.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-0{padding-top:0px;padding-bottom:0px}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem}.sm\:text-base{font-size:1rem}}@media(min-width: 768px){.md\:h-6{height:1.5rem}.md\:h-7{height:1.75rem}.md\:h-8{height:2rem}.md\:p-2{padding:0.5rem}.md\:py-0{padding-top:0px;padding-bottom:0px}.md\:text-base{font-size:1rem}}@media(min-width: 1024px){.lg\:h-6{height:1.5rem}.lg\:h-7{height:1.75rem}.lg\:h-8{height:2rem}.lg\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.lg\:p-2{padding:0.5rem}.lg\:py-1{padding-top:0.25rem;padding-bottom:0.25rem}.lg\:text-base{font-size:1rem}.lg\:shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.xl\:h-6{height:1.5rem}.xl\:h-7{height:1.75rem}.xl\:h-8{height:2rem}.xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.xl\:px-4{padding-left:1rem;padding-right:1rem}.xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.xl\:py-4{padding-top:1rem;padding-bottom:1rem}.xl\:text-base{font-size:1rem}}@media(min-width: 1536px){.\32xl\:h-6{height:1.5rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-8{height:2rem}.\32xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.\32xl\:px-4{padding-left:1rem;padding-right:1rem}.\32xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.\32xl\:py-4{padding-top:1rem;padding-bottom:1rem}.\32xl\:text-base{font-size:1rem}}*,::before,::after{box-sizing:border-box}html{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui, + -apple-system, /* Firefox supports this but not yet `system-ui` */ + 'Segoe UI', + Roboto, + Helvetica, + Arial, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, + SFMono-Regular, + Consolas, + 'Liberation Mono', + Menlo, + monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type='button']{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}pre,code,kbd,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::before,::after{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.alm{margin-top:1rem;width:100%;border-radius:0.5rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity));padding:0.5rem;--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}@media(min-width: 640px){.alm{padding:0.5rem}}@media(min-width: 768px){.alm{padding:0.5rem}}@media(min-width: 1024px){.alm{padding:0.5rem;--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.alm{padding-left:1rem;padding-right:1rem;padding-top:0.5rem;padding-bottom:0.5rem}}@media(min-width: 1536px){.alm{padding-left:1rem;padding-right:1rem;padding-top:0.5rem;padding-bottom:0.5rem}}.alm-hdr{padding-bottom:0px;text-align:center;font-size:1rem;font-weight:700;--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0px;right:0px;bottom:0px;left:0px}.top-1{top:0.25rem}.left-1{left:0.25rem}.z-10{z-index:10}.z-50{z-index:50}.m-auto{margin:auto}.mt-0{margin-top:0px}.mt-2{margin-top:0.5rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mb-0{margin-bottom:0px}.mb-2{margin-bottom:0.5rem}.ml-0{margin-left:0px}.ml-36{margin-left:9rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-2{height:0.5rem}.h-3{height:0.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-40{height:10rem}.h-80{height:20rem}.h-auto{height:auto}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-1\/12{width:8.333333%}.w-11\/12{width:91.666667%}.w-full{width:100%}.flex-1{flex:1 1 0%}.table-fixed{table-layout:fixed}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}@-webkit-keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}@keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.flex-col{flex-direction:column}.content-center{align-content:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:0.25rem}.rounded-md{border-radius:0.375rem}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.border-2{border-width:2px}.border-4{border-width:4px}.border{border-width:1px}.border-solid{border-style:solid}.border-transparent{border-color:transparent}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239, 68, 68, var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgba(96, 165, 250, var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243, 244, 246, var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgba(209, 213, 219, var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(107, 114, 128, var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75, 85, 99, var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgba(254, 242, 242, var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgba(252, 165, 165, var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220, 38, 38, var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgba(255, 251, 235, var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgba(236, 253, 245, var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgba(239, 246, 255, var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgba(37, 99, 235, var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgba(99, 102, 241, var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185, 28, 28, var(--tw-bg-opacity))}.hover\:bg-blue-200:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgba(67, 56, 202, var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-cover{background-size:cover}.p-0{padding:0px}.p-2{padding:0.5rem}.px-1{padding-left:0.25rem;padding-right:0.25rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0px;padding-bottom:0px}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.pt-0{padding-top:0px}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pb-20{padding-bottom:5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.text-xxs{font-size:.5rem}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgba(0, 0, 0, var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255, 255, 255, var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17, 24, 39, var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239, 68, 68, var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245, 158, 11, var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgba(52, 211, 153, var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16, 185, 129, var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgba(59, 130, 246, var(--tw-text-opacity))}*,::before,::after{--tw-shadow:0 0 #0000}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}*,::before,::after{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(239, 68, 68, var(--tw-ring-opacity))}.focus\:ring-indigo-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(129, 140, 248, var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(99, 102, 241, var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition{transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}@media(min-width: 640px){.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:mt-0{margin-top:0px}.sm\:ml-3{margin-left:0.75rem}.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:h-6{height:1.5rem}.sm\:h-7{height:1.75rem}.sm\:h-8{height:2rem}.sm\:h-screen{height:100vh}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-start{align-items:flex-start}.sm\:p-0{padding:0px}.sm\:p-2{padding:0.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-0{padding-top:0px;padding-bottom:0px}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem}.sm\:text-base{font-size:1rem}}@media(min-width: 768px){.md\:h-6{height:1.5rem}.md\:h-7{height:1.75rem}.md\:h-8{height:2rem}.md\:p-2{padding:0.5rem}.md\:py-0{padding-top:0px;padding-bottom:0px}.md\:text-base{font-size:1rem}}@media(min-width: 1024px){.lg\:h-6{height:1.5rem}.lg\:h-7{height:1.75rem}.lg\:h-8{height:2rem}.lg\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.lg\:p-2{padding:0.5rem}.lg\:py-1{padding-top:0.25rem;padding-bottom:0.25rem}.lg\:text-base{font-size:1rem}.lg\:shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.xl\:h-6{height:1.5rem}.xl\:h-7{height:1.75rem}.xl\:h-8{height:2rem}.xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.xl\:px-4{padding-left:1rem;padding-right:1rem}.xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.xl\:py-4{padding-top:1rem;padding-bottom:1rem}.xl\:text-base{font-size:1rem}}@media(min-width: 1536px){.\32xl\:h-6{height:1.5rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-8{height:2rem}.\32xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.\32xl\:px-4{padding-left:1rem;padding-right:1rem}.\32xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.\32xl\:py-4{padding-top:1rem;padding-bottom:1rem}.\32xl\:text-base{font-size:1rem}}*,::before,::after{box-sizing:border-box}html{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui, + -apple-system, /* Firefox supports this but not yet `system-ui` */ + 'Segoe UI', + Roboto, + Helvetica, + Arial, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, + SFMono-Regular, + Consolas, + 'Liberation Mono', + Menlo, + monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type='button']{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}pre,code,kbd,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::before,::after{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.crd{margin-top:1rem;width:100%;border-radius:0.5rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity));padding:0.5rem;--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}@media(min-width: 640px){.crd{padding:0.5rem}}@media(min-width: 768px){.crd{padding:0.5rem}}@media(min-width: 1024px){.crd{padding:0.5rem;--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.crd{padding-left:1rem;padding-right:1rem;padding-top:1rem;padding-bottom:1rem}}@media(min-width: 1536px){.crd{padding-left:1rem;padding-right:1rem;padding-top:1rem;padding-bottom:1rem}}.crd-hdr{padding-bottom:1rem;text-align:center;font-size:1.125rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0px;right:0px;bottom:0px;left:0px}.top-1{top:0.25rem}.left-1{left:0.25rem}.z-10{z-index:10}.z-50{z-index:50}.m-auto{margin:auto}.mt-0{margin-top:0px}.mt-2{margin-top:0.5rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mb-0{margin-bottom:0px}.mb-2{margin-bottom:0.5rem}.ml-0{margin-left:0px}.ml-36{margin-left:9rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-2{height:0.5rem}.h-3{height:0.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-40{height:10rem}.h-80{height:20rem}.h-auto{height:auto}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-1\/12{width:8.333333%}.w-11\/12{width:91.666667%}.w-full{width:100%}.flex-1{flex:1 1 0%}.table-fixed{table-layout:fixed}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}@-webkit-keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}@keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.flex-col{flex-direction:column}.content-center{align-content:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:0.25rem}.rounded-md{border-radius:0.375rem}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.border-2{border-width:2px}.border-4{border-width:4px}.border{border-width:1px}.border-solid{border-style:solid}.border-transparent{border-color:transparent}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239, 68, 68, var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgba(96, 165, 250, var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243, 244, 246, var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgba(209, 213, 219, var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(107, 114, 128, var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75, 85, 99, var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgba(254, 242, 242, var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgba(252, 165, 165, var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220, 38, 38, var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgba(255, 251, 235, var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgba(236, 253, 245, var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgba(239, 246, 255, var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgba(37, 99, 235, var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgba(99, 102, 241, var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185, 28, 28, var(--tw-bg-opacity))}.hover\:bg-blue-200:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgba(67, 56, 202, var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-cover{background-size:cover}.p-0{padding:0px}.p-2{padding:0.5rem}.px-1{padding-left:0.25rem;padding-right:0.25rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0px;padding-bottom:0px}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.pt-0{padding-top:0px}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pb-20{padding-bottom:5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.text-xxs{font-size:.5rem}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgba(0, 0, 0, var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255, 255, 255, var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17, 24, 39, var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239, 68, 68, var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245, 158, 11, var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgba(52, 211, 153, var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16, 185, 129, var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgba(59, 130, 246, var(--tw-text-opacity))}*,::before,::after{--tw-shadow:0 0 #0000}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}*,::before,::after{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(239, 68, 68, var(--tw-ring-opacity))}.focus\:ring-indigo-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(129, 140, 248, var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(99, 102, 241, var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition{transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}@media(min-width: 640px){.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:mt-0{margin-top:0px}.sm\:ml-3{margin-left:0.75rem}.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:h-6{height:1.5rem}.sm\:h-7{height:1.75rem}.sm\:h-8{height:2rem}.sm\:h-screen{height:100vh}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-start{align-items:flex-start}.sm\:p-0{padding:0px}.sm\:p-2{padding:0.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-0{padding-top:0px;padding-bottom:0px}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem}.sm\:text-base{font-size:1rem}}@media(min-width: 768px){.md\:h-6{height:1.5rem}.md\:h-7{height:1.75rem}.md\:h-8{height:2rem}.md\:p-2{padding:0.5rem}.md\:py-0{padding-top:0px;padding-bottom:0px}.md\:text-base{font-size:1rem}}@media(min-width: 1024px){.lg\:h-6{height:1.5rem}.lg\:h-7{height:1.75rem}.lg\:h-8{height:2rem}.lg\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.lg\:p-2{padding:0.5rem}.lg\:py-1{padding-top:0.25rem;padding-bottom:0.25rem}.lg\:text-base{font-size:1rem}.lg\:shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.xl\:h-6{height:1.5rem}.xl\:h-7{height:1.75rem}.xl\:h-8{height:2rem}.xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.xl\:px-4{padding-left:1rem;padding-right:1rem}.xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.xl\:py-4{padding-top:1rem;padding-bottom:1rem}.xl\:text-base{font-size:1rem}}@media(min-width: 1536px){.\32xl\:h-6{height:1.5rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-8{height:2rem}.\32xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.\32xl\:px-4{padding-left:1rem;padding-right:1rem}.\32xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.\32xl\:py-4{padding-top:1rem;padding-bottom:1rem}.\32xl\:text-base{font-size:1rem}} \ No newline at end of file diff --git a/data_svelte/build/bundle.js b/data_svelte/build/bundle.js new file mode 100644 index 00000000..05d97824 --- /dev/null +++ b/data_svelte/build/bundle.js @@ -0,0 +1,2 @@ +var app=function(){"use strict";function t(){}function e(t){return t()}function n(){return Object.create(null)}function s(t){t.forEach(e)}function r(t){return"function"==typeof t}function l(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function o(e,n,s){e.$$.on_destroy.push(function(e,...n){if(null==e)return t;const s=e.subscribe(...n);return s.unsubscribe?()=>s.unsubscribe():s}(n,s))}function c(t,e,n,s){if(t){const r=i(t,e,n,s);return t[0](r)}}function i(t,e,n,s){return t[1]&&s?function(t,e){for(const n in e)t[n]=e[n];return t}(n.ctx.slice(),t[1](s(e))):n.ctx}function a(t,e,n,s){if(t[2]&&s){const r=t[2](s(n));if(void 0===e.dirty)return r;if("object"==typeof r){const t=[],n=Math.max(e.dirty.length,r.length);for(let s=0;s32){const e=[],n=t.ctx.length/32;for(let t=0;tt.removeEventListener(e,n,s)}function y(t,e,n){null==n?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function k(t){return""===t?null:+t}function _(t,e){e=""+e,t.wholeText!==e&&(t.data=e)}function J(t,e){t.value=null==e?"":e}function j(t,e){for(let n=0;n{V.delete(t),s&&(n&&t.d(1),s())})),t.o(e)}}function X(t,e,n){const s=t.$$.props[e];void 0!==s&&(t.$$.bound[s]=n,n(t.$$.ctx[s]))}function tt(t){t&&t.c()}function et(t,n,l,o){const{fragment:c,on_mount:i,on_destroy:a,after_update:u}=t.$$;c&&c.m(n,l),o||D((()=>{const n=i.map(e).filter(r);a?a.push(...n):s(n),t.$$.on_mount=[]})),u.forEach(D)}function nt(t,e){const n=t.$$;null!==n.fragment&&(s(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function st(e,r,l,o,c,i,a,u=[-1]){const d=T;M(e);const f=e.$$={fragment:null,ctx:null,props:i,update:t,not_equal:c,bound:n(),on_mount:[],on_destroy:[],on_disconnect:[],before_update:[],after_update:[],context:new Map(r.context||(d?d.$$.context:[])),callbacks:n(),dirty:u,skip_bound:!1,root:r.target||d.$$.root};a&&a(f.root);let p=!1;if(f.ctx=l?l(e,r.props||{},((t,n,...s)=>{const r=s.length?s[0]:n;return f.ctx&&c(f.ctx[t],f.ctx[t]=r)&&(!f.skip_bound&&f.bound[t]&&f.bound[t](r),p&&function(t,e){-1===t.$$.dirty[0]&&(C.push(t),B(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}$set(t){var e;this.$$set&&(e=t,0!==Object.keys(e).length)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}const lt=[];function ot(e,n=t){let s;const r=new Set;function o(t){if(l(e,t)&&(e=t,s)){const t=!lt.length;for(const t of r)t[1](),lt.push(t,e);if(t){for(let t=0;t{r.delete(i),0===r.size&&(s(),s=null)}}}}function ct(t,e=!1){return(t=t.slice(t.startsWith("/#")?2:0,t.endsWith("/*")?-2:void 0)).startsWith("/")||(t="/"+t),"/"===t&&(t=""),e&&!t.endsWith("/")&&(t+="/"),t}function it(t,e,n){if(""===n)return t;if("/"===n[0])return n;let s=t=>t.split("/").filter((t=>""!==t)),r=s(t);return"/"+(e?s(e):[]).map(((t,e)=>r[e])).join("/")+"/"+n}function at(t,e,n,s){let r=[e,"data-"+e].reduce(((e,s)=>{let r=t.getAttribute(s);return n&&t.removeAttribute(s),null===r?e:r}),!1);return!s&&""===r||(r||s||!1)}function ut(t){let e=t.split("&").map((t=>t.split("="))).reduce(((t,e)=>{let n=e[0];if(!n)return t;let s=!(e.length>1)||e[e.length-1];return"string"==typeof s&&s.includes(",")&&(s=s.split(",")),void 0===t[n]?t[n]=[s]:t[n].push(s),t}),{});return Object.entries(e).reduce(((t,e)=>(t[e[0]]=e[1].length>1?e[1]:e[1][0],t)),{})}var dt,ft,pt={HISTORY:1,HASH:2,MEMORY:3,OFF:4,run:function(t,e,n,s){return 1===t?e&&e():2===t?n&&n():s&&s()},getDeafault:function(){return window&&"srcdoc"!==window.location.pathname?1:3}},gt=function(){let t,e=pt.getDeafault(),n=n=>t&&t(mt(e));function s(t){t&&(e=t),window.onhashchange=window.onpopstate=ft=null,e!==pt.OFF&&pt.run(e,(t=>window.onpopstate=n),(t=>window.onhashchange=n))&&n()}return{mode:t=>s(t),get:t=>mt(e),go(t,s){(function(t,e,n){let s=t=>history[n?"replaceState":"pushState"]({},"",t);pt.run(t,(t=>s(e)),(t=>s(`#${e}`)),(t=>ft=e))})(e,t,s),n()},start(e){t=e,s()},stop(){t=null,s(pt.OFF)}}}();function mt(t){let e=dt,n=dt=pt.run(t,(t=>window.location.pathname+window.location.search),(t=>String(window.location.hash.slice(1)||"/")),(t=>ft||"/")),s=n.match(/^([^?#]+)(?:\?([^#]+))?(?:\#(.+))?$/);return{url:n,from:e,path:s[1]||"",query:ut(s[2]||""),hash:s[3]||""}}function ht(t){let e=E("tinro");e&&(e.exact||e.fallback)&&function(t){throw new Error("[Tinro] "+t)}(`${t.fallback?"":``} can't be inside ${e.fallback?"":` with exact path`}`);let n=t.fallback?"fallbacks":"childs",s=ot({}),r={router:{},exact:!1,pattern:null,meta:{},parent:e,fallback:t.fallback,redirect:!1,firstmatch:!1,breadcrumb:null,matched:!1,childs:new Set,activeChilds:new Set,fallbacks:new Set,update(t){r.exact=!t.path.endsWith("/*"),r.pattern=ct(`${r.parent&&r.parent.pattern||""}${t.path}`),r.redirect=t.redirect,r.firstmatch=t.firstmatch,r.breadcrumb=t.breadcrumb,r.match()},register:()=>{if(r.parent)return r.parent[n].add(r),()=>{r.parent[n].delete(r),r.router.un&&r.router.un()}},show:()=>{t.onShow(),!r.fallback&&r.parent&&r.parent.activeChilds.add(r)},hide:()=>{t.onHide(),!r.fallback&&r.parent&&r.parent.activeChilds.delete(r)},match:async()=>{r.matched=!1;let{path:e,url:n,from:l,query:o}=r.router,c=function(t,e){t=ct(t,!0),e=ct(e,!0);let n=[],s={},r=!0,l=t.split("/").map((t=>t.startsWith(":")?(n.push(t.slice(1)),"([^\\/]+)"):t)).join("\\/"),o=e.match(new RegExp(`^${l}$`));return o||(r=!1,o=e.match(new RegExp(`^${l}`))),o?(n.forEach(((t,e)=>s[t]=o[e+1])),{exact:r,params:s,part:o[0].slice(0,-1)}):null}(r.pattern,e);if(!r.fallback&&c&&r.redirect&&(!r.exact||r.exact&&c.exact)){await z();let t=it(e,r.parent&&r.parent.pattern,r.redirect);return xt.goto(t,!0)}if(r.meta=c&&{from:l,url:n,query:o,match:c.part,pattern:r.pattern,breadcrumbs:r.parent&&r.parent.meta&&r.parent.meta.breadcrumbs.slice()||[],params:c.params,subscribe:s.subscribe},r.breadcrumb&&r.meta&&r.meta.breadcrumbs.push({name:r.breadcrumb,path:c.part}),s.set(r.meta),!c||r.fallback||!(!r.exact||r.exact&&c.exact)||r.parent&&r.parent.firstmatch&&r.parent.matched?r.hide():(t.onMeta(r.meta),r.parent&&(r.parent.matched=!0),r.show()),await z(),c&&!r.fallback&&(r.childs.size>0&&0==r.activeChilds.size||0==r.childs.size&&r.fallbacks.size>0)){let t=r;for(;0==t.fallbacks.size;)if(t=t.parent,!t)return;t&&t.fallbacks.forEach((t=>{if(t.redirect){let e=it("/",t.parent&&t.parent.pattern,t.redirect);xt.goto(e,!0)}else t.show()}))}}};return l="tinro",o=r,L().$$.context.set(l,o),q((()=>r.register())),r.router.un=xt.subscribe((t=>{r.router.path=t.path,r.router.url=t.url,r.router.query=t.query,r.router.from=t.from,null!==r.pattern&&r.match()})),r;var l,o}function $t(){return E("tinro").meta}var xt=function(){let{subscribe:t}=ot(gt.get(),(t=>{gt.start(t);let e=function(t){let e=e=>{let n=e.target.closest("a[href]"),s=n&&at(n,"target",!1,"_self"),r=n&&at(n,"tinro-ignore"),l=e.ctrlKey||e.metaKey||e.altKey||e.shiftKey;if("_self"==s&&!r&&!l&&n){let s=n.getAttribute("href").replace(/^\/#/,"");/^\/\/|^[a-zA-Z]+:/.test(s)||(e.preventDefault(),t(s.startsWith("/")?s:n.href.replace(window.location.origin,"")))}};return addEventListener("click",e),()=>removeEventListener("click",e)}(gt.go);return()=>{gt.stop(),e()}}));return{subscribe:t,goto:gt.go,params:bt,meta:$t,useHashNavigation:t=>gt.mode(t?pt.HASH:pt.HISTORY),mode:{hash:()=>gt.mode(pt.HASH),history:()=>gt.mode(pt.HISTORY),memory:()=>gt.mode(pt.MEMORY)}}}();function bt(){return E("tinro").meta.params}const wt=t=>({params:2&t,meta:4&t}),vt=t=>({params:t[1],meta:t[2]});function yt(t){let e;const n=t[9].default,s=c(n,t,t[8],vt);return{c(){s&&s.c()},m(t,n){s&&s.m(t,n),e=!0},p(t,r){s&&s.p&&(!e||262&r)&&u(s,n,t,t[8],e?a(n,t[8],r,wt):d(t[8]),vt)},i(t){e||(G(s,t),e=!0)},o(t){Z(s,t),e=!1},d(t){s&&s.d(t)}}}function kt(t){let e,n,s=t[0]&&yt(t);return{c(){s&&s.c(),e=w()},m(t,r){s&&s.m(t,r),p(t,e,r),n=!0},p(t,[n]){t[0]?s?(s.p(t,n),1&n&&G(s,1)):(s=yt(t),s.c(),G(s,1),s.m(e.parentNode,e)):s&&(K(),Z(s,1,1,(()=>{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function _t(t,e,n){let{$$slots:s={},$$scope:r}=e,{path:l="/*"}=e,{fallback:o=!1}=e,{redirect:c=!1}=e,{firstmatch:i=!1}=e,{breadcrumb:a=null}=e,u=!1,d={},f={};const p=ht({fallback:o,onShow(){n(0,u=!0)},onHide(){n(0,u=!1)},onMeta(t){n(2,f=t),n(1,d=f.params)}});return t.$$set=t=>{"path"in t&&n(3,l=t.path),"fallback"in t&&n(4,o=t.fallback),"redirect"in t&&n(5,c=t.redirect),"firstmatch"in t&&n(6,i=t.firstmatch),"breadcrumb"in t&&n(7,a=t.breadcrumb),"$$scope"in t&&n(8,r=t.$$scope)},t.$$.update=()=>{232&t.$$.dirty&&p.update({path:l,redirect:c,firstmatch:i,breadcrumb:a})},[u,d,f,l,o,c,i,a,r,s]}class Jt extends rt{constructor(t){super(),st(this,t,_t,kt,l,{path:3,fallback:4,redirect:5,firstmatch:6,breadcrumb:7})}}function jt(e){let n,s,l,o,c;return{c(){n=$("svg"),s=$("line"),l=$("line"),y(s,"x1","18"),y(s,"y1","6"),y(s,"x2","6"),y(s,"y2","18"),y(l,"x1","6"),y(l,"y1","6"),y(l,"x2","18"),y(l,"y2","18"),y(n,"class","h-6 w-6 text-red-400 cursor-pointer"),y(n,"viewBox","0 -2 24 24"),y(n,"fill","none"),y(n,"stroke","currentColor"),y(n,"stroke-width","2"),y(n,"stroke-linecap","round"),y(n,"stroke-linejoin","round")},m(t,i){p(t,n,i),f(n,s),f(n,l),o||(c=v(n,"click",(function(){r(e[0]())&&e[0]().apply(this,arguments)})),o=!0)},p(t,[n]){e=t},i:t,o:t,d(t){t&&g(n),o=!1,c()}}}function St(t,e,n){let{click:s=(()=>{})}=e;return t.$$set=t=>{"click"in t&&n(0,s=t.click)},[s]}class Tt extends rt{constructor(t){super(),st(this,t,St,jt,l,{click:0})}}function Mt(t){let e,n,s,r,l,o,c,i;return c=new Tt({props:{click:t[5]}}),{c(){e=h("div"),n=h("div"),s=h("h1"),r=x(t[0]),l=b(),o=h("div"),tt(c.$$.fragment),y(s,"class","alm-hdr"),y(n,"class","w-11/12"),y(o,"class","flex justify-end w-1/12"),y(e,"class","flex items-center")},m(t,a){p(t,e,a),f(e,n),f(n,s),f(s,r),f(e,l),f(e,o),et(c,o,null),i=!0},p(t,e){(!i||1&e)&&_(r,t[0]);const n={};4&e&&(n.click=t[5]),c.$set(n)},i(t){i||(G(c.$$.fragment,t),i=!0)},o(t){Z(c.$$.fragment,t),i=!1},d(t){t&&g(e),nt(c)}}}function Lt(e){let n,s;return{c(){n=h("h1"),s=x(e[0]),y(n,"class","alm-hdr")},m(t,e){p(t,n,e),f(n,s)},p(t,e){1&e&&_(s,t[0])},i:t,o:t,d(t){t&&g(n)}}}function qt(t){let e,n,s,r,l;const o=[Lt,Mt],i=[];function m(t,e){return t[0]&&!t[1]?0:t[0]&&t[1]?1:-1}~(n=m(t))&&(s=i[n]=o[n](t));const $=t[4].default,x=c($,t,t[3],null);return{c(){e=h("div"),s&&s.c(),r=b(),x&&x.c(),y(e,"class","alm")},m(t,s){p(t,e,s),~n&&i[n].m(e,null),f(e,r),x&&x.m(e,null),l=!0},p(t,[c]){let f=n;n=m(t),n===f?~n&&i[n].p(t,c):(s&&(K(),Z(i[f],1,1,(()=>{i[f]=null})),Q()),~n?(s=i[n],s?s.p(t,c):(s=i[n]=o[n](t),s.c()),G(s,1),s.m(e,r)):s=null),x&&x.p&&(!l||8&c)&&u(x,$,t,t[3],l?a($,t[3],c,null):d(t[3]),null)},i(t){l||(G(s),G(x,t),l=!0)},o(t){Z(s),Z(x,t),l=!1},d(t){t&&g(e),~n&&i[n].d(),x&&x.d(t)}}}function Et(t,e,n){let{$$slots:s={},$$scope:r}=e,{title:l=!1}=e,{cross:o=!1}=e,{close:c=(()=>{})}=e;return t.$$set=t=>{"title"in t&&n(0,l=t.title),"cross"in t&&n(1,o=t.cross),"close"in t&&n(2,c=t.close),"$$scope"in t&&n(3,r=t.$$scope)},[l,o,c,r,s,()=>c()]}class Ct extends rt{constructor(t){super(),st(this,t,Et,qt,l,{title:0,cross:1,close:2})}}function Ot(e){let n;return{c(){n=h("div"),n.innerHTML='
\n
',y(n,"class","fixed z-10 inset-0 overflow-y-auto"),y(n,"aria-labelledby","modal-title"),y(n,"role","dialog"),y(n,"aria-modal","true")},m(t,e){p(t,n,e)},p:t,i:t,o:t,d(t){t&&g(n)}}}class Nt extends rt{constructor(t){super(),st(this,t,null,Ot,l,{})}}function Ht(t){let e,n,s,r=t[0]&&Pt(t);const l=t[3].default,o=c(l,t,t[2],null);return{c(){e=h("div"),r&&r.c(),n=b(),o&&o.c(),y(e,"class","crd")},m(t,l){p(t,e,l),r&&r.m(e,null),f(e,n),o&&o.m(e,null),s=!0},p(t,c){t[0]?r?r.p(t,c):(r=Pt(t),r.c(),r.m(e,n)):r&&(r.d(1),r=null),o&&o.p&&(!s||4&c)&&u(o,l,t,t[2],s?a(l,t[2],c,null):d(t[2]),null)},i(t){s||(G(o,t),s=!0)},o(t){Z(o,t),s=!1},d(t){t&&g(e),r&&r.d(),o&&o.d(t)}}}function Pt(t){let e,n;return{c(){e=h("h1"),n=x(t[0]),y(e,"class","crd-hdr")},m(t,s){p(t,e,s),f(e,n)},p(t,e){1&e&&_(n,t[0])},d(t){t&&g(e)}}}function At(t){let e,n,s=t[1]&&Ht(t);return{c(){s&&s.c(),e=w()},m(t,r){s&&s.m(t,r),p(t,e,r),n=!0},p(t,[n]){t[1]?s?(s.p(t,n),2&n&&G(s,1)):(s=Ht(t),s.c(),G(s,1),s.m(e.parentNode,e)):s&&(K(),Z(s,1,1,(()=>{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function Bt(t,e,n){let{$$slots:s={},$$scope:r}=e,{title:l=!1}=e,{show:o=!0}=e;return t.$$set=t=>{"title"in t&&n(0,l=t.title),"show"in t&&n(1,o=t.show),"$$scope"in t&&n(2,r=t.$$scope)},[l,o,r,s]}class zt extends rt{constructor(t){super(),st(this,t,Bt,At,l,{title:0,show:1})}}function Dt(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"step","0.1"),y(e,"type","number")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[2]),v(e,"input",t[3])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&k(e.value)!==t[0].status&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function It(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"type","text")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[4]),v(e,"input",t[5])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&e.value!==t[0].status&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function Rt(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"type","date")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[6]),v(e,"input",t[7])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function Ft(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"type","time")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[8]),v(e,"input",t[9])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function Ut(e){let n,s,r,l,o,c,i,a,u,d=(e[0].descr?e[0].descr:"")+"",m="number"==e[0].type&&Dt(e),$="text"==e[0].type&&It(e),w="date"==e[0].type&&Rt(e),v="time"==e[0].type&&Ft(e);return{c(){n=h("div"),s=h("div"),r=h("label"),l=x(d),o=b(),c=h("div"),m&&m.c(),i=b(),$&&$.c(),a=b(),w&&w.c(),u=b(),v&&v.c(),y(r,"class","wgt-dscr-stl"),y(s,"class","w-2/3"),y(c,"class","flex justify-end w-1/3"),y(n,"class","crd-itm-psn")},m(t,e){p(t,n,e),f(n,s),f(s,r),f(r,l),f(n,o),f(n,c),m&&m.m(c,null),f(c,i),$&&$.m(c,null),f(c,a),w&&w.m(c,null),f(c,u),v&&v.m(c,null)},p(t,[e]){1&e&&d!==(d=(t[0].descr?t[0].descr:"")+"")&&_(l,d),"number"==t[0].type?m?m.p(t,e):(m=Dt(t),m.c(),m.m(c,i)):m&&(m.d(1),m=null),"text"==t[0].type?$?$.p(t,e):($=It(t),$.c(),$.m(c,a)):$&&($.d(1),$=null),"date"==t[0].type?w?w.p(t,e):(w=Rt(t),w.c(),w.m(c,u)):w&&(w.d(1),w=null),"time"==t[0].type?v?v.p(t,e):(v=Ft(t),v.c(),v.m(c,null)):v&&(v.d(1),v=null)},i:t,o:t,d(t){t&&g(n),m&&m.d(),$&&$.d(),w&&w.d(),v&&v.d()}}}function Wt(t,e,n){let{widget:s}=e,{wsPush:r=((t,e,n)=>{})}=e;return t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"wsPush"in t&&n(1,r=t.wsPush)},[s,r,()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=k(this.value),n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=this.value,n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=this.value,n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=this.value,n(0,s)}]}class Vt extends rt{constructor(t){super(),st(this,t,Wt,Ut,l,{widget:0,wsPush:1})}}function Yt(e){let n,r,l,o,c,i,a,u,d,m,$,w=(e[0].descr?e[0].descr:"")+"",k=e[0].after+"";return{c(){n=h("label"),r=x(w),l=b(),o=x(e[1]),c=b(),i=x(k),a=b(),u=h("input"),y(n,"class","wgt-dscr-stl"),y(u,"class",d="form-range range-secondary w-full h-2 p-0 rounded-lg "+(e[0].sent?"bg-red-300":"bg-gray-300")+" focus:outline-none appearance-none"),y(u,"type","range"),y(u,"min","0"),y(u,"max","1024")},m(t,s){p(t,n,s),f(n,r),f(n,l),f(n,o),f(n,c),f(n,i),p(t,a,s),p(t,u,s),J(u,e[0].status),m||($=[v(u,"change",e[3]),v(u,"input",e[3]),v(u,"change",e[4])],m=!0)},p(t,[e]){1&e&&w!==(w=(t[0].descr?t[0].descr:"")+"")&&_(r,w),2&e&&_(o,t[1]),1&e&&k!==(k=t[0].after+"")&&_(i,k),1&e&&d!==(d="form-range range-secondary w-full h-2 p-0 rounded-lg "+(t[0].sent?"bg-red-300":"bg-gray-300")+" focus:outline-none appearance-none")&&y(u,"class",d),1&e&&J(u,t[0].status)},i:t,o:t,d(t){t&&g(n),t&&g(a),t&&g(u),m=!1,s($)}}}function Kt(t,e,n){let{widget:s}=e,{wsPush:r=((t,e,n)=>{})}=e,{val:l=0}=e;function o(){n(1,l=function(t,e,n,s,r){return Math.round((t-e)*(r-s)/(n-e)+s)}(s.status,0,1024,s.min,s.max))}return t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"wsPush"in t&&n(2,r=t.wsPush),"val"in t&&n(1,l=t.val)},t.$$.update=()=>{1&t.$$.dirty&&(s.status,o())},[s,l,r,function(){s.status=k(this.value),n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status))]}class Qt extends rt{constructor(t){super(),st(this,t,Kt,Yt,l,{widget:0,wsPush:2,val:1})}}function Gt(e){let n,r,l,o,c,i,a,u,d,m,$,w,k,J,j,S,T,M,L,q=(e[0].descr?e[0].descr:"")+"";return{c(){n=h("div"),r=h("div"),l=h("label"),o=x(q),c=b(),i=h("div"),a=h("label"),u=h("div"),d=h("input"),$=b(),w=h("div"),J=b(),j=h("div"),y(l,"class","wgt-dscr-stl"),y(r,"class","w-2/3"),y(d,"id",m=e[0].topic),y(d,"type","checkbox"),y(d,"class","sr-only"),y(w,"class",k="block "+(e[1]?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg"),y(j,"class",S="dot "+(e[0].sent?"bg-red-300":"bg-gray-100")+" absolute left-1 top-1 w-4 h-4 rounded-full transition shadow-lg"),y(u,"class","relative"),y(a,"for",T=e[0].topic),y(a,"class","items-center cursor-pointer"),y(i,"class","flex justify-end w-1/3"),y(n,"class","crd-itm-psn")},m(t,s){p(t,n,s),f(n,r),f(r,l),f(l,o),f(n,c),f(n,i),f(i,a),f(a,u),f(u,d),d.checked=e[1],f(u,$),f(u,w),f(u,J),f(u,j),M||(L=[v(d,"change",e[4]),v(d,"change",e[5])],M=!0)},p(t,[e]){1&e&&q!==(q=(t[0].descr?t[0].descr:"")+"")&&_(o,q),1&e&&m!==(m=t[0].topic)&&y(d,"id",m),2&e&&(d.checked=t[1]),2&e&&k!==(k="block "+(t[1]?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg")&&y(w,"class",k),1&e&&S!==(S="dot "+(t[0].sent?"bg-red-300":"bg-gray-100")+" absolute left-1 top-1 w-4 h-4 rounded-full transition shadow-lg")&&y(j,"class",S),1&e&&T!==(T=t[0].topic)&&y(a,"for",T)},i:t,o:t,d(t){t&&g(n),M=!1,s(L)}}}function Zt(t,e,n){let{widget:s}=e,{toggleState:r=!1}=e,{wsPush:l=((t,e,n)=>{})}=e;function o(){n(0,s.sent=!0,s),n(0,s.status=r?"1":"0",s)}return t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"toggleState"in t&&n(1,r=t.toggleState),"wsPush"in t&&n(2,l=t.wsPush)},t.$$.update=()=>{1&t.$$.dirty&&(s.status,"1"==s.status?n(1,r=!0):"0"==s.status&&n(1,r=!1))},[s,r,l,o,function(){r=this.checked,n(1,r)},()=>(o(),l(s.ws,s.topic,s.status))]}class Xt extends rt{constructor(t){super(),st(this,t,Zt,Gt,l,{widget:0,toggleState:1,wsPush:2})}}function te(e){let n,s,r,l,o,c,i,a,u,d,m,$,w=(e[0].descr?e[0].descr:"")+"",v=(e[0].status?e[0].status:"")+"",k=(e[0].after?e[0].after:"")+"";return{c(){n=h("div"),s=h("div"),r=h("label"),l=x(w),o=b(),c=h("div"),i=h("label"),a=x(v),u=b(),d=h("label"),m=x(" "),$=x(k),y(r,"class","wgt-dscr-stl"),y(s,"class","w-2/3"),y(i,"class","wgt-adt-stl"),y(d,"class","wgt-adt-stl"),y(c,"class","flex justify-end w-1/3"),y(n,"class","crd-itm-psn")},m(t,e){p(t,n,e),f(n,s),f(s,r),f(r,l),f(n,o),f(n,c),f(c,i),f(i,a),f(c,u),f(c,d),f(d,m),f(d,$)},p(t,[e]){1&e&&w!==(w=(t[0].descr?t[0].descr:"")+"")&&_(l,w),1&e&&v!==(v=(t[0].status?t[0].status:"")+"")&&_(a,v),1&e&&k!==(k=(t[0].after?t[0].after:"")+"")&&_($,k)},i:t,o:t,d(t){t&&g(n)}}}function ee(t,e,n){let{widget:s}=e,{value:r}=e;return r=r,t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"value"in t&&n(1,r=t.value)},[s,r]}class ne extends rt{constructor(t){super(),st(this,t,ee,te,l,{widget:0,value:1})}}function se(t,e,n){const s=t.slice();return s[11]=e[n],s[13]=n,s}function re(t,e,n){const s=t.slice();return s[14]=e[n],s[15]=e,s[16]=n,s}function le(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function oe(t){let e,n,s,r=t[0]===[]&&ce(),l=t[1],o=[];for(let e=0;eZ(o[t],1,1,(()=>{o[t]=null}));return{c(){e=h("div"),r&&r.c(),n=b();for(let t=0;t{r=null})),Q()),11&s){let n;for(l=t[1],n=0;n{o=null})),Q()),"toggle"===t[14].widget?c?(c.p(t,l),1&l&&G(c,1)):(c=ue(t),c.c(),G(c,1),c.m(n.parentNode,n)):c&&(K(),Z(c,1,1,(()=>{c=null})),Q()),"anydata"===t[14].widget?i?(i.p(t,l),1&l&&G(i,1)):(i=de(t),i.c(),G(i,1),i.m(s.parentNode,s)):i&&(K(),Z(i,1,1,(()=>{i=null})),Q()),"range"===t[14].widget?a?(a.p(t,l),1&l&&G(a,1)):(a=fe(t),a.c(),G(a,1),a.m(r.parentNode,r)):a&&(K(),Z(a,1,1,(()=>{a=null})),Q())},i(t){l||(G(o),G(c),G(i),G(a),l=!0)},o(t){Z(o),Z(c),Z(i),Z(a),l=!1},d(t){o&&o.d(t),t&&g(e),c&&c.d(t),t&&g(n),i&&i.d(t),t&&g(s),a&&a.d(t),t&&g(r)}}}function ae(t){let e,n,s;function r(e){t[5](e,t[14])}let l={widget:t[14],wsPush:t[4]};return void 0!==t[14].status&&(l.value=t[14].status),e=new Vt({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),8&r&&(l.wsPush=t[4]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function ue(t){let e,n,s;function r(e){t[7](e,t[14])}let l={widget:t[14],wsPush:t[6]};return void 0!==t[14].status&&(l.value=t[14].status),e=new Xt({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),8&r&&(l.wsPush=t[6]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function de(t){let e,n,s;function r(e){t[8](e,t[14])}let l={widget:t[14]};return void 0!==t[14].status&&(l.value=t[14].status),e=new ne({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function fe(t){let e,n,s;function r(e){t[10](e,t[14])}let l={widget:t[14],wsPush:t[9]};return void 0!==t[14].status&&(l.value=t[14].status),e=new Qt({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),8&r&&(l.wsPush=t[9]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function pe(t){let e,n,s=t[14].page===t[11].page&&ie(t);return{c(){s&&s.c(),e=w()},m(t,r){s&&s.m(t,r),p(t,e,r),n=!0},p(t,n){t[14].page===t[11].page?s?(s.p(t,n),3&n&&G(s,1)):(s=ie(t),s.c(),G(s,1),s.m(e.parentNode,e)):s&&(K(),Z(s,1,1,(()=>{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function ge(t){let e,n,s=t[0],r=[];for(let e=0;eZ(r[t],1,1,(()=>{r[t]=null}));return{c(){for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function $e(t,e,n){let{layoutJson:s}=e,{pages:r}=e,{show:l}=e,{wsPush:o=((t,e,n)=>{})}=e;return t.$$set=t=>{"layoutJson"in t&&n(0,s=t.layoutJson),"pages"in t&&n(1,r=t.pages),"show"in t&&n(2,l=t.show),"wsPush"in t&&n(3,o=t.wsPush)},[s,r,l,o,(t,e,n)=>o(t,e,n),function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))},(t,e,n)=>o(t,e,n),function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))},function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))},(t,e,n)=>o(t,e,n),function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))}]}class xe extends rt{constructor(t){super(),st(this,t,$e,he,l,{layoutJson:0,pages:1,show:2,wsPush:3})}}function be(e){let n,s,l,o,c,i,a;return{c(){n=$("svg"),s=$("path"),l=$("circle"),o=$("circle"),c=$("circle"),y(s,"stroke","none"),y(s,"d","M0 0h24v24H0z"),y(l,"cx","5"),y(l,"cy","12"),y(l,"r","1"),y(o,"cx","12"),y(o,"cy","12"),y(o,"r","1"),y(c,"cx","19"),y(c,"cy","12"),y(c,"r","1"),y(n,"class","h-6 w-6 text-green-400 cursor-pointer"),y(n,"width","24"),y(n,"height","24"),y(n,"viewBox","0 -2 24 24"),y(n,"stroke-width","2"),y(n,"stroke","currentColor"),y(n,"fill","none"),y(n,"stroke-linecap","round"),y(n,"stroke-linejoin","round")},m(t,u){p(t,n,u),f(n,s),f(n,l),f(n,o),f(n,c),i||(a=v(n,"click",(function(){r(e[0]())&&e[0]().apply(this,arguments)})),i=!0)},p(t,[n]){e=t},i:t,o:t,d(t){t&&g(n),i=!1,a()}}}function we(t,e,n){let{click:s=(()=>{})}=e;return t.$$set=t=>{"click"in t&&n(0,s=t.click)},[s]}class ve extends rt{constructor(t){super(),st(this,t,we,be,l,{click:0})}}function ye(t,e,n){const s=t.slice();return s[25]=e[n],s[26]=e,s[27]=n,s}function ke(t,e,n){const s=t.slice();return s[28]=e[n][0],s[29]=e[n][1],s[30]=e,s[31]=n,s}function _e(t,e,n){const s=t.slice();return s[32]=e[n],s}function Je(t,e,n){const s=t.slice();return s[35]=e[n],s}function je(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function Se(t){let e,n,s,r,l,o,c,i;return n=new zt({props:{title:"Конфигуратор",$$slots:{default:[He]},$$scope:{ctx:t}}}),r=new zt({props:{title:"Сценарии",$$slots:{default:[Pe]},$$scope:{ctx:t}}}),c=new zt({props:{$$slots:{default:[Ae]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),l=b(),o=h("div"),tt(c.$$.fragment),y(e,"class","grd-2col1"),y(o,"class","grd-1col1")},m(t,a){p(t,e,a),et(n,e,null),f(e,s),et(r,e,null),p(t,l,a),p(t,o,a),et(c,o,null),i=!0},p(t,e){const s={};398&e[0]|128&e[1]&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};513&e[0]|128&e[1]&&(l.$$scope={dirty:e,ctx:t}),r.$set(l);const o={};96&e[0]|128&e[1]&&(o.$$scope={dirty:e,ctx:t}),c.$set(o)},i(t){i||(G(n.$$.fragment,t),G(r.$$.fragment,t),G(c.$$.fragment,t),i=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),Z(c.$$.fragment,t),i=!1},d(t){t&&g(e),nt(n),nt(r),t&&g(l),t&&g(o),nt(c)}}}function Te(t){let e,n;return{c(){e=h("optgroup"),y(e,"label",n=t[35].header)},m(t,n){p(t,e,n)},p(t,s){8&s[0]&&n!==(n=t[35].header)&&y(e,"label",n)},d(t){t&&g(e)}}}function Me(t){let e,n,s,r,l=t[35].name+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[35].num,e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){8&s[0]&&l!==(l=t[35].name+"")&&_(n,l),8&s[0]&&r!==(r=t[35].num)&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function Le(t){let e,n,s=t[35].header&&Te(t),r=!t[35].header&&Me(t);return{c(){s&&s.c(),e=w(),r&&r.c(),n=w()},m(t,l){s&&s.m(t,l),p(t,e,l),r&&r.m(t,l),p(t,n,l)},p(t,l){t[35].header?s?s.p(t,l):(s=Te(t),s.c(),s.m(e.parentNode,e)):s&&(s.d(1),s=null),t[35].header?r&&(r.d(1),r=null):r?r.p(t,l):(r=Me(t),r.c(),r.m(n.parentNode,n))},d(t){s&&s.d(t),t&&g(e),r&&r.d(t),t&&g(n)}}}function qe(t){let e,n,s,r,l=t[32].label+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[32].name,e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){4&s[0]&&l!==(l=t[32].label+"")&&_(n,l),4&s[0]&&r!==(r=t[32].name)&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function Ee(t){let e,n=Object.entries(t[25]),s=[];for(let e=0;eZ(S[t],1,1,(()=>{S[t]=null}));return{c(){e=h("div"),n=h("select");for(let t=0;t<_.length;t+=1)_[t].c();r=b(),l=h("select"),o=h("option"),o.textContent="Выберите пресет",c=b(),i=h("table"),a=h("thead"),a.innerHTML='Тип \n Id \n Виджет \n Вкладка \n Название \n \n ',u=b(),d=h("tbody");for(let t=0;tt[12].call(n))),o.__value="Выберите пресет",o.value=o.__value,y(l,"class","slct-lg"),y(e,"class","grd-2col2"),y(a,"class","bg-gray-100"),y(d,"class","bg-white"),y(i,"class","tbl")},m(s,g){p(s,e,g),f(e,n);for(let t=0;t<_.length;t+=1)_[t].m(n,null);j(n,t[7]),f(e,r),f(e,l),f(l,o),p(s,c,g),p(s,i,g),f(i,a),f(i,u),f(i,d);for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function ze(t,e,n){let s,{configJson:r}=e,{widgetsJson:l}=e,{itemsJson:o}=e,{show:c}=e,{scenarioTxt:i}=e,a=0,{saveConfig:u=(()=>{})}=e,{rebootEsp:d=(()=>{})}=e,f=!0;function p(){for(let t=0;t{"configJson"in t&&n(1,r=t.configJson),"widgetsJson"in t&&n(2,l=t.widgetsJson),"itemsJson"in t&&n(3,o=t.itemsJson),"show"in t&&n(4,c=t.show),"scenarioTxt"in t&&n(0,i=t.scenarioTxt),"saveConfig"in t&&n(5,u=t.saveConfig),"rebootEsp"in t&&n(6,d=t.rebootEsp)},t.$$.update=()=>{1&t.$$.dirty[0]&&n(9,s=Math.round(i.split("\n").length)+1)},[i,r,l,o,c,u,d,a,f,s,p,g,function(){a=S(this),n(7,a),n(3,o)},()=>p(),function(t,e){t[e].id=this.value,n(1,r),n(2,l)},function(t,e){t[e].widget=S(this),n(1,r),n(2,l)},function(t,e){t[e].page=this.value,n(1,r),n(2,l)},function(t,e){t[e].descr=this.value,n(1,r),n(2,l)},()=>n(8,f=!f),t=>g(t),function(t,e,s){e[s][t]=this.value,n(1,r),n(2,l)},function(){i=this.value,n(0,i)},()=>u(),()=>d()]}class De extends rt{constructor(t){super(),st(this,t,ze,Be,l,{configJson:1,widgetsJson:2,itemsJson:3,show:4,scenarioTxt:0,saveConfig:5,rebootEsp:6},null,[-1,-1])}}function Ie(t,e,n){const s=t.slice();return s[23]=e[n][0],s[24]=e[n][1],s}function Re(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function Fe(t){let e,n,s,r,l,o,c,i;return n=new zt({props:{title:"Подключение к WiFi",$$slots:{default:[Ve]},$$scope:{ctx:t}}}),r=new zt({props:{title:"Подключение к MQTT",$$slots:{default:[Ze]},$$scope:{ctx:t}}}),c=new zt({props:{$$slots:{default:[Xe]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),l=b(),o=h("div"),tt(c.$$.fragment),y(e,"class","grd-2col1"),y(o,"class","grd-1col1")},m(t,a){p(t,e,a),et(n,e,null),f(e,s),et(r,e,null),p(t,l,a),p(t,o,a),et(c,o,null),i=!0},p(t,e){const s={};134217783&e&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};134217795&e&&(l.$$scope={dirty:e,ctx:t}),r.$set(l);const o={};134217856&e&&(o.$$scope={dirty:e,ctx:t}),c.$set(o)},i(t){i||(G(n.$$.fragment,t),G(r.$$.fragment,t),G(c.$$.fragment,t),i=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),Z(c.$$.fragment,t),i=!1},d(t){t&&g(e),nt(n),nt(r),t&&g(l),t&&g(o),nt(c)}}}function Ue(t){let e,n,s,r,l=t[24]+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[24],e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){4&s&&l!==(l=t[24]+"")&&_(n,l),4&s&&r!==(r=t[24])&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function We(t){let e,n,s;return n=new Ct({props:{title:"Введен неправильный пароль"}}),{c(){e=h("div"),tt(n.$$.fragment),y(e,"class","grd-1col1")},m(t,r){p(t,e,r),et(n,e,null),s=!0},i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){t&&g(e),nt(n)}}}function Ve(t){let e,n,r,l,o,c,i,a,u,d,$,x,w,k,_,S,T,M,L,q,E,C,O,N,H,P,A,B,z,I,R,F,U,W,V,Y,X,tt,et,nt,st,rt=Object.entries(t[2]),lt=[];for(let e=0;eНазвание устройства

',r=b(),l=h("div"),o=h("input"),c=b(),i=h("div"),a=h("div"),a.innerHTML='

Точка доступа

',u=b(),d=h("div"),$=h("input"),x=b(),w=h("div"),k=h("div"),k.innerHTML='

Пароль точки доступа

',_=b(),S=h("div"),T=h("input"),M=b(),L=h("div"),q=h("div"),q.innerHTML='

Название wifi сети

',E=b(),C=h("div"),O=h("select");for(let t=0;tПароль

',A=b(),B=h("div"),z=h("input"),I=b(),R=h("div"),F=h("div"),F.innerHTML='

Сервер обновления

',U=b(),W=h("div"),V=h("input"),Y=b(),ot&&ot.c(),X=b(),tt=h("button"),tt.textContent="Сохранить",y(n,"class","w-4/6"),y(o,"class","ipt-rnd text-left focus:border-indigo-500"),y(o,"type","text"),y(l,"class","flex justify-end w-3/6"),y(e,"class","crd-itm-psn"),y(a,"class","w-4/6"),y($,"class","ipt-rnd text-left focus:border-indigo-500"),y($,"type","text"),y(d,"class","flex justify-end w-3/6"),y(i,"class","crd-itm-psn"),y(k,"class","w-4/6"),y(T,"class","ipt-rnd text-left focus:border-indigo-500"),y(T,"type","text"),y(S,"class","flex justify-end w-3/6"),y(w,"class","crd-itm-psn"),y(q,"class","w-4/6"),y(O,"class","ipt-rnd text-left focus:border-indigo-500"),void 0===t[0].routerssid&&D((()=>t[11].call(O))),y(C,"class","flex justify-end w-3/6"),y(L,"class","crd-itm-psn"),y(P,"class","w-4/6"),y(z,"class","ipt-rnd text-left focus:border-indigo-500"),y(z,"type","text"),y(B,"class","flex justify-end w-3/6"),y(H,"class","crd-itm-psn"),y(F,"class","w-4/6"),y(V,"class","ipt-rnd text-left focus:border-indigo-500"),y(V,"type","text"),y(W,"class","flex justify-end w-3/6"),y(R,"class","crd-itm-psn"),y(tt,"class","btn-lg")},m(s,g){p(s,e,g),f(e,n),f(e,r),f(e,l),f(l,o),J(o,t[0].name),p(s,c,g),p(s,i,g),f(i,a),f(i,u),f(i,d),f(d,$),J($,t[0].apssid),p(s,x,g),p(s,w,g),f(w,k),f(w,_),f(w,S),f(S,T),J(T,t[0].appass),p(s,M,g),p(s,L,g),f(L,q),f(L,E),f(L,C),f(C,O);for(let t=0;t{ot=null})),Q())},i(t){et||(G(ot),et=!0)},o(t){Z(ot),et=!1},d(t){t&&g(e),t&&g(c),t&&g(i),t&&g(x),t&&g(w),t&&g(M),t&&g(L),m(lt,t),t&&g(N),t&&g(H),t&&g(I),t&&g(R),t&&g(Y),ot&&ot.d(t),t&&g(X),t&&g(tt),nt=!1,s(st)}}}function Ye(t){let e;return{c(){e=h("p"),e.textContent="Ошибка",y(e,"class","text-red-500 font-bold h-8 bg-red-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Ke(t){let e;return{c(){e=h("p"),e.textContent="Ожидание",y(e,"class","text-blue-500 font-bold h-8 bg-blue-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Qe(t){let e;return{c(){e=h("p"),e.textContent="Подключение",y(e,"class","text-yellow-500 font-bold h-8 bg-yellow-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Ge(t){let e;return{c(){e=h("p"),e.textContent="Подключено",y(e,"class","text-green-500 font-bold h-8 bg-green-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Ze(t){let e,n,r,l,o,c,i,a,u,d,m,$,x,w,k,_,j,S,T,M,L,q,E,C,O,N,H,P,A,B,z,D,I,R,F,U,W,V;function Y(t,e){return"e5"===t[1].mqtt?Ge:"e13"===t[1].mqtt?Qe:void 0===t[1].mqtt?Ke:Ye}let K=Y(t),Q=K(t);return{c(){e=h("div"),n=h("div"),n.innerHTML='

Состояние подключения

',r=b(),l=h("div"),Q.c(),o=b(),c=h("div"),i=h("div"),i.innerHTML='

Название сервера

',a=b(),u=h("div"),d=h("input"),m=b(),$=h("div"),x=h("div"),x.innerHTML='

Порт

',w=b(),k=h("div"),_=h("input"),j=b(),S=h("div"),T=h("div"),T.innerHTML='

Префикс

',M=b(),L=h("div"),q=h("input"),E=b(),C=h("div"),O=h("div"),O.innerHTML='

Имя пользователя

',N=b(),H=h("div"),P=h("input"),A=b(),B=h("div"),z=h("div"),z.innerHTML='

Пароль

',D=b(),I=h("div"),R=h("input"),F=b(),U=h("button"),U.textContent="Сохранить",y(n,"class","w-4/6"),y(l,"class","flex justify-center w-3/6 align-baseline text-sm sm:text-sm md:text-base lg:text-base xl:text-base 2xl:text-base break-words"),y(e,"class","crd-itm-psn"),y(i,"class","w-4/6"),y(d,"class","ipt-rnd text-left focus:border-indigo-500"),y(d,"type","text"),y(u,"class","flex justify-end w-3/6"),y(c,"class","crd-itm-psn"),y(x,"class","w-4/6"),y(_,"class","ipt-rnd text-left focus:border-indigo-500"),y(_,"type","text"),y(k,"class","flex justify-end w-3/6"),y($,"class","crd-itm-psn"),y(T,"class","w-4/6"),y(q,"class","ipt-rnd text-left focus:border-indigo-500"),y(q,"type","text"),y(L,"class","flex justify-end w-3/6"),y(S,"class","crd-itm-psn"),y(O,"class","w-4/6"),y(P,"class","ipt-rnd text-left focus:border-indigo-500"),y(P,"type","text"),y(H,"class","flex justify-end w-3/6"),y(C,"class","crd-itm-psn"),y(z,"class","w-4/6"),y(R,"class","ipt-rnd text-left focus:border-indigo-500"),y(R,"type","text"),y(I,"class","flex justify-end w-3/6"),y(B,"class","crd-itm-psn"),y(U,"class","btn-lg")},m(s,g){p(s,e,g),f(e,n),f(e,r),f(e,l),Q.m(l,null),p(s,o,g),p(s,c,g),f(c,i),f(c,a),f(c,u),f(u,d),J(d,t[0].mqttServer),p(s,m,g),p(s,$,g),f($,x),f($,w),f($,k),f(k,_),J(_,t[0].mqttPort),p(s,j,g),p(s,S,g),f(S,T),f(S,M),f(S,L),f(L,q),J(q,t[0].mqttPrefix),p(s,E,g),p(s,C,g),f(C,O),f(C,N),f(C,H),f(H,P),J(P,t[0].mqttUser),p(s,A,g),p(s,B,g),f(B,z),f(B,D),f(B,I),f(I,R),J(R,t[0].mqttPass),p(s,F,g),p(s,U,g),W||(V=[v(d,"input",t[16]),v(_,"input",t[17]),v(q,"input",t[18]),v(P,"input",t[19]),v(R,"input",t[20]),v(U,"click",t[21])],W=!0)},p(t,e){K!==(K=Y(t))&&(Q.d(1),Q=K(t),Q&&(Q.c(),Q.m(l,null))),5&e&&d.value!==t[0].mqttServer&&J(d,t[0].mqttServer),5&e&&_.value!==t[0].mqttPort&&J(_,t[0].mqttPort),5&e&&q.value!==t[0].mqttPrefix&&J(q,t[0].mqttPrefix),5&e&&P.value!==t[0].mqttUser&&J(P,t[0].mqttUser),5&e&&R.value!==t[0].mqttPass&&J(R,t[0].mqttPass)},d(t){t&&g(e),Q.d(),t&&g(o),t&&g(c),t&&g(m),t&&g($),t&&g(j),t&&g(S),t&&g(E),t&&g(C),t&&g(A),t&&g(B),t&&g(F),t&&g(U),W=!1,s(V)}}}function Xe(e){let n,s,r;return{c(){n=h("button"),n.textContent="Перезагрузить устройство",y(n,"class","btn-lg")},m(t,l){p(t,n,l),s||(r=v(n,"click",e[22]),s=!0)},p:t,d(t){t&&g(n),s=!1,r()}}}function tn(t){let e,n,s,r;const l=[Fe,Re],o=[];function c(t,e){return t[3]?0:1}return e=c(t),n=o[e]=l[e](t),{c(){n.c(),s=w()},m(t,n){o[e].m(t,n),p(t,s,n),r=!0},p(t,[r]){let i=e;e=c(t),e===i?o[e].p(t,r):(K(),Z(o[i],1,1,(()=>{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function en(t,e,n){let{settingsJson:s}=e,{errorsJson:r}=e,{ssidJson:l}=e,{show:o}=e,{ssidClick:c=(()=>{})}=e,{saveSett:i=(()=>{})}=e,{saveMqtt:a=(()=>{})}=e,{rebootEsp:u=(()=>{})}=e;return t.$$set=t=>{"settingsJson"in t&&n(0,s=t.settingsJson),"errorsJson"in t&&n(1,r=t.errorsJson),"ssidJson"in t&&n(2,l=t.ssidJson),"show"in t&&n(3,o=t.show),"ssidClick"in t&&n(4,c=t.ssidClick),"saveSett"in t&&n(5,i=t.saveSett),"saveMqtt"in t&&n(6,a=t.saveMqtt),"rebootEsp"in t&&n(7,u=t.rebootEsp)},[s,r,l,o,c,i,a,u,function(){s.name=this.value,n(0,s),n(2,l)},function(){s.apssid=this.value,n(0,s),n(2,l)},function(){s.appass=this.value,n(0,s),n(2,l)},function(){s.routerssid=S(this),n(0,s),n(2,l)},()=>c(),function(){s.routerpass=this.value,n(0,s),n(2,l)},function(){s.serverip=this.value,n(0,s),n(2,l)},()=>i(),function(){s.mqttServer=this.value,n(0,s),n(2,l)},function(){s.mqttPort=this.value,n(0,s),n(2,l)},function(){s.mqttPrefix=this.value,n(0,s),n(2,l)},function(){s.mqttUser=this.value,n(0,s),n(2,l)},function(){s.mqttPass=this.value,n(0,s),n(2,l)},()=>a(),()=>u()]}class nn extends rt{constructor(t){super(),st(this,t,en,tn,l,{settingsJson:0,errorsJson:1,ssidJson:2,show:3,ssidClick:4,saveSett:5,saveMqtt:6,rebootEsp:7})}}function sn(t,e,n){const s=t.slice();return s[13]=e[n],s[15]=n,s}function rn(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function ln(t){let e,n,s,r,l;return n=new zt({props:{title:"Список устройств",$$slots:{default:[an]},$$scope:{ctx:t}}}),r=new Ct({props:{$$slots:{default:[un]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),y(e,"class","grd-1col1")},m(t,o){p(t,e,o),et(n,e,null),f(e,s),et(r,e,null),l=!0},p(t,e){const s={};65591&e&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};65536&e&&(l.$$scope={dirty:e,ctx:t}),r.$set(l)},i(t){l||(G(n.$$.fragment,t),G(r.$$.fragment,t),l=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),l=!1},d(t){t&&g(e),nt(n),nt(r)}}}function on(t){let e,n,s,r,l,o,c,i,a,u,d,m,$,w,v,k,J,j,S,T=t[13].name+"",M=t[13].ip+"",L=t[13].id+"",q=t[13].status?"online":"offline";return j=new Tt({props:{click:function(){return t[7](t[15])}}}),{c(){e=h("tr"),n=h("td"),s=x(T),r=b(),l=h("td"),o=h("a"),c=x(M),a=b(),u=h("td"),d=x(L),m=b(),$=h("td"),w=x(q),k=b(),J=h("td"),tt(j.$$.fragment),y(n,"class","tbl-bdy-lg ipt-lg w-full"),y(o,"href",i="http://"+t[13].ip),y(l,"class","tbl-bdy-lg ipt-lg w-full"),y(u,"class","tbl-bdy-lg ipt-lg w-full"),y($,"class",v="tbl-bdy-lg ipt-lg w-full "+(t[13].status?"bg-green-50":"bg-red-50")),y(J,"class","tbl-bdy-lg"),y(e,"class","txt-sz txt-pad")},m(t,i){p(t,e,i),f(e,n),f(n,s),f(e,r),f(e,l),f(l,o),f(o,c),f(e,a),f(e,u),f(u,d),f(e,m),f(e,$),f($,w),f(e,k),f(e,J),et(j,J,null),S=!0},p(e,n){t=e,(!S||1&n)&&T!==(T=t[13].name+"")&&_(s,T),(!S||1&n)&&M!==(M=t[13].ip+"")&&_(c,M),(!S||1&n&&i!==(i="http://"+t[13].ip))&&y(o,"href",i),(!S||1&n)&&L!==(L=t[13].id+"")&&_(d,L),(!S||1&n)&&q!==(q=t[13].status?"online":"offline")&&_(w,q),(!S||1&n&&v!==(v="tbl-bdy-lg ipt-lg w-full "+(t[13].status?"bg-green-50":"bg-red-50")))&&y($,"class",v)},i(t){S||(G(j.$$.fragment,t),S=!0)},o(t){Z(j.$$.fragment,t),S=!1},d(t){t&&g(e),nt(j)}}}function cn(t){let e,n,r,l,o,c,i,a,u,d,m,$,x;return{c(){e=h("tr"),n=h("td"),r=h("input"),l=b(),o=h("td"),c=h("input"),i=b(),a=h("td"),u=h("input"),d=b(),m=h("td"),y(r,"class","ipt-lg w-full"),y(r,"type","text"),y(n,"class","tbl-bdy-lg"),y(c,"class","ipt-lg w-full"),y(c,"type","text"),y(o,"class","tbl-bdy-lg"),y(u,"class","ipt-lg w-full"),y(u,"type","text"),y(a,"class","tbl-bdy-lg"),y(m,"class","tbl-bdy-lg"),y(e,"class","txt-sz txt-pad")},m(s,g){p(s,e,g),f(e,n),f(n,r),J(r,t[2].name),f(e,l),f(e,o),f(o,c),J(c,t[2].ip),f(e,i),f(e,a),f(a,u),J(u,t[2].id),f(e,d),f(e,m),$||(x=[v(r,"input",t[8]),v(c,"input",t[9]),v(u,"input",t[10])],$=!0)},p(t,e){4&e&&r.value!==t[2].name&&J(r,t[2].name),4&e&&c.value!==t[2].ip&&J(c,t[2].ip),4&e&&u.value!==t[2].id&&J(u,t[2].id)},d(t){t&&g(e),$=!1,s(x)}}}function an(t){let e,n,r,l,o,c,i,a,u,d,$,w,k,J,j=t[1]?"Сохранить":"Добавить устройство",S=t[0],T=[];for(let e=0;eZ(T[t],1,1,(()=>{T[t]=null}));let L=t[1]&&cn(t);return{c(){e=h("table"),n=h("thead"),n.innerHTML='Название устройства \n IP адрес \n Идентификатор \n Состояние \n ',r=b(),l=h("tbody");for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function fn(t,e,n){let{show:s}=e,{deviceList:r}=e,{showInput:l}=e,{newDevice:o={}}=e,{addDevInList:c=(()=>{})}=e,{sendToAllDevices:i=(t=>{})}=e;function a(t){for(let e=0;e{"show"in t&&n(3,s=t.show),"deviceList"in t&&n(0,r=t.deviceList),"showInput"in t&&n(1,l=t.showInput),"newDevice"in t&&n(2,o=t.newDevice),"addDevInList"in t&&n(4,c=t.addDevInList),"sendToAllDevices"in t&&n(5,i=t.sendToAllDevices)},[r,l,o,s,c,i,a,t=>a(t),function(){o.name=this.value,n(2,o)},function(){o.ip=this.value,n(2,o)},function(){o.id=this.value,n(2,o)},()=>(n(1,l=!l),c()),t=>i("/reboot|")]}class pn extends rt{constructor(t){super(),st(this,t,fn,dn,l,{show:3,deviceList:0,showInput:1,newDevice:2,addDevInList:4,sendToAllDevices:5})}}function gn(t,e,n){const s=t.slice();return s[21]=e[n][0],s[22]=e[n][1],s[24]=n,s}function mn(t,e,n){const s=t.slice();return s[25]=e[n],s[24]=n,s}function hn(t,e,n){const s=t.slice();return s[21]=e[n][0],s[22]=e[n][1],s}function $n(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function xn(t){let e,n,s,r,l,o,c,i,a,u;return n=new zt({props:{title:"Системная информация",$$slots:{default:[Sn]},$$scope:{ctx:t}}}),r=new zt({props:{title:"Системные настройки",$$slots:{default:[Mn]},$$scope:{ctx:t}}}),o=new zt({props:{title:"Лог",class:"z-50",$$slots:{default:[qn]},$$scope:{ctx:t}}}),a=new zt({props:{title:"Системные ошибки",$$slots:{default:[Hn]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),l=b(),tt(o.$$.fragment),c=b(),i=h("div"),tt(a.$$.fragment),y(e,"class","grd-3col1"),y(i,"class","grd-1col1")},m(t,d){p(t,e,d),et(n,e,null),f(e,s),et(r,e,null),f(e,l),et(o,e,null),p(t,c,d),p(t,i,d),et(a,i,null),u=!0},p(t,e){const s={};536871001&e&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};536871046&e&&(l.$$scope={dirty:e,ctx:t}),r.$set(l);const c={};536870944&e&&(c.$$scope={dirty:e,ctx:t}),o.$set(c);const i={};536871432&e&&(i.$$scope={dirty:e,ctx:t}),a.$set(i)},i(t){u||(G(n.$$.fragment,t),G(r.$$.fragment,t),G(o.$$.fragment,t),G(a.$$.fragment,t),u=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),Z(o.$$.fragment,t),Z(a.$$.fragment,t),u=!1},d(t){t&&g(e),nt(n),nt(r),nt(o),t&&g(c),t&&g(i),nt(a)}}}function bn(t){let e,n,s,r,l=t[22]+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[22],e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){16&s&&l!==(l=t[22]+"")&&_(n,l),16&s&&r!==(r=t[22])&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function wn(t){let e;return{c(){e=h("p"),e.textContent="не подключено",y(e,"class","text-red-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function vn(t){let e;return{c(){e=h("p"),e.textContent="нет сигнала",y(e,"class","text-red-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function yn(t){let e;return{c(){e=h("p"),e.textContent="очень низкий",y(e,"class","text-red-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function kn(t){let e;return{c(){e=h("p"),e.textContent="низкий",y(e,"class","text-yellow-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function _n(t){let e;return{c(){e=h("p"),e.textContent="хороший",y(e,"class","text-yellow-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Jn(t){let e;return{c(){e=h("p"),e.textContent="очень хороший",y(e,"class","text-green-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function jn(t){let e;return{c(){e=h("p"),e.textContent="отличный",y(e,"class","text-green-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Sn(t){let e,n,r,l,o,c,i,a,u,d,$,w,k,J,S,T,M,L,q,E,C,O,N,H,P,A,B,z,I,R,F,U,W,V,Y,K,Q,G,Z,X,tt,et,nt,st,rt,lt,ot,ct,it,at,ut,dt,ft,pt,gt,mt,ht,$t,xt,bt,wt,vt,yt,kt,_t,Jt,jt,St,Tt,Mt,Lt,qt,Et,Ct,Ot,Nt,Ht,Pt,At,Bt,zt,Dt,It,Rt,Ft=t[3].bn+"",Ut=t[3].bver+"",Wt=t[3].timenow+"",Vt=t[3].upt+"",Yt=t[3].uptm+"",Kt=t[3].uptw+"",Qt=t[3].heap+"",Gt=t[3].fl+"",Zt=t[3].rst+"",Xt=Object.entries(t[4]),te=[];for(let e=0;eНазвание прошивки

',r=b(),l=h("div"),o=h("p"),c=x(Ft),i=b(),a=h("div"),u=h("div"),u.innerHTML='

Доступные версии

',d=b(),$=h("div"),w=h("select");for(let t=0;tВерсия прошивки

',T=b(),M=h("div"),L=h("p"),q=x(Ut),E=b(),C=h("div"),O=h("div"),O.innerHTML='

Время на устройстве

',N=b(),H=h("div"),P=h("p"),A=x(Wt),B=b(),z=h("div"),I=h("div"),I.innerHTML='

Uptime устройства

',R=b(),F=h("div"),U=h("p"),W=x(Vt),V=b(),Y=h("div"),K=h("div"),K.innerHTML='

Uptime сессии mqtt

',Q=b(),G=h("div"),Z=h("p"),X=x(Yt),tt=b(),et=h("div"),nt=h("div"),nt.innerHTML='

Uptime сессии wifi

',st=b(),rt=h("div"),lt=h("p"),ot=x(Kt),ct=b(),it=h("div"),at=h("div"),at.innerHTML='

Качество WiFi сигнала

',ut=b(),dt=h("div"),ee&&ee.c(),ft=b(),ne&&ne.c(),pt=b(),se&&se.c(),gt=b(),re&&re.c(),mt=b(),le&&le.c(),ht=b(),oe&&oe.c(),$t=b(),ce&&ce.c(),xt=b(),bt=h("div"),wt=h("div"),wt.innerHTML='

Остаток RAM

',vt=b(),yt=h("div"),kt=h("p"),_t=x(Qt),Jt=b(),jt=h("div"),St=h("div"),St.innerHTML='

Кол-во записей на flash

',Tt=b(),Mt=h("div"),Lt=h("p"),qt=x(Gt),Et=b(),Ct=h("div"),Ot=h("div"),Ot.innerHTML='

Причина перезагрузки

',Nt=b(),Ht=h("div"),Pt=h("p"),At=x(Zt),zt=b(),Dt=h("button"),Dt.textContent="Обновить прошивку",y(n,"class","w-2/3"),y(o,"class","text-gray-500 font-bold text-sm text-center truncate"),y(l,"class","flex justify-center w-1/3"),y(e,"class","flex mb-2 h-6 items-center"),y(u,"class","w-2/3"),y(w,"class","border border-indigo-500 border-4 text-center"),void 0===t[0]&&D((()=>t[12].call(w))),y($,"class","flex justify-center w-1/3"),y(a,"class","flex mb-2 h-6 items-center"),y(S,"class","w-2/3"),y(L,"class","text-gray-500 font-bold text-sm text-center truncate"),y(M,"class","flex justify-center w-1/3"),y(J,"class","flex mb-2 h-6 items-center"),y(O,"class","w-2/3"),y(P,"class","text-gray-500 font-bold text-sm text-center truncate"),y(H,"class","flex justify-center w-1/3"),y(C,"class","flex mb-2 h-6 items-center"),y(I,"class","w-2/3"),y(U,"class","text-gray-500 font-bold text-sm text-center truncate"),y(F,"class","flex justify-center w-1/3"),y(z,"class","flex mb-2 h-6 items-center"),y(K,"class","w-2/3"),y(Z,"class","text-gray-500 font-bold text-sm text-center truncate"),y(G,"class","flex justify-center w-1/3"),y(Y,"class","flex mb-2 h-6 items-center"),y(nt,"class","w-2/3"),y(lt,"class","text-gray-500 font-bold text-sm text-center truncate"),y(rt,"class","flex justify-center w-1/3"),y(et,"class","flex mb-2 h-6 items-center"),y(at,"class","w-2/3"),y(dt,"class","flex justify-center w-1/3 text-xs sm:text-sm md:text-base lg:text-base xl:text-base 2xl:text-base break-words"),y(it,"class","flex mb-2 h-6 items-center"),y(wt,"class","w-2/3"),y(kt,"class","text-green-500 font-bold text-center truncate"),y(yt,"class","flex justify-center w-1/3 text-sm text-center"),y(bt,"class","flex mb-2 h-6 items-center"),y(St,"class","w-2/3"),y(Lt,"class","text-green-500 font-bold text-center truncate"),y(Mt,"class","flex justify-center w-1/3 text-sm"),y(jt,"class","flex mb-2 h-6 items-center"),y(Ot,"class","w-2/3"),y(Pt,"class",Bt=(t[3].rst.toString().includes("Watchdog")||t[3].rst.toString().includes("Exception")?"text-red-500":"text-green-500")+" font-bold text-center truncate"),y(Ht,"class","flex justify-center w-1/3 text-sm"),y(Ct,"class","flex mb-2 h-6 items-center"),y(Dt,"class","btn-lg")},m(s,g){p(s,e,g),f(e,n),f(e,r),f(e,l),f(l,o),f(o,c),p(s,i,g),p(s,a,g),f(a,u),f(a,d),f(a,$),f($,w);for(let t=0;tВключить лог

',r=b(),l=h("div"),o=h("label"),c=h("div"),i=h("input"),a=b(),u=h("div"),m=b(),$=h("div"),x=b(),_=h("div"),j=h("div"),j.innerHTML='

Часовой пояс

',S=b(),T=h("div"),M=h("input"),L=b(),O&&O.c(),q=w(),y(n,"class","w-2/3"),y(i,"id","log"),y(i,"type","checkbox"),y(i,"class","sr-only"),y(u,"class",d="block "+(t[1].log?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg"),y($,"class","dot bg-gray-100 absolute left-1 top-1 w-4 h-4 rounded-full transition shadow-lg"),y(c,"class","relative"),y(o,"for","log"),y(o,"class","items-center cursor-pointer"),y(l,"class","flex justify-center w-1/3"),y(e,"class","flex mb-2 h-6 items-center"),y(j,"class","w-2/3"),y(M,"class","ipt-rnd text-center focus:border-indigo-500"),y(M,"type","number"),y(T,"class","flex justify-center w-1/3"),y(_,"class","flex mb-2 h-6 items-center")},m(s,d){p(s,e,d),f(e,n),f(e,r),f(e,l),f(l,o),f(o,c),f(c,i),i.checked=t[1].log,f(c,a),f(c,u),f(c,m),f(c,$),p(s,x,d),p(s,_,d),f(_,j),f(_,S),f(_,T),f(T,M),J(M,t[1].timezone),p(s,L,d),O&&O.m(s,d),p(s,q,d),E||(C=[v(i,"change",t[14]),v(i,"change",t[15]),v(M,"input",t[16]),v(M,"change",t[17])],E=!0)},p(t,e){2&e&&(i.checked=t[1].log),2&e&&d!==(d="block "+(t[1].log?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg")&&y(u,"class",d),2&e&&k(M.value)!==t[1].timezone&&J(M,t[1].timezone),t[2]?O?O.p(t,e):(O=Tn(t),O.c(),O.m(q.parentNode,q)):O&&(O.d(1),O=null)},d(t){t&&g(e),t&&g(x),t&&g(_),t&&g(L),O&&O.d(t),t&&g(q),E=!1,s(C)}}}function Ln(t){let e,n,s,r=t[25].msg+"";return{c(){e=h("div"),n=x(r),y(e,"class",s=t[25].msg.toString().includes("[E]")?"text-xs text-red-500":"text-xs text-black")},m(t,s){p(t,e,s),f(e,n)},p(t,l){32&l&&r!==(r=t[25].msg+"")&&_(n,r),32&l&&s!==(s=t[25].msg.toString().includes("[E]")?"text-xs text-red-500":"text-xs text-black")&&y(e,"class",s)},d(t){t&&g(e)}}}function qn(t){let e,n=t[5],s=[];for(let e=0;e{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function Hn(t){let e,n,s=Object.entries(t[3]),r=[];for(let e=0;eZ(r[t],1,1,(()=>{r[t]=null}));return{c(){for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function An(t,e,n){let{errorsJson:s}=e,{rebootEsp:r=(()=>{})}=e,{versionsList:l}=e,{choosingVersion:o}=e,{coreMessages:c}=e,{settingsJson:i}=e,{startUpdate:a=(()=>{})}=e,{saveSett:u=(()=>{})}=e,{show:d}=e,{paramsBeenChanged:f=!1}=e,{cancelAlarm:p=(t=>{})}=e;return t.$$set=t=>{"errorsJson"in t&&n(3,s=t.errorsJson),"rebootEsp"in t&&n(11,r=t.rebootEsp),"versionsList"in t&&n(4,l=t.versionsList),"choosingVersion"in t&&n(0,o=t.choosingVersion),"coreMessages"in t&&n(5,c=t.coreMessages),"settingsJson"in t&&n(1,i=t.settingsJson),"startUpdate"in t&&n(6,a=t.startUpdate),"saveSett"in t&&n(7,u=t.saveSett),"show"in t&&n(8,d=t.show),"paramsBeenChanged"in t&&n(2,f=t.paramsBeenChanged),"cancelAlarm"in t&&n(9,p=t.cancelAlarm)},[o,i,f,s,l,c,a,u,d,p,{mqtt:{e1:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Нет ответа от сервера",cancel:!1},e2:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Соединение было разорвано",cancel:!1},e3:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Ошибка соединения. Обычно возникает когда неверно указано название сервера MQTT",cancel:!1},e4:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Клиент был отключен",cancel:!1},e6:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Ошибка версии",cancel:!1},e7:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Отклонен идентификатор",cancel:!1},e8:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Не могу установить соединение",cancel:!1},e9:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Неправильное имя пользователя/пароль"},e10:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Не авторизован для подключения",cancel:!1},e11:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Название сервера пустое",cancel:!1},e12:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Имя пользователя или пароль пустые",cancel:!1},e13:{descr:"Mqtt",color:"text-red-500",txt:"Подключение в процессе",cancel:!1}},wse1:{1:{descr:"Ошибка веб сокетов",color:"text-red-500",txt:"Слишком много клиентов было открыто. Допускается не более четырех.",cancel:!0}},jse1:{1:{descr:"Ошибка json",color:"text-red-500",txt:"Недостаточный размер буфера библиотеки Arduino Json. Устройство может вести себя непредсказуемо. Обратитесь к разработчику.",cancel:!0}},jse2:{1:{descr:"Ошибка json",color:"text-red-500",txt:"Ошибка записи/чтения json.",cancel:!0,num:!0}},jse3:{1:{descr:"Ошибка json",color:"text-red-500",txt:"Ошибка чтения json файла с виджетами",cancel:!0}}},r,function(){o=S(this),n(0,o),n(4,l)},()=>a(),function(){i.log=this.checked,n(1,i)},()=>n(2,f=!0),function(){i.timezone=k(this.value),n(1,i)},()=>n(2,f=!0),()=>(u(),n(2,f=!1)),t=>p(t)]}class Bn extends rt{constructor(t){super(),st(this,t,An,Pn,l,{errorsJson:3,rebootEsp:11,versionsList:4,choosingVersion:0,coreMessages:5,settingsJson:1,startUpdate:6,saveSett:7,show:8,paramsBeenChanged:2,cancelAlarm:9})}}function zn(e){let n,s,r,l;return{c(){n=$("svg"),s=$("path"),y(s,"d","M7 18a4.6 4.4 0 0 1 0 -9h0a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12"),y(n,"class",r="h-8 w-8 "+e[0]),y(n,"width","8"),y(n,"height","8"),y(n,"viewBox",l=e[1]+" "+e[2]+" 24 24"),y(n,"stroke-width","2"),y(n,"stroke","currentColor"),y(n,"fill","none"),y(n,"stroke-linecap","round"),y(n,"stroke-linejoin","round")},m(t,e){p(t,n,e),f(n,s)},p(t,[e]){1&e&&r!==(r="h-8 w-8 "+t[0])&&y(n,"class",r),6&e&&l!==(l=t[1]+" "+t[2]+" 24 24")&&y(n,"viewBox",l)},i:t,o:t,d(t){t&&g(n)}}}function Dn(t,e,n){let{color:s}=e,{x:r=0}=e,{y:l=0}=e;return t.$$set=t=>{"color"in t&&n(0,s=t.color),"x"in t&&n(1,r=t.x),"y"in t&&n(2,l=t.y)},[s,r,l]}class In extends rt{constructor(t){super(),st(this,t,Dn,zn,l,{color:0,x:1,y:2})}}function Rn(t,e,n){const s=t.slice();return s[118]=e[n],s}function Fn(t){let e,n;return e=new Nt({}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Un(t){let e,n,s,r,l=t[118].name+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[118].ws,e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){524288&s[0]&&l!==(l=t[118].name+"")&&_(n,l),524288&s[0]&&r!==(r=t[118].ws)&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function Wn(t){let e,n,s,r,l,o,c,i,a,u;return e=new Jt({props:{path:"/",$$slots:{default:[Yn]},$$scope:{ctx:t}}}),s=new Jt({props:{path:"/config",$$slots:{default:[Kn]},$$scope:{ctx:t}}}),l=new Jt({props:{path:"/connection",$$slots:{default:[Qn]},$$scope:{ctx:t}}}),c=new Jt({props:{path:"/list",$$slots:{default:[Gn]},$$scope:{ctx:t}}}),a=new Jt({props:{path:"/system",$$slots:{default:[Zn]},$$scope:{ctx:t}}}),{c(){tt(e.$$.fragment),n=b(),tt(s.$$.fragment),r=b(),tt(l.$$.fragment),o=b(),tt(c.$$.fragment),i=b(),tt(a.$$.fragment)},m(t,d){et(e,t,d),p(t,n,d),et(s,t,d),p(t,r,d),et(l,t,d),p(t,o,d),et(c,t,d),p(t,i,d),et(a,t,d),u=!0},p(t,n){const r={};16408&n[0]|268435456&n[3]&&(r.$$scope={dirty:n,ctx:t}),e.$set(r);const o={};276512&n[0]|268435456&n[3]&&(o.$$scope={dirty:n,ctx:t}),s.$set(o);const i={};229440&n[0]|268435456&n[3]&&(i.$$scope={dirty:n,ctx:t}),l.$set(i);const u={};4718720&n[0]|268435456&n[3]&&(u.$$scope={dirty:n,ctx:t}),c.$set(u);const d={};8488704&n[0]|268435456&n[3]&&(d.$$scope={dirty:n,ctx:t}),a.$set(d)},i(t){u||(G(e.$$.fragment,t),G(s.$$.fragment,t),G(l.$$.fragment,t),G(c.$$.fragment,t),G(a.$$.fragment,t),u=!0)},o(t){Z(e.$$.fragment,t),Z(s.$$.fragment,t),Z(l.$$.fragment,t),Z(c.$$.fragment,t),Z(a.$$.fragment,t),u=!1},d(t){nt(e,t),t&&g(n),nt(s,t),t&&g(r),nt(l,t),t&&g(o),nt(c,t),t&&g(i),nt(a,t)}}}function Vn(e){let n,s;return n=new Ct({props:{title:"Нет соединения"}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function Yn(t){let e,n,s,r;return e=new xe({props:{show:t[4],layoutJson:t[14],pages:t[3],wsPush:t[40]}}),{c(){tt(e.$$.fragment),n=b(),s=w()},m(t,l){et(e,t,l),p(t,n,l),p(t,s,l),r=!0},p(t,n){const s={};16&n[0]&&(s.show=t[4]),16384&n[0]&&(s.layoutJson=t[14]),8&n[0]&&(s.pages=t[3]),e.$set(s)},i(t){r||(G(e.$$.fragment,t),r=!0)},o(t){Z(e.$$.fragment,t),r=!1},d(t){nt(e,t),t&&g(n),t&&g(s)}}}function Kn(t){let e,n;return e=new De({props:{show:t[5],configJson:t[11],widgetsJson:t[12],itemsJson:t[13],saveConfig:t[41],rebootEsp:t[42],scenarioTxt:t[18]}}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},p(t,n){const s={};32&n[0]&&(s.show=t[5]),2048&n[0]&&(s.configJson=t[11]),4096&n[0]&&(s.widgetsJson=t[12]),8192&n[0]&&(s.itemsJson=t[13]),262144&n[0]&&(s.scenarioTxt=t[18]),e.$set(s)},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Qn(t){let e,n;return e=new nn({props:{show:t[6],rebootEsp:t[43],ssidClick:t[44],saveSett:t[45],saveMqtt:t[46],settingsJson:t[15],errorsJson:t[16],ssidJson:t[17]}}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},p(t,n){const s={};64&n[0]&&(s.show=t[6]),32768&n[0]&&(s.settingsJson=t[15]),65536&n[0]&&(s.errorsJson=t[16]),131072&n[0]&&(s.ssidJson=t[17]),e.$set(s)},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Gn(t){let e,n;return e=new pn({props:{show:t[7],deviceList:t[19],showInput:ts,addDevInList:t[47],newDevice:t[22],sendToAllDevices:t[48]}}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},p(t,n){const s={};128&n[0]&&(s.show=t[7]),524288&n[0]&&(s.deviceList=t[19]),4194304&n[0]&&(s.newDevice=t[22]),e.$set(s)},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Zn(t){let e,n,s;function r(e){t[53](e)}let l={show:t[8],errorsJson:t[16],settingsJson:t[15],saveSett:t[49],rebootEsp:t[50],cancelAlarm:t[51],versionsList:t[9],startUpdate:t[52],coreMessages:t[23]};return void 0!==t[10]&&(l.choosingVersion=t[10]),e=new Bn({props:l}),O.push((()=>X(e,"choosingVersion",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(t,s){const r={};256&s[0]&&(r.show=t[8]),65536&s[0]&&(r.errorsJson=t[16]),32768&s[0]&&(r.settingsJson=t[15]),512&s[0]&&(r.versionsList=t[9]),8388608&s[0]&&(r.coreMessages=t[23]),!n&&1024&s[0]&&(n=!0,r.choosingVersion=t[10],I((()=>n=!1))),e.$set(r)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function Xn(t){let e,n,r,l,o,c,i,a,u,d,$,x,w,k,_,J,S,T,M,L,q,E,C,O,N,H,P,A,B,z,I,R,F,U,W,V,Y,X,st,rt,lt,ot,ct=t[2]&&Fn(),it=t[19],at=[];for(let e=0;e",_=b(),J=h("ul"),S=h("li"),T=h("a"),T.textContent="Управление",M=b(),L=h("li"),q=h("a"),q.textContent="Конфигуратор",E=b(),C=h("li"),O=h("a"),O.textContent="Подключение",N=b(),H=h("li"),P=h("a"),P.textContent="Устройства",A=b(),B=h("li"),z=h("a"),z.textContent="Системные",I=b(),R=h("main"),F=h("ul"),U=h("div"),V.c(),X=b(),st=h("footer"),st.innerHTML='
Developed by Dmitry Borisenko
',y(c,"class","border border-indigo-500 border-4"),void 0===t[21]&&D((()=>t[36].call(c))),y(o,"class","px-15 py-1"),y(a,"class","pl-4 pr-4 py-1"),y(l,"class","flex content-center items-center justify-end"),y(r,"class","h-10 w-full bg-gray-100 overflow-auto shadow-md"),y(x,"class","w-0 h-0"),y(x,"id","menu__toggle"),y(x,"type","checkbox"),y(k,"class","menu__btn"),y(k,"for","menu__toggle"),y(T,"class","menu__item"),y(T,"href","/"),y(q,"class","menu__item"),y(q,"href","/config"),y(O,"class","menu__item"),y(O,"href","/connection"),y(P,"class","menu__item"),y(P,"href","/list"),y(z,"class","menu__item"),y(z,"href","/system"),y(J,"class","menu__box"),y($,"class","flex"),y(U,"class","bg-cover pt-0 px-4"),y(F,"class","menu__main"),y(R,"class",Y="flex-1 overflow-y-auto p-0 "+(!0!==t[0]||t[1]?"ml-0":"ml-36")),y(st,"class","h-4 bg-gray-100 border-gray-200 shadow-lg"),y(e,"class","flex flex-col h-screen bg-gray-50")},m(s,g){p(s,e,g),ct&&ct.m(e,null),f(e,n),f(e,r),f(r,l),f(l,o),f(o,c);for(let t=0;t{ct=null})),Q()),524288&s[0]){let e;for(it=t[19],e=0;e{dt[l]=null})),Q(),V=dt[W],V?V.p(t,s):(V=dt[W]=ut[W](t),V.c()),G(V,1),V.m(U,null)),(!rt||3&s[0]&&Y!==(Y="flex-1 overflow-y-auto p-0 "+(!0!==t[0]||t[1]?"ml-0":"ml-36")))&&y(R,"class",Y)},i(t){rt||(G(ct),G(u.$$.fragment,t),G(V),rt=!0)},o(t){Z(ct),Z(u.$$.fragment,t),Z(V),rt=!1},d(t){t&&g(e),ct&&ct.d(),m(at,t),nt(u),dt[W].d(),lt=!1,s(ot)}}}let ts=!1;function es(t){try{JSON.parse(t)}catch(e){return console.log("[e]","json parce error: ",t),!1}return!0}function ns(t,e,n){let s;o(t,xt,(t=>n(82,s=t))),xt.mode.hash();let r=!1,l=!1,c=document.location.hostname,i=!0,a=!1;const u=void 0;let d,f=[],p=!1,g=!1,m=!1,h=!1,$=!1,x={},b=[],w=!1,v=!1,y=[],k=!1,_=!1,J=[],j=!1,T=!1,M=[],L={},E=!1,C={},O=!1,N={},H=!1,P={},A=[],B=!1,z="",D=!1,I=!1,R=[];R=[{name:"--",id:"--",ip:c,ws:0,status:!1}];let F,U=[],W=!1,V=0,Y=!0,K={},Q=[];var G=function(){this.parts=[]};let Z;G.prototype.append=function(t){this.parts.push(t),this.blob=void 0},G.prototype.getBlob=function(){return this.blob||(this.blob=new Blob(this.parts,{type:"binary"})),this.blob},G.prototype.clear=function(){this.parts=[]};var X=new G,tt=new G,et=new G,nt=new G,st=[];function rt(){void 0!==V&&bt(V,Z)}function lt(){Jt(V);let t=0;R.forEach((e=>{e.ws=t,e.status||(ct(t),at(t)),t++})),n(19,R)}function ot(t,e){R.forEach((n=>{n.ws===t&&(n.status=e,n.status?console.log("[i]",n.ip,"status online"):console.log("[i]",n.ip,"status offline"))})),n(19,R),Jt(V),n(20,W=F.status)}function ct(t){let e=it(t);"error"===e?console.log("[e]","device list wrong"):(U[t]=new WebSocket("ws://"+e+":81"),U.binaryType="blob",console.log("[i]",e,t,"started connecting..."))}function it(t){let e="error";return R.forEach((n=>{t===n.ws&&(e=n.ip)})),e}function at(t){if(U[t]){let e=it(t);console.log("[i]",e,t,"web socket events added"),U[t].addEventListener("open",(function(n){console.log("[i]",e,t,"completed connecting"),ot(t,!0),i&&bt(0,"/list|"),"/|"===Z?bt(t,Z):t===V&&rt()})),U[t].addEventListener("message",(function(e){if("string"==typeof e.data){let c=e.data;if(t===V){if(c.includes('devicelist":"')&&es(c)&&(A=JSON.parse(c),A=A,i?(n(19,R=A),n(19,R[0].status=!0,R)):n(19,(r=R,l=A,o=new Set(r.map((t=>t.ip))),R=[...r,...l.filter((t=>!o.has(t.ip)))])),i=!1,n(19,R),B=!0,console.log("✔","deviceList json parced"),ut(),kt(),lt()),c.includes('ssid":"')&&es(c)&&(n(17,N=JSON.parse(c)),n(17,N),console.log("✔","ssidJson parced"),H=!0,ut()),c.includes('errors":"')&&es(c)&&(n(16,C=JSON.parse(c)),n(16,C),O=!0,console.log("✔","errorsJson json parced"),ut()),c.includes('settings":"')&&es(c)&&(n(15,L=JSON.parse(c)),n(15,L),E=!0,console.log("✔","settingsJson json parced"),ut()),c.includes("/log|")&&(c=c.replace("/log|",""),console.log("",c),yt(c)),"/st/scenario.txt"===c&&(D=!0),"/end/scenario.txt"===c){D=!1;var s=nt.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{n(18,z=t.result),n(18,z),I=!0,console.log("✔","scenarioTxt parced"),ut()}}if("/st/config.json"===c&&(w=!0),"/end/config.json"===c){w=!1;s=X.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{let e=t.result;es(e)&&(n(11,b=JSON.parse(e)),n(11,b),v=!0,console.log("✔","configJson parced"),ut())}}if("/st/widgets.json"===c&&(k=!0),"/end/widgets.json"===c){k=!1;s=tt.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{let e=t.result;es(e)&&(n(12,y=JSON.parse(e)),n(12,y),_=!0,console.log("✔","widgetsJson parced"),ut())}}if("/st/items.json"===c&&(j=!0),"/end/items.json"===c){j=!1;s=et.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{let e=t.result;es(e)&&(n(13,J=JSON.parse(e)),n(13,J),T=!0,console.log("✔","itemsJson parced"),ut())}}}if("/end/layout.json"===c&&async function(t){var e=st[t].getBlob();let s=new FileReader;s.readAsText(e),s.onload=()=>{let e=JSON.parse(s.result);!function(t,e){for(const[n,s]of Object.entries(P))for(let r=0;r{console.log("[e]",e,"connection closed"),ot(t,!1)})),U[t].addEventListener("error",(function(n){console.log("[e]",e,"connection error"),ot(t,!1)}))}else console.log("[e]","socket not exist")}async function ut(){"/|"===Z&&(mt(),console.log("✔","dashboard packet received"),n(4,p=!0)),"/config|"===Z&&T&&_&&v&&E&&I&&(mt(),console.log("✔✔","config data parced"),n(5,g=!0)),"/connection|"===Z&&H&&E&&O&&(mt(),console.log("✔✔","connection data parced"),n(6,m=!0)),"/list|"===Z&&B&&(mt(),console.log("✔✔","list data parced"),n(7,h=!0)),"/system|"===Z&&O&&E&&(mt(),async function(){try{let t=L.serverip+"/iotm/ver.json";console.log("url",t);let e=await fetch(t,{mode:"cors",method:"GET"});e.ok?(n(9,x=await e.json()),n(9,x=x[C.bn]),n(10,d=C.bver),console.log(JSON.stringify(x))):(n(10,d=void 0),console.log("error, versions list not received",e.statusText))}catch(t){n(10,d=void 0),console.log("error, versions list not received"),console.log(t)}}(),console.log("✔✔","system data parced"),n(8,$=!0))}function dt(){n(18,z),bt(V,"/tuoyal|"+JSON.stringify(function(){let t=[];for(let e=0;e5?(!function(t,e,n,s,r){for(let l=0;l5?bt(V,"/sgnittes|"+JSON.stringify(L)):window.alert("Ошибка"),gt(),bt(V,"/mqtt|")}function gt(){n(11,b=[]),X.clear(),n(12,y=[]),tt.clear(),n(13,J=[]),et.clear(),n(14,M=[]),st=[],n(18,z=""),nt.clear(),n(15,L={}),n(16,C={}),n(23,Q=[]),n(4,p=!1),n(5,g=!1),n(6,m=!1),n(7,h=!1),n(8,$=!1),mt(),console.log("[i]","all app data cleared")}function mt(){v=!1,_=!1,T=!1,E=!1,O=!1,H=!1,B=!1,I=!1,function(){for(let t=0;t{!function(t){let e=!1;return R.forEach((n=>{t===n.ws&&(e=n.status)})),e}(t.ws)?(ct(t.ws),at(t.ws)):bt(t.ws,"/tst|")})),Y=!1)}function bt(t,e){U[t]&&1===U[t].readyState?(U[t].send(e),console.log("[i]",it(t),t,"msg send success",e)):console.log("[e]",it(t),t,"msg not send",e)}function wt(t){R.forEach((e=>{e.status&&bt(e.ws,t)}))}function vt(){M.sort((function(t,e){return t.descre.descr?1:0})),n(3,f=[]);Array.from(new Set(Array.from(M,(({page:t})=>t)))).forEach((function(t,e,s){n(3,f=[...f,JSON.parse(JSON.stringify({page:t}))])})),f.sort((function(t,e){return t.pagee.page?1:0}))}xt.subscribe((function(){console.log("[i]","handle navigation"),gt(),Z=s.path.toString(),Z+="|",console.log("[i]","user on page:",Z),"/|"===Z?wt(Z):rt()})),q((async()=>{console.log("[i]","mounted"),kt(),i=!0,lt(),$t(),vt()}));const yt=t=>{Q.length>=100&&Q.shift(),n(23,Q=[...Q,{msg:t}]),Q.sort((function(t,e){return t.time>e.time?-1:t.time_t(),function(){r=this.checked,n(0,r)},()=>St(),(t,e,n)=>ht(t,e,n),()=>dt(),()=>Mt(),()=>Mt(),()=>Tt(),()=>ft(),()=>pt(),()=>jt(),t=>wt(t),()=>ft(),()=>Mt(),t=>qt(t),()=>Et(),function(t){d=t,n(10,d)}]}return new class extends rt{constructor(t){super(),st(this,t,ns,Xn,l,{},null,[-1,-1,-1,-1])}}({target:document.body,props:{name:"world"}})}(); +//# sourceMappingURL=bundle.js.map diff --git a/include/Const.h b/include/Const.h index d93e4c43..37cfccba 100644 --- a/include/Const.h +++ b/include/Const.h @@ -13,6 +13,7 @@ //Размер буфера json #define JSON_BUFFER_SIZE 2048 +#define WEB_SOCKETS_FRAME_SIZE 2048 //выбор сервера //#define ASYNC_WEB_SERVER diff --git a/include/WsServer.h b/include/WsServer.h index 58b9ab07..fb5efbef 100644 --- a/include/WsServer.h +++ b/include/WsServer.h @@ -17,11 +17,11 @@ void sendFileToWs(String filename, int num, size_t frameSize); void publishStatusWs(const String& topic, const String& data); void publishChartWs(int num, String& path); void periodicWsSend(); -void sendStringToWs(const String& msg, uint8_t num, String name); + void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, String id); - -void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t client_id, size_t frameSize); +void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize); +void sendStringToWs(const String& header, String& payload, uint8_t client_id); // void sendMark(const char* filename, const char* mark, uint8_t num); // void sendFileToWs3(const String& filename, uint8_t num); diff --git a/src/StandWebServer.cpp b/src/StandWebServer.cpp index 6a846158..3a465a7e 100644 --- a/src/StandWebServer.cpp +++ b/src/StandWebServer.cpp @@ -14,9 +14,9 @@ void standWebServerInit() { // HTTP.on("/devicelist.json", HTTP_GET, []() { // HTTP.send(200, "application/json", devListHeapJson); // }); - // HTTP.on("/settings.h.json", HTTP_GET, []() { - // HTTP.send(200, "application/json", settingsFlashJson); - // }); + HTTP.on("/settings.h.json", HTTP_GET, []() { + HTTP.send(200, "application/json", settingsFlashJson); + }); // HTTP.on("/settings.f.json", HTTP_GET, []() { // HTTP.send(200, "application/json", readFile(F("settings.json"), 20000)); // }); diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 7ed90a58..4a1398bb 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -93,12 +93,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //отвечаем данными на запрос страницы if (headerStr == "/config|") { - sendFileToWs("/items.json", num, 1024); - sendFileToWs("/widgets.json", num, 1024); - sendFileToWs("/config.json", num, 1024); - sendFileToWs("/scenario.txt", num, 1024); + sendFileToWsByFrames("/items.json", "itemsj", "", num, WEB_SOCKETS_FRAME_SIZE); + sendFileToWsByFrames("/widgets.json", "widget", "", num, WEB_SOCKETS_FRAME_SIZE); + sendFileToWsByFrames("/config.json", "config", "", num, WEB_SOCKETS_FRAME_SIZE); + sendFileToWsByFrames("/scenario.txt", "scenar", "", num, WEB_SOCKETS_FRAME_SIZE); + sendStringToWs("settin", settingsFlashJson, num); + //шлется для того что бы получить топик устройства - standWebSocket.sendTXT(num, settingsFlashJson); + // standWebSocket.sendTXT(num, settingsFlashJson); } //отправляем все графики в веб для экспорта @@ -222,7 +224,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) } if (headerStr == "/test|") { - sendBlobToWsStrHeader("/items.json", "layout|0000|", num, 2048); + sendFileToWsByFrames("/items.json", "itemsj", "", num, WEB_SOCKETS_FRAME_SIZE); + sendFileToWsByFrames("/widgets.json", "widget", "", num, WEB_SOCKETS_FRAME_SIZE); + sendFileToWsByFrames("/config.json", "config", "", num, WEB_SOCKETS_FRAME_SIZE); + sendFileToWsByFrames("/scenario.txt", "scenar", "", num, WEB_SOCKETS_FRAME_SIZE); } } break; @@ -350,16 +355,16 @@ void sendFileToWs(String filename, int num, size_t frameSize) { } //посылка данных из string -void sendStringToWs(const String& msg, uint8_t num, String name) { - String st = "/st" + String(name); - standWebSocket.sendTXT(num, st); - size_t size = msg.length(); - char dataArray[size]; - msg.toCharArray(dataArray, size); - standWebSocket.sendBIN(num, (uint8_t*)dataArray, size); - String end = "/end" + String(name); - standWebSocket.sendTXT(num, end); -} +// void sendStringToWs(const String& msg, uint8_t num, String name) { +// String st = "/st" + String(name); +// standWebSocket.sendTXT(num, st); +// size_t size = msg.length(); +// char dataArray[size]; +// msg.toCharArray(dataArray, size); +// standWebSocket.sendBIN(num, (uint8_t*)dataArray, size); +// String end = "/end" + String(name); +// standWebSocket.sendTXT(num, end); +//} //особая функция отправки графиков в веб void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, String id) { @@ -400,13 +405,12 @@ void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, } } -// 6 4 -// layout|0120|{status:12}|...from file... -// layout|0000|...from file... -// layout|0000|...from file... +void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize) { + if (header.length() != 6) { + SerialPrint("E", "FS", F("wrong header size")); + return; + } -void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t client_id, size_t frameSize) { - // откроем файл auto path = filepath(filename); auto file = FileFS.open(path, "r"); if (!file) { @@ -417,19 +421,26 @@ void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t size_t totalSize = file.size(); SerialPrint("I", "FS", "Send file '" + String(filename) + "', file size: " + String(totalSize)); - // размер заголовка - auto headerSize = header.length(); - // выделим буфер размером с фрейм + char buf[32]; + sprintf(buf, "%04d", json.length() + 12); + String data = header + "|" + String(buf) + "|" + json; + + size_t headerSize = data.length(); auto frameBuf = new uint8_t[frameSize]; - // заголовок у нас не меняется, запием его в начало буфера - header.toCharArray((char*)frameBuf, frameSize); - // указатель на начало полезной нагрузки - auto payloadBuf = &frameBuf[headerSize]; - // и сколько осталось места для нее - auto maxPayloadSize = frameSize - headerSize; + size_t maxPayloadSize = frameSize - headerSize; + uint8_t* payloadBuf = nullptr; + int i = 0; while (file.available()) { - // прочитаем кусок в буфер + if (i == 0) { + data.toCharArray((char*)frameBuf, frameSize); + payloadBuf = &frameBuf[headerSize]; + } else { + maxPayloadSize = frameSize; + headerSize = 0; + payloadBuf = &frameBuf[0]; + } + size_t payloadSize = file.read(payloadBuf, maxPayloadSize); if (payloadSize) { size_t size = headerSize + payloadSize; @@ -448,13 +459,90 @@ void sendBlobToWsStrHeader(const String& filename, const String& header, uint8_t continuation = true; } - SerialPrint("I", "FS", String(i) + ") sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); + SerialPrint("I", "FS", String(i) + ") fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); } i++; } + payloadBuf = &frameBuf[0]; + delete[] payloadBuf; + file.close(); } +void sendStringToWs(const String& header, String& payload, uint8_t client_id) { + if (header.length() != 6) { + SerialPrint("E", "FS", F("wrong header size")); + return; + } + + String msg = header + "|0012|" + payload; + size_t totalSize = msg.length(); + SerialPrint("I", "FS", "Send string '" + header + "', str size: " + String(totalSize)); + + char dataArray[totalSize]; + msg.toCharArray(dataArray, totalSize + 1); + standWebSocket.sendBIN(client_id, (uint8_t*)dataArray, totalSize); +} + +// void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize) { +// if (header.length() != 6) { +// SerialPrint("E", "FS", F("wrong header size")); +// return; +// } +// // откроем файл +// auto path = filepath(filename); +// auto file = FileFS.open(path, "r"); +// if (!file) { +// SerialPrint("E", "FS", F("reed file error")); +// return; +// } +// +// size_t totalSize = file.size(); +// SerialPrint("I", "FS", "Send file '" + String(filename) + "', file size: " + String(totalSize)); +// +// char buf[32]; +// sprintf(buf, "%04d", json.length() + 12); +// +// String data = header + "|" + String(buf) + "|" + json; +// +// // размер заголовка +// auto headerSize = data.length(); +// // выделим буфер размером с фрейм +// auto frameBuf = new uint8_t[frameSize]; +// // заголовок у нас не меняется, запием его в начало буфера +// data.toCharArray((char*)frameBuf, frameSize); +// // указатель на начало полезной нагрузки +// auto payloadBuf = &frameBuf[headerSize]; +// // и сколько осталось места для нее +// auto maxPayloadSize = frameSize - headerSize; +// int i = 0; +// while (file.available()) { +// // прочитаем кусок в буфер +// size_t payloadSize = file.read(payloadBuf, maxPayloadSize); +// if (payloadSize) { +// size_t size = headerSize + payloadSize; +// +// bool fin = false; +// if (size == frameSize) { +// fin = false; +// } else { +// fin = true; +// } +// +// bool continuation = false; +// if (i == 0) { +// continuation = false; +// } else { +// continuation = true; +// } +// +// SerialPrint("I", "FS", String(i) + ") sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); +// standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); +// } +// i++; +// } +// } + // void sendMark(const char* filename, const char* mark, uint8_t num) { // char outChar[strlen(filename) + strlen(mark) + 1]; // strcpy(outChar, mark); diff --git a/src/modules/virtual/Loging/Loging (1).cpp b/src/modules/virtual/Loging/Loging (1).cpp new file mode 100644 index 00000000..cd6697a1 --- /dev/null +++ b/src/modules/virtual/Loging/Loging (1).cpp @@ -0,0 +1,34 @@ +#include "Global.h" +#include "classes/IoTItem.h" + +class Loging : public IoTItem { + private: + String logval; + + public: + Loging(String parameters) : IoTItem(parameters) { + jsonRead(parameters, F("logid"), logval); + } + + // void setValue(IoTValue Value) { + // value = Value; + // regEvent((String)(int)value.valD, "Loging"); + // } + + void doByInterval() { + String value = getItemValue(logval); + if (value == "") { + SerialPrint("E", F("Logging"), F("no value set")); + } else { + regEvent(value, "Logging"); + } + } +}; + +void* getAPI_Loging(String subtype, String param) { + if (subtype == F("Loging")) { + return new Loging(param); + } else { + return nullptr; + } +} From 8287bb9ebb1587cabb1f822e9104f99ae7edf688 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Sun, 9 Oct 2022 14:30:42 +0200 Subject: [PATCH 008/107] =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20api=20=D0=B2=D0=B5=D0=B1=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 23 --------------- include/WsServer.h | 2 +- lib/WebSockets/src/WebSocketsServer.cpp | 4 +-- lib/WebSockets/src/WebSocketsServer.h | 2 +- src/WsServer.cpp | 33 ++++++++++------------ src/modules/virtual/Loging/Loging (1).cpp | 34 ----------------------- 6 files changed, 19 insertions(+), 79 deletions(-) delete mode 100644 .vscode/settings.json delete mode 100644 src/modules/virtual/Loging/Loging (1).cpp diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 4c76fa80..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "files.associations": { - "array": "cpp", - "deque": "cpp", - "list": "cpp", - "string": "cpp", - "unordered_map": "cpp", - "vector": "cpp", - "string_view": "cpp", - "initializer_list": "cpp", - "ranges": "cpp", - "thread": "cpp", - "optional": "cpp", - "istream": "cpp", - "ostream": "cpp", - "ratio": "cpp", - "system_error": "cpp", - "functional": "cpp", - "tuple": "cpp", - "type_traits": "cpp", - "utility": "cpp" - } -} \ No newline at end of file diff --git a/include/WsServer.h b/include/WsServer.h index fb5efbef..8c88e92d 100644 --- a/include/WsServer.h +++ b/include/WsServer.h @@ -21,7 +21,7 @@ void periodicWsSend(); void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, String id); void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize); -void sendStringToWs(const String& header, String& payload, uint8_t client_id); +void sendStringToWs(const String& header, String& payload, int client_id); // void sendMark(const char* filename, const char* mark, uint8_t num); // void sendFileToWs3(const String& filename, uint8_t num); diff --git a/lib/WebSockets/src/WebSocketsServer.cpp b/lib/WebSockets/src/WebSocketsServer.cpp index 5c5abea8..e864bfe2 100644 --- a/lib/WebSockets/src/WebSocketsServer.cpp +++ b/lib/WebSockets/src/WebSocketsServer.cpp @@ -251,13 +251,13 @@ bool WebSocketsServerCore::sendBIN(uint8_t num, const uint8_t * payload, size_t * @param headerToPayload bool (see sendFrame for more details) * @return true if ok */ -bool WebSocketsServerCore::broadcastBIN(uint8_t * payload, size_t length, bool fin, bool headerToPayload) { +bool WebSocketsServerCore::broadcastBIN(uint8_t * payload, size_t length, bool fin, bool continuation, bool headerToPayload) { WSclient_t * client; bool ret = true; for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { client = &_clients[i]; if(clientIsConnected(client)) { - if(!sendFrame(client, WSop_binary, payload, length, true, headerToPayload)) { + if(!sendFrame(client, WSop_binary, payload, length, fin, headerToPayload)) { ret = false; } } diff --git a/lib/WebSockets/src/WebSocketsServer.h b/lib/WebSockets/src/WebSocketsServer.h index cd224dd2..a8472a5b 100644 --- a/lib/WebSockets/src/WebSocketsServer.h +++ b/lib/WebSockets/src/WebSocketsServer.h @@ -68,7 +68,7 @@ class WebSocketsServerCore : protected WebSockets { bool sendBIN(uint8_t num, uint8_t * payload, size_t length, bool fin = true, bool continuation = false, bool headerToPayload = false); bool sendBIN(uint8_t num, const uint8_t * payload, size_t length); - bool broadcastBIN(uint8_t * payload, size_t length, bool fin = true, bool headerToPayload = false); + bool broadcastBIN(uint8_t * payload, size_t length, bool fin = true, bool continuation = false, bool headerToPayload = false); bool broadcastBIN(const uint8_t * payload, size_t length); bool sendPing(uint8_t num, uint8_t * payload = NULL, size_t length = 0); diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 4a1398bb..dbcdc553 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -98,13 +98,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) sendFileToWsByFrames("/config.json", "config", "", num, WEB_SOCKETS_FRAME_SIZE); sendFileToWsByFrames("/scenario.txt", "scenar", "", num, WEB_SOCKETS_FRAME_SIZE); sendStringToWs("settin", settingsFlashJson, num); - - //шлется для того что бы получить топик устройства - // standWebSocket.sendTXT(num, settingsFlashJson); - } - - //отправляем все графики в веб для экспорта - if (headerStr == "/expcharts|") { } //обработка кнопки сохранить @@ -130,9 +123,9 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //отвечаем данными на запрос страницы if (headerStr == "/connection|") { - standWebSocket.sendTXT(num, settingsFlashJson); - standWebSocket.sendTXT(num, ssidListHeapJson); - standWebSocket.sendTXT(num, errorsHeapJson); + sendStringToWs("settin", settingsFlashJson, num); + sendStringToWs("ssidli", ssidListHeapJson, num); + sendStringToWs("errors", errorsHeapJson, num); // запуск асинхронного сканирования wifi сетей при переходе на страницу соединений // RouterFind(jsonReadStr(settingsFlashJson, F("routerssid"))); } @@ -166,7 +159,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //отвечаем данными на запрос страницы if (headerStr == "/list|") { - standWebSocket.sendTXT(num, devListHeapJson); + sendStringToWs("devlis", devListHeapJson, num); } //----------------------------------------------------------------------// @@ -175,8 +168,8 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //отвечаем данными на запрос страницы if (headerStr == "/system|") { - standWebSocket.sendTXT(num, errorsHeapJson); - standWebSocket.sendTXT(num, settingsFlashJson); + sendStringToWs("errors", errorsHeapJson, num); + sendStringToWs("settin", settingsFlashJson, num); } //----------------------------------------------------------------------// @@ -297,9 +290,9 @@ void publishChartWs(int num, String& path) { //данные которые мы отправляем в сокеты переодически void periodicWsSend() { - // standWebSocket.broadcastTXT(devListHeapJson); - // standWebSocket.broadcastTXT(ssidListHeapJson); - // standWebSocket.broadcastTXT(errorsHeapJson); + sendStringToWs("ssidli", ssidListHeapJson, -1); + sendStringToWs("errors", errorsHeapJson, -1); + sendStringToWs("devlis", devListHeapJson, -1); } #ifdef ESP32 @@ -469,7 +462,7 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St file.close(); } -void sendStringToWs(const String& header, String& payload, uint8_t client_id) { +void sendStringToWs(const String& header, String& payload, int client_id) { if (header.length() != 6) { SerialPrint("E", "FS", F("wrong header size")); return; @@ -481,7 +474,11 @@ void sendStringToWs(const String& header, String& payload, uint8_t client_id) { char dataArray[totalSize]; msg.toCharArray(dataArray, totalSize + 1); - standWebSocket.sendBIN(client_id, (uint8_t*)dataArray, totalSize); + if (client_id == -1) { + standWebSocket.broadcastBIN((uint8_t*)dataArray, totalSize); + } else { + standWebSocket.sendBIN(client_id, (uint8_t*)dataArray, totalSize); + } } // void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize) { diff --git a/src/modules/virtual/Loging/Loging (1).cpp b/src/modules/virtual/Loging/Loging (1).cpp deleted file mode 100644 index cd6697a1..00000000 --- a/src/modules/virtual/Loging/Loging (1).cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "Global.h" -#include "classes/IoTItem.h" - -class Loging : public IoTItem { - private: - String logval; - - public: - Loging(String parameters) : IoTItem(parameters) { - jsonRead(parameters, F("logid"), logval); - } - - // void setValue(IoTValue Value) { - // value = Value; - // regEvent((String)(int)value.valD, "Loging"); - // } - - void doByInterval() { - String value = getItemValue(logval); - if (value == "") { - SerialPrint("E", F("Logging"), F("no value set")); - } else { - regEvent(value, "Logging"); - } - } -}; - -void* getAPI_Loging(String subtype, String param) { - if (subtype == F("Loging")) { - return new Loging(param); - } else { - return nullptr; - } -} From fe30d7c27d4929adb340e6b81bafc149bd820588 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Sun, 9 Oct 2022 17:04:05 +0200 Subject: [PATCH 009/107] =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B4=D0=BE=D0=BB?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/WebSockets/src/WebSocketsServer.cpp | 6 +- src/MqttClient.cpp | 5 +- src/UpgradeFirm.cpp | 2 +- src/WsServer.cpp | 144 +++++++++--------- src/modules/virtual/Loging/Loging.cpp | 4 +- .../virtual/LogingDaily/LogingDaily.cpp | 2 +- src/utils/SerialPrint.cpp | 7 +- 7 files changed, 84 insertions(+), 86 deletions(-) diff --git a/lib/WebSockets/src/WebSocketsServer.cpp b/lib/WebSockets/src/WebSocketsServer.cpp index e864bfe2..39d8d3e2 100644 --- a/lib/WebSockets/src/WebSocketsServer.cpp +++ b/lib/WebSockets/src/WebSocketsServer.cpp @@ -257,8 +257,10 @@ bool WebSocketsServerCore::broadcastBIN(uint8_t * payload, size_t length, bool f for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) { client = &_clients[i]; if(clientIsConnected(client)) { - if(!sendFrame(client, WSop_binary, payload, length, fin, headerToPayload)) { - ret = false; + if(continuation) { + ret = sendFrame(client, WSop_continuation, payload, length, fin, headerToPayload); + } else { + ret = sendFrame(client, WSop_binary, payload, length, fin, headerToPayload); } } WEBSOCKETS_YIELD(); diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index 89a2ba7e..7fa2c7ef 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -325,14 +325,15 @@ void handleMqttStatus(bool send) { String stateStr = getStateStr(mqtt.state()); // Serial.println(stateStr); jsonWriteStr_(errorsHeapJson, F("mqtt"), stateStr); - if (!send) standWebSocket.broadcastTXT(errorsHeapJson); + + if (!send) sendStringToWs("errors", errorsHeapJson, -1); } void handleMqttStatus(bool send, int state) { String stateStr = getStateStr(state); // Serial.println(stateStr); jsonWriteStr_(errorsHeapJson, F("mqtt"), stateStr); - if (!send) standWebSocket.broadcastTXT(errorsHeapJson); + if (!send) sendStringToWs("errors", errorsHeapJson, -1); } // log-20384820.txt diff --git a/src/UpgradeFirm.cpp b/src/UpgradeFirm.cpp index ec953ba7..48c55fdf 100644 --- a/src/UpgradeFirm.cpp +++ b/src/UpgradeFirm.cpp @@ -132,5 +132,5 @@ void saveUserDataToFlash() { void handleUpdateStatus(bool send, int state) { jsonWriteInt_(errorsHeapJson, F("upd"), state); - if (!send) standWebSocket.broadcastTXT(errorsHeapJson); + if (!send) sendStringToWs("errors", errorsHeapJson, -1); } \ No newline at end of file diff --git a/src/WsServer.cpp b/src/WsServer.cpp index dbcdc553..9014f600 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -217,10 +217,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) } if (headerStr == "/test|") { - sendFileToWsByFrames("/items.json", "itemsj", "", num, WEB_SOCKETS_FRAME_SIZE); - sendFileToWsByFrames("/widgets.json", "widget", "", num, WEB_SOCKETS_FRAME_SIZE); - sendFileToWsByFrames("/config.json", "config", "", num, WEB_SOCKETS_FRAME_SIZE); - sendFileToWsByFrames("/scenario.txt", "scenar", "", num, WEB_SOCKETS_FRAME_SIZE); } } break; @@ -313,38 +309,38 @@ void hexdump(const void* mem, uint32_t len, uint8_t cols = 16) { //посылка данных из файла в бинарном виде void sendFileToWs(String filename, int num, size_t frameSize) { - String st = "/st" + String(filename); - if (num == -1) { - standWebSocket.broadcastTXT(st); - } else { - standWebSocket.sendTXT(num, st); - } - - String path = filepath(filename); - auto file = FileFS.open(path, "r"); - if (!file) { - SerialPrint(F("E"), F("FS"), F("reed file error")); - return; - } - size_t fileSize = file.size(); - SerialPrint(F("i"), F("FS"), "Send file '" + String(filename) + "', file size: " + String(fileSize)); - uint8_t payload[frameSize]; - int countRead = file.read(payload, sizeof(payload)); - while (countRead > 0) { - if (num == -1) { - standWebSocket.broadcastBIN(payload, countRead); - } else { - standWebSocket.sendBIN(num, payload, countRead); - } - countRead = file.read(payload, sizeof(payload)); - } - file.close(); - String end = "/end" + String(filename); - if (num == -1) { - standWebSocket.broadcastTXT(end); - } else { - standWebSocket.sendTXT(num, end); - } + // String st = "/st" + String(filename); + // if (num == -1) { + // standWebSocket.broadcastTXT(st); + // } else { + // standWebSocket.sendTXT(num, st); + // } + // + // String path = filepath(filename); + // auto file = FileFS.open(path, "r"); + // if (!file) { + // SerialPrint(F("E"), F("FS"), F("reed file error")); + // return; + //} + // size_t fileSize = file.size(); + // SerialPrint(F("i"), F("FS"), "Send file '" + String(filename) + "', file size: " + String(fileSize)); + // uint8_t payload[frameSize]; + // int countRead = file.read(payload, sizeof(payload)); + // while (countRead > 0) { + // if (num == -1) { + // standWebSocket.broadcastBIN(payload, countRead); + // } else { + // standWebSocket.sendBIN(num, payload, countRead); + // } + // countRead = file.read(payload, sizeof(payload)); + //} + // file.close(); + // String end = "/end" + String(filename); + // if (num == -1) { + // standWebSocket.broadcastTXT(end); + //} else { + // standWebSocket.sendTXT(num, end); + //} } //посылка данных из string @@ -361,41 +357,41 @@ void sendFileToWs(String filename, int num, size_t frameSize) { //особая функция отправки графиков в веб void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, String id) { - String json; - jsonWriteStr(json, "topic", mqttRootDevice + "/" + id); - jsonWriteInt(json, "maxCount", maxCount); - - String st = "/st/chart.json|" + json; - if (num == -1) { - standWebSocket.broadcastTXT(st); - } else { - standWebSocket.sendTXT(num, st); - } - String path = filepath(filename); - auto file = FileFS.open(path, "r"); - if (!file) { - SerialPrint(F("E"), F("FS"), F("reed file error")); - return; - } - size_t fileSize = file.size(); - SerialPrint(F("i"), F("FS"), "Send file '" + String(filename) + "', file size: " + String(fileSize)); - uint8_t payload[frameSize]; - int countRead = file.read(payload, sizeof(payload)); - while (countRead > 0) { - if (num == -1) { - standWebSocket.broadcastBIN(payload, countRead); - } else { - standWebSocket.sendBIN(num, payload, countRead); - } - countRead = file.read(payload, sizeof(payload)); - } - file.close(); - String end = "/end/chart.json|" + json; - if (num == -1) { - standWebSocket.broadcastTXT(end); - } else { - standWebSocket.sendTXT(num, end); - } + // String json; + // jsonWriteStr(json, "topic", mqttRootDevice + "/" + id); + // jsonWriteInt(json, "maxCount", maxCount); + // + // String st = "/st/chart.json|" + json; + // if (num == -1) { + // standWebSocket.broadcastTXT(st); + //} else { + // standWebSocket.sendTXT(num, st); + //} + // String path = filepath(filename); + // auto file = FileFS.open(path, "r"); + // if (!file) { + // SerialPrint(F("E"), F("FS"), F("reed file error")); + // return; + //} + // size_t fileSize = file.size(); + // SerialPrint(F("i"), F("FS"), "Send file '" + String(filename) + "', file size: " + String(fileSize)); + // uint8_t payload[frameSize]; + // int countRead = file.read(payload, sizeof(payload)); + // while (countRead > 0) { + // if (num == -1) { + // standWebSocket.broadcastBIN(payload, countRead); + // } else { + // standWebSocket.sendBIN(num, payload, countRead); + // } + // countRead = file.read(payload, sizeof(payload)); + //} + // file.close(); + // String end = "/end/chart.json|" + json; + // if (num == -1) { + // standWebSocket.broadcastTXT(end); + //} else { + // standWebSocket.sendTXT(num, end); + //} } void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize) { @@ -412,7 +408,7 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St } size_t totalSize = file.size(); - SerialPrint("I", "FS", "Send file '" + String(filename) + "', file size: " + String(totalSize)); + // Serial.println("Send file '" + String(filename) + "', file size: " + String(totalSize)); char buf[32]; sprintf(buf, "%04d", json.length() + 12); @@ -452,7 +448,7 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St continuation = true; } - SerialPrint("I", "FS", String(i) + ") fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); + // Serial.println(String(i) + ") fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); } i++; @@ -470,7 +466,7 @@ void sendStringToWs(const String& header, String& payload, int client_id) { String msg = header + "|0012|" + payload; size_t totalSize = msg.length(); - SerialPrint("I", "FS", "Send string '" + header + "', str size: " + String(totalSize)); + // Serial.println("Send string '" + header + "', str size: " + String(totalSize)); char dataArray[totalSize]; msg.toCharArray(dataArray, totalSize + 1); diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index d7649e1f..72048ec6 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -228,14 +228,14 @@ class Loging : public IoTItem { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":0,\"topic\":\"" + topic + "\",\"status\":[]}"; String pk = "/string/chart.json|" + json; - standWebSocket.broadcastTXT(pk); + //standWebSocket.broadcastTXT(pk); } void publishChartToWsSinglePoint(String value) { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; String pk = "/string/chart.json|" + json; - standWebSocket.broadcastTXT(pk); + //standWebSocket.broadcastTXT(pk); } void setPublishDestination(int publishType, int wsNum = -1) { diff --git a/src/modules/virtual/LogingDaily/LogingDaily.cpp b/src/modules/virtual/LogingDaily/LogingDaily.cpp index fa4d5f59..529ca3ef 100644 --- a/src/modules/virtual/LogingDaily/LogingDaily.cpp +++ b/src/modules/virtual/LogingDaily/LogingDaily.cpp @@ -192,7 +192,7 @@ class LogingDaily : public IoTItem { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; String pk = "/string/chart.json|" + json; - standWebSocket.broadcastTXT(pk); + //standWebSocket.broadcastTXT(pk); } void setPublishDestination(int publishType, int wsNum = -1) { diff --git a/src/utils/SerialPrint.cpp b/src/utils/SerialPrint.cpp index ce713c12..6287029c 100644 --- a/src/utils/SerialPrint.cpp +++ b/src/utils/SerialPrint.cpp @@ -6,15 +6,14 @@ void SerialPrint(String errorLevel, String module, String msg) { tosend = prettyMillis(millis()); - // if (module == "Loging") { tosend = tosend + " [" + errorLevel + "] [" + module + "] " + msg; Serial.println(tosend); if (isNetworkActive()) { if (jsonReadInt(settingsFlashJson, F("log")) != 0) { - String pl = "/log|" + tosend; - standWebSocket.broadcastTXT(pl); + // String pl = "/log|" + tosend; + // standWebSocket.broadcastTXT(pl); + sendStringToWs("corelg", tosend, -1); } } - //} } \ No newline at end of file From db69c96ea054079a0a06050b081d3547c276b836 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Sun, 9 Oct 2022 20:17:02 +0200 Subject: [PATCH 010/107] =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=B4=D0=BE=D0=BB?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= =?UTF-8?q?!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/settings.json | 1 - src/DeviceList.cpp | 2 +- src/EspFileSystem.cpp | 4 ++-- src/WsServer.cpp | 6 +++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/data_svelte/settings.json b/data_svelte/settings.json index f2b7d36c..bfeb5544 100644 --- a/data_svelte/settings.json +++ b/data_svelte/settings.json @@ -1,5 +1,4 @@ { - "settings_": "", "name": "IoTmanagerVer4", "apssid": "IoTmanager", "appass": "", diff --git a/src/DeviceList.cpp b/src/DeviceList.cpp index c9998917..1b817d98 100644 --- a/src/DeviceList.cpp +++ b/src/DeviceList.cpp @@ -2,7 +2,7 @@ const String getThisDevice() { String thisDevice = "{}"; - jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга + // jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга jsonWriteStr_(thisDevice, F("ip"), jsonReadStr(settingsFlashJson, F("ip"))); jsonWriteStr_(thisDevice, F("id"), jsonReadStr(settingsFlashJson, F("id"))); jsonWriteStr_(thisDevice, F("name"), jsonReadStr(settingsFlashJson, F("name"))); diff --git a/src/EspFileSystem.cpp b/src/EspFileSystem.cpp index 91dd4bf1..817aa8bf 100644 --- a/src/EspFileSystem.cpp +++ b/src/EspFileSystem.cpp @@ -19,8 +19,8 @@ void globalVarsSync() { jsonWriteStr_(settingsFlashJson, "root", mqttRootDevice); jsonWriteStr_(settingsFlashJson, "id", chipId); - jsonWriteStr_(errorsHeapJson, "errors_", ""); //метка для парсинга - jsonWriteStr_(ssidListHeapJson, "ssids_", ""); //метка для парсинга + // jsonWriteStr_(errorsHeapJson, "errors_", ""); //метка для парсинга + // jsonWriteStr_(ssidListHeapJson, "ssids_", ""); //метка для парсинга } String getParamsJson() { diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 9014f600..fb7bd9a8 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -56,19 +56,19 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //отвечаем данными на запрос страницы if (headerStr == "/|") { - sendFileToWs("/layout.json", num, 1024); + sendFileToWsByFrames("/layout.json", "layout", "", num, WEB_SOCKETS_FRAME_SIZE); } //отвечаем на запрос параметров if (headerStr == "/params|") { String params = "{}"; - jsonWriteStr(params, "params_", ""); //метка для парсинга + // jsonWriteStr(params, "params_", ""); //метка для парсинга for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getSubtype() != "Loging") { if ((*it)->iAmLocal) jsonWriteStr(params, (*it)->getID(), (*it)->getValue()); } } - standWebSocket.sendTXT(num, params); + sendStringToWs("params", params, num); } //отвечаем на запрос графиков From caff040a91a2c2ff601d41ee50ee0438927042c9 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Sun, 9 Oct 2022 21:38:01 +0200 Subject: [PATCH 011/107] =?UTF-8?q?=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B3=D1=80?= =?UTF-8?q?=D0=B0=D1=84=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/DeviceList.cpp | 4 +-- src/EspFileSystem.cpp | 4 +-- src/WsServer.cpp | 31 ++++++--------------- src/modules/virtual/Loging/Loging.cpp | 40 ++++++++++++++++----------- 4 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/DeviceList.cpp b/src/DeviceList.cpp index 1b817d98..be3e4b2d 100644 --- a/src/DeviceList.cpp +++ b/src/DeviceList.cpp @@ -2,7 +2,7 @@ const String getThisDevice() { String thisDevice = "{}"; - // jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга + jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга нужна для udp валидации jsonWriteStr_(thisDevice, F("ip"), jsonReadStr(settingsFlashJson, F("ip"))); jsonWriteStr_(thisDevice, F("id"), jsonReadStr(settingsFlashJson, F("id"))); jsonWriteStr_(thisDevice, F("name"), jsonReadStr(settingsFlashJson, F("name"))); @@ -68,7 +68,7 @@ void asyncUdpInit() { } bool udpPacketValidation(String& data) { - if (data.indexOf("devicelist") != -1) { + if (data.indexOf("devicelist_") != -1) { return true; } else { return false; diff --git a/src/EspFileSystem.cpp b/src/EspFileSystem.cpp index 817aa8bf..9ede484a 100644 --- a/src/EspFileSystem.cpp +++ b/src/EspFileSystem.cpp @@ -19,8 +19,8 @@ void globalVarsSync() { jsonWriteStr_(settingsFlashJson, "root", mqttRootDevice); jsonWriteStr_(settingsFlashJson, "id", chipId); - // jsonWriteStr_(errorsHeapJson, "errors_", ""); //метка для парсинга - // jsonWriteStr_(ssidListHeapJson, "ssids_", ""); //метка для парсинга + // jsonWriteStr_(errorsHeapJson, "errors_", ""); //метка для парсинга удалить + // jsonWriteStr_(ssidListHeapJson, "ssids_", ""); //метка для парсинга удалить } String getParamsJson() { diff --git a/src/WsServer.cpp b/src/WsServer.cpp index fb7bd9a8..f4de865e 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -62,7 +62,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //отвечаем на запрос параметров if (headerStr == "/params|") { String params = "{}"; - // jsonWriteStr(params, "params_", ""); //метка для парсинга + // jsonWriteStr(params, "params_", ""); //метка для парсинга удалить for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getSubtype() != "Loging") { if ((*it)->iAmLocal) jsonWriteStr(params, (*it)->getID(), (*it)->getValue()); @@ -258,28 +258,13 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) //публикация статус сообщений (недостаток в том что делаем бродкаст всем клиентам поднятым в свелте!!!) void publishStatusWs(const String& topic, const String& data) { - // String path = mqttRootDevice + "/" + topic; - // String json = "{}"; - // jsonWriteStr(json, "status", data); - // jsonWriteStr(json, "topic", path); - // standWebSocket.broadcastTXT(json); + String path = mqttRootDevice + "/" + topic; + String json = "{}"; + jsonWriteStr(json, "status", data); + jsonWriteStr(json, "topic", path); + sendStringToWs("status", json, -1); } -//публикация статус сообщений -// void publishChartWs2(int num, String& data) { -// bool ok = false; -// if (num == -1) { -// ok = standWebSocket.broadcastTXT(data); -// } else { -// ok = standWebSocket.sendTXT(num, data); -// } -// if (ok) { -// SerialPrint(F("i"), F("WS"), F("sent sucsess")); -// } else { -// SerialPrint(F("E"), F("WS"), F("sent error")); -// } -//} - void publishChartWs(int num, String& path) { sendFileToWs(path, num, 1000); } @@ -407,8 +392,8 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St return; } - size_t totalSize = file.size(); - // Serial.println("Send file '" + String(filename) + "', file size: " + String(totalSize)); + // size_t totalSize = file.size(); + // Serial.println("Send file '" + String(filename) + "', file size: " + String(totalSize)); char buf[32]; sprintf(buf, "%04d", json.length() + 12); diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index 72048ec6..edc80831 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -176,13 +176,15 @@ class Loging : public IoTItem { unsigned long reqUnixTime = strDateToUnix(getItemValue(id + "-date")); if (fileUnixTimeLocal > reqUnixTime && fileUnixTimeLocal < reqUnixTime + 86400) { noData = false; + String json = getAdditionalJson(); if (_publishType == TO_MQTT) { publishChartFileToMqtt(path, id, calculateMaxCount()); } else if (_publishType == TO_WS) { - publishChartToWs(path, _wsNum, 1000, calculateMaxCount(), id); + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + } else if (_publishType == TO_MQTT_WS) { publishChartFileToMqtt(path, id, calculateMaxCount()); - publishChartToWs(path, _wsNum, 1000, calculateMaxCount(), id); + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); } SerialPrint("i", F("Loging"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", sent"); } else { @@ -197,6 +199,26 @@ class Loging : public IoTItem { } } + String getAdditionalJson() { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\"}"; + return json; + } + + void publishChartToWsSinglePoint(String value) { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; + String pk = "/string/chart.json|" + json; + // standWebSocket.broadcastTXT(pk); + } + + void clearValue() { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":0,\"topic\":\"" + topic + "\",\"status\":[]}"; + String pk = "/string/chart.json|" + json; + // standWebSocket.broadcastTXT(pk); + } + void clearHistory() { String dir = "/lg/" + id; cleanDirectory(dir); @@ -224,20 +246,6 @@ class Loging : public IoTItem { } } - void clearValue() { - String topic = mqttRootDevice + "/" + id; - String json = "{\"maxCount\":0,\"topic\":\"" + topic + "\",\"status\":[]}"; - String pk = "/string/chart.json|" + json; - //standWebSocket.broadcastTXT(pk); - } - - void publishChartToWsSinglePoint(String value) { - String topic = mqttRootDevice + "/" + id; - String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; - String pk = "/string/chart.json|" + json; - //standWebSocket.broadcastTXT(pk); - } - void setPublishDestination(int publishType, int wsNum = -1) { _publishType = publishType; _wsNum = wsNum; From 1668ab8aa87488af46edfcfcc438a007513a0c0d Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Mon, 10 Oct 2022 00:16:58 +0200 Subject: [PATCH 012/107] =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B0?= =?UTF-8?q?=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/WsServer.h | 9 +- src/DeviceList.cpp | 2 +- src/WsServer.cpp | 209 +----------------- src/modules/virtual/Loging/Loging.cpp | 14 +- .../virtual/LogingDaily/LogingDaily.cpp | 24 +- 5 files changed, 28 insertions(+), 230 deletions(-) diff --git a/include/WsServer.h b/include/WsServer.h index 8c88e92d..ff08d671 100644 --- a/include/WsServer.h +++ b/include/WsServer.h @@ -13,16 +13,9 @@ extern void hexdump(const void* mem, uint32_t len, uint8_t cols); #endif #endif -void sendFileToWs(String filename, int num, size_t frameSize); void publishStatusWs(const String& topic, const String& data); void publishChartWs(int num, String& path); void periodicWsSend(); -void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, String id); - void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize); -void sendStringToWs(const String& header, String& payload, int client_id); - -// void sendMark(const char* filename, const char* mark, uint8_t num); -// void sendFileToWs3(const String& filename, uint8_t num); -// void sendFileToWs4(const String& filename, uint8_t num); \ No newline at end of file +void sendStringToWs(const String& header, String& payload, int client_id); \ No newline at end of file diff --git a/src/DeviceList.cpp b/src/DeviceList.cpp index be3e4b2d..d2f97724 100644 --- a/src/DeviceList.cpp +++ b/src/DeviceList.cpp @@ -2,7 +2,7 @@ const String getThisDevice() { String thisDevice = "{}"; - jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга нужна для udp валидации + jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга нужна для udp валидации может быть рабочей группой в последствии jsonWriteStr_(thisDevice, F("ip"), jsonReadStr(settingsFlashJson, F("ip"))); jsonWriteStr_(thisDevice, F("id"), jsonReadStr(settingsFlashJson, F("id"))); jsonWriteStr_(thisDevice, F("name"), jsonReadStr(settingsFlashJson, F("name"))); diff --git a/src/WsServer.cpp b/src/WsServer.cpp index f4de865e..b6f03380 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -266,7 +266,7 @@ void publishStatusWs(const String& topic, const String& data) { } void publishChartWs(int num, String& path) { - sendFileToWs(path, num, 1000); + // sendFileToWs(path, num, 1000); } //данные которые мы отправляем в сокеты переодически @@ -292,93 +292,6 @@ void hexdump(const void* mem, uint32_t len, uint8_t cols = 16) { #endif #endif -//посылка данных из файла в бинарном виде -void sendFileToWs(String filename, int num, size_t frameSize) { - // String st = "/st" + String(filename); - // if (num == -1) { - // standWebSocket.broadcastTXT(st); - // } else { - // standWebSocket.sendTXT(num, st); - // } - // - // String path = filepath(filename); - // auto file = FileFS.open(path, "r"); - // if (!file) { - // SerialPrint(F("E"), F("FS"), F("reed file error")); - // return; - //} - // size_t fileSize = file.size(); - // SerialPrint(F("i"), F("FS"), "Send file '" + String(filename) + "', file size: " + String(fileSize)); - // uint8_t payload[frameSize]; - // int countRead = file.read(payload, sizeof(payload)); - // while (countRead > 0) { - // if (num == -1) { - // standWebSocket.broadcastBIN(payload, countRead); - // } else { - // standWebSocket.sendBIN(num, payload, countRead); - // } - // countRead = file.read(payload, sizeof(payload)); - //} - // file.close(); - // String end = "/end" + String(filename); - // if (num == -1) { - // standWebSocket.broadcastTXT(end); - //} else { - // standWebSocket.sendTXT(num, end); - //} -} - -//посылка данных из string -// void sendStringToWs(const String& msg, uint8_t num, String name) { -// String st = "/st" + String(name); -// standWebSocket.sendTXT(num, st); -// size_t size = msg.length(); -// char dataArray[size]; -// msg.toCharArray(dataArray, size); -// standWebSocket.sendBIN(num, (uint8_t*)dataArray, size); -// String end = "/end" + String(name); -// standWebSocket.sendTXT(num, end); -//} - -//особая функция отправки графиков в веб -void publishChartToWs(String filename, int num, size_t frameSize, int maxCount, String id) { - // String json; - // jsonWriteStr(json, "topic", mqttRootDevice + "/" + id); - // jsonWriteInt(json, "maxCount", maxCount); - // - // String st = "/st/chart.json|" + json; - // if (num == -1) { - // standWebSocket.broadcastTXT(st); - //} else { - // standWebSocket.sendTXT(num, st); - //} - // String path = filepath(filename); - // auto file = FileFS.open(path, "r"); - // if (!file) { - // SerialPrint(F("E"), F("FS"), F("reed file error")); - // return; - //} - // size_t fileSize = file.size(); - // SerialPrint(F("i"), F("FS"), "Send file '" + String(filename) + "', file size: " + String(fileSize)); - // uint8_t payload[frameSize]; - // int countRead = file.read(payload, sizeof(payload)); - // while (countRead > 0) { - // if (num == -1) { - // standWebSocket.broadcastBIN(payload, countRead); - // } else { - // standWebSocket.sendBIN(num, payload, countRead); - // } - // countRead = file.read(payload, sizeof(payload)); - //} - // file.close(); - // String end = "/end/chart.json|" + json; - // if (num == -1) { - // standWebSocket.broadcastTXT(end); - //} else { - // standWebSocket.sendTXT(num, end); - //} -} - void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize) { if (header.length() != 6) { SerialPrint("E", "FS", F("wrong header size")); @@ -434,7 +347,11 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St } // Serial.println(String(i) + ") fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); - standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); + if (client_id == -1) { + standWebSocket.broadcastBIN(frameBuf, size, fin, continuation); + } else { + standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); + } } i++; } @@ -461,117 +378,3 @@ void sendStringToWs(const String& header, String& payload, int client_id) { standWebSocket.sendBIN(client_id, (uint8_t*)dataArray, totalSize); } } - -// void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize) { -// if (header.length() != 6) { -// SerialPrint("E", "FS", F("wrong header size")); -// return; -// } -// // откроем файл -// auto path = filepath(filename); -// auto file = FileFS.open(path, "r"); -// if (!file) { -// SerialPrint("E", "FS", F("reed file error")); -// return; -// } -// -// size_t totalSize = file.size(); -// SerialPrint("I", "FS", "Send file '" + String(filename) + "', file size: " + String(totalSize)); -// -// char buf[32]; -// sprintf(buf, "%04d", json.length() + 12); -// -// String data = header + "|" + String(buf) + "|" + json; -// -// // размер заголовка -// auto headerSize = data.length(); -// // выделим буфер размером с фрейм -// auto frameBuf = new uint8_t[frameSize]; -// // заголовок у нас не меняется, запием его в начало буфера -// data.toCharArray((char*)frameBuf, frameSize); -// // указатель на начало полезной нагрузки -// auto payloadBuf = &frameBuf[headerSize]; -// // и сколько осталось места для нее -// auto maxPayloadSize = frameSize - headerSize; -// int i = 0; -// while (file.available()) { -// // прочитаем кусок в буфер -// size_t payloadSize = file.read(payloadBuf, maxPayloadSize); -// if (payloadSize) { -// size_t size = headerSize + payloadSize; -// -// bool fin = false; -// if (size == frameSize) { -// fin = false; -// } else { -// fin = true; -// } -// -// bool continuation = false; -// if (i == 0) { -// continuation = false; -// } else { -// continuation = true; -// } -// -// SerialPrint("I", "FS", String(i) + ") sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); -// standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); -// } -// i++; -// } -// } - -// void sendMark(const char* filename, const char* mark, uint8_t num) { -// char outChar[strlen(filename) + strlen(mark) + 1]; -// strcpy(outChar, mark); -// strcat(outChar, filename); -// size_t size = strlen(outChar); -// uint8_t outUint[size]; -// for (size_t i = 0; i < size; i++) { -// outUint[i] = uint8_t(outChar[i]); -// } -// standWebSocket.sendBIN(num, outUint, sizeof(outUint)); -// } - -//посылка данных из файла в string -// void sendFileToWs3(const String& filename, uint8_t num) { -// standWebSocket.sendTXT(num, "/st" + filename); -// size_t ws_buffer = 512; -// String path = filepath(filename); -// auto file = FileFS.open(path, "r"); -// if (!file) { -// SerialPrint(F("E"), F("FS"), F("reed file error")); -// } -// size_t fileSize = file.size(); -// SerialPrint(F("i"), F("WS"), "Send file '" + filename + "', file size: " + String(fileSize)); -// String ret; -// char temp[ws_buffer + 1]; -// int countRead = file.readBytes(temp, sizeof(temp) - 1); -// while (countRead > 0) { -// temp[countRead] = 0; -// ret = temp; -// standWebSocket.sendTXT(num, ret); -// countRead = file.readBytes(temp, sizeof(temp) - 1); -// } -// standWebSocket.sendTXT(num, "/end" + filename); -//} - -//посылка данных из файла в char -// void sendFileToWs4(const String& filename, uint8_t num) { -// standWebSocket.sendTXT(num, "/st" + filename); -// size_t ws_buffer = 512; -// String path = filepath(filename); -// auto file = FileFS.open(path, "r"); -// if (!file) { -// SerialPrint(F("E"), F("FS"), F("reed file error")); -// } -// size_t fileSize = file.size(); -// SerialPrint(F("i"), F("WS"), "Send file '" + filename + "', file size: " + String(fileSize)); -// char temp[ws_buffer + 1]; -// int countRead = file.readBytes(temp, sizeof(temp) - 1); -// while (countRead > 0) { -// temp[countRead] = 0; -// standWebSocket.sendTXT(num, temp, countRead); -// countRead = file.readBytes(temp, sizeof(temp) - 1); -// } -//} diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index edc80831..c0aea13b 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -181,10 +181,9 @@ class Loging : public IoTItem { publishChartFileToMqtt(path, id, calculateMaxCount()); } else if (_publishType == TO_WS) { sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); - } else if (_publishType == TO_MQTT_WS) { - publishChartFileToMqtt(path, id, calculateMaxCount()); sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); + publishChartFileToMqtt(path, id, calculateMaxCount()); } SerialPrint("i", F("Loging"), String(f) + ") " + path + ", " + getDateTimeDotFormatedFromUnix(fileUnixTimeLocal) + ", sent"); } else { @@ -208,15 +207,13 @@ class Loging : public IoTItem { void publishChartToWsSinglePoint(String value) { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; - String pk = "/string/chart.json|" + json; - // standWebSocket.broadcastTXT(pk); + sendStringToWs("chartb", json, -1); } void clearValue() { String topic = mqttRootDevice + "/" + id; String json = "{\"maxCount\":0,\"topic\":\"" + topic + "\",\"status\":[]}"; - String pk = "/string/chart.json|" + json; - // standWebSocket.broadcastTXT(pk); + sendStringToWs("chartb", json, -1); } void clearHistory() { @@ -246,7 +243,7 @@ class Loging : public IoTItem { } } - void setPublishDestination(int publishType, int wsNum = -1) { + void setPublishDestination(int publishType, int wsNum) { _publishType = publishType; _wsNum = wsNum; } @@ -321,8 +318,7 @@ class Date : public IoTItem { for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getSubtype() == "Loging") { if ((*it)->getID() == selectToMarker(id, "-")) { - (*it)->setPublishDestination(TO_MQTT_WS); - (*it)->clearValue(); + (*it)->setPublishDestination(TO_MQTT_WS, -1); (*it)->publishValue(); } } diff --git a/src/modules/virtual/LogingDaily/LogingDaily.cpp b/src/modules/virtual/LogingDaily/LogingDaily.cpp index 529ca3ef..05981d39 100644 --- a/src/modules/virtual/LogingDaily/LogingDaily.cpp +++ b/src/modules/virtual/LogingDaily/LogingDaily.cpp @@ -168,14 +168,14 @@ class LogingDaily : public IoTItem { path = "/lgd/" + id + path; f++; - + String json = getAdditionalJson(); if (_publishType == TO_MQTT) { publishChartFileToMqtt(path, id, calculateMaxCount()); } else if (_publishType == TO_WS) { - publishChartToWs(path, _wsNum, 1000, calculateMaxCount(), id); + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); } else if (_publishType == TO_MQTT_WS) { publishChartFileToMqtt(path, id, calculateMaxCount()); - publishChartToWs(path, _wsNum, 1000, calculateMaxCount(), id); + sendFileToWsByFrames(path, "charta", json, _wsNum, WEB_SOCKETS_FRAME_SIZE); } SerialPrint("i", F("LogingDaily"), String(f) + ") " + path + ", sent"); @@ -183,17 +183,23 @@ class LogingDaily : public IoTItem { } } + String getAdditionalJson() { + String topic = mqttRootDevice + "/" + id; + String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\"}"; + return json; + } + void clearHistory() { String dir = "/lgd/" + id; cleanDirectory(dir); } - void publishChartToWsSinglePoint(String value) { - String topic = mqttRootDevice + "/" + id; - String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; - String pk = "/string/chart.json|" + json; - //standWebSocket.broadcastTXT(pk); - } + // void publishChartToWsSinglePoint(String value) { + // String topic = mqttRootDevice + "/" + id; + // String json = "{\"maxCount\":" + String(calculateMaxCount()) + ",\"topic\":\"" + topic + "\",\"status\":[{\"x\":" + String(unixTime) + ",\"y1\":" + value + "}]}"; + // String pk = "/string/chart.json|" + json; + // standWebSocket.broadcastTXT(pk); + // } void setPublishDestination(int publishType, int wsNum = -1) { _publishType = publishType; From c1283bc5b201b4e567b1e2be7aab85c1a58eedb1 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Mon, 10 Oct 2022 00:21:22 +0200 Subject: [PATCH 013/107] =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=B2?= =?UTF-8?q?=D0=B5=D0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/build/bundle.css | 40 -------------------------------- data_svelte/build/bundle.css.gz | Bin 5492 -> 5492 bytes data_svelte/build/bundle.js | 2 -- data_svelte/build/bundle.js.gz | Bin 47654 -> 47064 bytes data_svelte/index.html | 6 ++--- 5 files changed, 3 insertions(+), 45 deletions(-) delete mode 100644 data_svelte/build/bundle.css delete mode 100644 data_svelte/build/bundle.js diff --git a/data_svelte/build/bundle.css b/data_svelte/build/bundle.css deleted file mode 100644 index 40e33a4a..00000000 --- a/data_svelte/build/bundle.css +++ /dev/null @@ -1,40 +0,0 @@ -*,::before,::after{box-sizing:border-box}html{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui, - -apple-system, /* Firefox supports this but not yet `system-ui` */ - 'Segoe UI', - Roboto, - Helvetica, - Arial, - sans-serif, - 'Apple Color Emoji', - 'Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, - SFMono-Regular, - Consolas, - 'Liberation Mono', - Menlo, - monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type='button']{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}pre,code,kbd,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::before,::after{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.grd-1col1{display:grid;grid-template-columns:repeat(1, minmax(0, 1fr));justify-items:center}.grd-2col1{display:grid;grid-template-columns:repeat(1, minmax(0, 1fr));justify-items:center;gap:1rem}@media(min-width: 640px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1024px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1280px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1536px){.grd-2col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}.grd-2col2{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));justify-items:center;gap:1rem}.grd-3col1{display:grid;grid-template-columns:repeat(1, minmax(0, 1fr));justify-items:center;gap:1rem}@media(min-width: 640px){.grd-3col1{grid-template-columns:repeat(2, minmax(0, 1fr))}}@media(min-width: 1024px){.grd-3col1{grid-template-columns:repeat(3, minmax(0, 1fr))}}@media(min-width: 1280px){.grd-3col1{grid-template-columns:repeat(3, minmax(0, 1fr))}}@media(min-width: 1536px){.grd-3col1{grid-template-columns:repeat(3, minmax(0, 1fr))}}.crd-itm-psn{margin-bottom:0.5rem;display:flex;height:2rem;align-items:center}.wgt-dscr-stl{padding-right:1rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.wgt-adt-stl{text-align:center;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.tbl{margin-top:0.5rem;margin-bottom:0.5rem;width:100%;table-layout:fixed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.tbl-hd{overflow-wrap:break-word;padding-left:0.25rem;padding-right:0.25rem;text-align:center;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.tbl-bdy-lg{overflow-wrap:break-word;padding-left:0.25rem;padding-right:0.25rem;text-align:center}.tbl-bdy-sm{overflow-wrap:break-word;padding-left:0.25rem;padding-right:0.25rem}.ipt-lg{margin-top:0.5rem;height:1rem;align-content:center;border-width:2px;--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.ipt-lg:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.ipt-lg{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-lg:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-lg{text-align:center;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-lg:focus{outline:2px solid transparent;outline-offset:2px}@media(min-width: 640px){.ipt-lg{height:1.75rem}}@media(min-width: 768px){.ipt-lg{height:1.75rem}}@media(min-width: 1024px){.ipt-lg{height:1.75rem}}@media(min-width: 1280px){.ipt-lg{height:1.75rem}}@media(min-width: 1536px){.ipt-lg{height:1.75rem}}.ipt-sm{height:0.75rem;align-content:center;border-radius:0.125rem;border-width:2px;--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.ipt-sm:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.ipt-sm{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-sm:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-sm{text-align:center;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-sm:focus{outline:2px solid transparent;outline-offset:2px}@media(min-width: 640px){.ipt-sm{height:1.5rem}}@media(min-width: 768px){.ipt-sm{height:1.5rem}}@media(min-width: 1024px){.ipt-sm{height:1.5rem}}@media(min-width: 1280px){.ipt-sm{height:1.5rem}}@media(min-width: 1536px){.ipt-sm{height:1.5rem}}.ipt-rnd{height:2rem;width:100%;align-content:center;border-radius:0.25rem;border-width:2px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-rnd:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-rnd{padding-left:0.5rem;padding-right:0.5rem;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-rnd:focus{outline:2px solid transparent;outline-offset:2px}.ipt-big{height:2rem;width:100%;align-content:center;border-radius:0.25rem;border-width:2px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.ipt-big:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.ipt-big{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.ipt-big:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.ipt-big{padding-left:0.5rem;padding-right:0.5rem;line-height:1.25;--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.ipt-big:focus{outline:2px solid transparent;outline-offset:2px}.txt-ita{display:inline-block;text-align:right;vertical-align:top;font-style:italic;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.txt-pad{padding-left:0.5rem;padding-right:0.5rem;padding-top:0px;padding-bottom:0px}@media(min-width: 640px){.txt-pad{padding-top:0px;padding-bottom:0px}}@media(min-width: 768px){.txt-pad{padding-top:0px;padding-bottom:0px}}@media(min-width: 1024px){.txt-pad{padding-top:0.25rem;padding-bottom:0.25rem}}@media(min-width: 1280px){.txt-pad{padding-top:0.5rem;padding-bottom:0.5rem}}@media(min-width: 1536px){.txt-pad{padding-top:0.5rem;padding-bottom:0.5rem}}.txt-sz{font-size:.5rem}@media(min-width: 640px){.txt-sz{font-size:1rem}}@media(min-width: 768px){.txt-sz{font-size:1rem}}@media(min-width: 1024px){.txt-sz{font-size:1rem}}@media(min-width: 1280px){.txt-sz{font-size:1rem}}@media(min-width: 1536px){.txt-sz{font-size:1rem}}.btn-lg{margin-top:0px;display:flex;height:1.5rem;width:100%;align-content:center;justify-content:center;overflow-wrap:break-word;border-radius:0.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.btn-lg:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.btn-lg{font-size:.875rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}@media(min-width: 640px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 768px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 1024px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 1280px){.btn-lg{height:2rem;font-size:1rem}}@media(min-width: 1536px){.btn-lg{height:2rem;font-size:1rem}}.slct-lg{margin-bottom:0px;display:flex;height:1.5rem;width:100%;align-content:center;justify-content:center;overflow-wrap:break-word;border-radius:0.25rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.slct-lg:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.slct-lg{font-size:.875rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}@media(min-width: 640px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 768px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 1024px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 1280px){.slct-lg{height:2rem;font-size:1rem}}@media(min-width: 1536px){.slct-lg{height:2rem;font-size:1rem}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0px;right:0px;bottom:0px;left:0px}.top-1{top:0.25rem}.left-1{left:0.25rem}.z-10{z-index:10}.z-50{z-index:50}.m-auto{margin:auto}.mt-0{margin-top:0px}.mt-2{margin-top:0.5rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mb-0{margin-bottom:0px}.mb-2{margin-bottom:0.5rem}.ml-0{margin-left:0px}.ml-36{margin-left:9rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-2{height:0.5rem}.h-3{height:0.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-40{height:10rem}.h-80{height:20rem}.h-auto{height:auto}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-1\/12{width:8.333333%}.w-11\/12{width:91.666667%}.w-full{width:100%}.flex-1{flex:1 1 0%}.table-fixed{table-layout:fixed}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}@-webkit-keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}@keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.flex-col{flex-direction:column}.content-center{align-content:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:0.25rem}.rounded-md{border-radius:0.375rem}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.border-2{border-width:2px}.border-4{border-width:4px}.border{border-width:1px}.border-solid{border-style:solid}.border-transparent{border-color:transparent}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239, 68, 68, var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgba(96, 165, 250, var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243, 244, 246, var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgba(209, 213, 219, var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(107, 114, 128, var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75, 85, 99, var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgba(254, 242, 242, var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgba(252, 165, 165, var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220, 38, 38, var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgba(255, 251, 235, var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgba(236, 253, 245, var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgba(239, 246, 255, var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgba(37, 99, 235, var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgba(99, 102, 241, var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185, 28, 28, var(--tw-bg-opacity))}.hover\:bg-blue-200:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgba(67, 56, 202, var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-cover{background-size:cover}.p-0{padding:0px}.p-2{padding:0.5rem}.px-1{padding-left:0.25rem;padding-right:0.25rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0px;padding-bottom:0px}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.pt-0{padding-top:0px}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pb-20{padding-bottom:5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.text-xxs{font-size:.5rem}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgba(0, 0, 0, var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255, 255, 255, var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17, 24, 39, var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239, 68, 68, var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245, 158, 11, var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgba(52, 211, 153, var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16, 185, 129, var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgba(59, 130, 246, var(--tw-text-opacity))}*,::before,::after{--tw-shadow:0 0 #0000}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}*,::before,::after{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(239, 68, 68, var(--tw-ring-opacity))}.focus\:ring-indigo-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(129, 140, 248, var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(99, 102, 241, var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition{transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}#menu__toggle{position:relative;opacity:0}#menu__toggle:checked~.menu__btn>span{transform:rotate(45deg)}#menu__toggle:checked~.menu__btn>span::before{top:0;transform:rotate(0)}#menu__toggle:checked~.menu__btn>span::after{top:0;transform:rotate(90deg)}#menu__toggle:checked~.menu__box{visibility:visible;left:0}#menu__toggle:checked~.menu__main{margin-left:150px;transition-duration:0.25s}.menu__btn{display:flex;align-items:center;position:fixed;z-index:1;top:10px;left:20px;width:20px;height:20px;cursor:pointer}.menu__btn>span,.menu__btn>span::before,.menu__btn>span::after{display:block;position:absolute;width:100%;height:2px;background-color:#616161;transition-duration:0.25s}.menu__btn>span::before{content:"";top:-8px}.menu__btn>span::after{content:"";top:8px}.menu__box{display:block;position:fixed;visibility:hidden;top:0;left:-100%;width:150px;height:100%;margin:0;padding:80px 0;list-style:none;background-color:#eceff1;box-shadow:1px 0px 6px rgba(0, 0, 0, 0.2);transition-duration:0.25s}.menu__item{display:block;padding:12px 24px;color:rgba(51, 51, 51, 0.788);font-family:"Roboto", sans-serif;font-size:15px;font-weight:600;text-decoration:none;transition-duration:0.25s}.menu__item:hover{background-color:#cfd8dc}.upper__bar{background-color:rgba(51, 51, 51, 0.144);height:70px;position:fixed;z-index:-1;top:0px;left:0;width:100%;margin:0;padding:0;box-shadow:1px 0px 3px rgba(0, 0, 0, 0.2)}input[type="date"]::-webkit-calendar-picker-indicator{margin-left:5px;margin-right:-8px}input[type="time"]::-webkit-calendar-picker-indicator{margin-left:5px;margin-right:-8px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{margin-left:7px;margin-right:-6px;width:30px;height:30px;opacity:1}input:checked~.dot{transform:translateX(100%)}@media(min-width: 640px){.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:mt-0{margin-top:0px}.sm\:ml-3{margin-left:0.75rem}.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:h-6{height:1.5rem}.sm\:h-7{height:1.75rem}.sm\:h-8{height:2rem}.sm\:h-screen{height:100vh}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-start{align-items:flex-start}.sm\:p-0{padding:0px}.sm\:p-2{padding:0.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-0{padding-top:0px;padding-bottom:0px}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem}.sm\:text-base{font-size:1rem}}@media(min-width: 768px){.md\:h-6{height:1.5rem}.md\:h-7{height:1.75rem}.md\:h-8{height:2rem}.md\:p-2{padding:0.5rem}.md\:py-0{padding-top:0px;padding-bottom:0px}.md\:text-base{font-size:1rem}}@media(min-width: 1024px){.lg\:h-6{height:1.5rem}.lg\:h-7{height:1.75rem}.lg\:h-8{height:2rem}.lg\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.lg\:p-2{padding:0.5rem}.lg\:py-1{padding-top:0.25rem;padding-bottom:0.25rem}.lg\:text-base{font-size:1rem}.lg\:shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.xl\:h-6{height:1.5rem}.xl\:h-7{height:1.75rem}.xl\:h-8{height:2rem}.xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.xl\:px-4{padding-left:1rem;padding-right:1rem}.xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.xl\:py-4{padding-top:1rem;padding-bottom:1rem}.xl\:text-base{font-size:1rem}}@media(min-width: 1536px){.\32xl\:h-6{height:1.5rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-8{height:2rem}.\32xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.\32xl\:px-4{padding-left:1rem;padding-right:1rem}.\32xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.\32xl\:py-4{padding-top:1rem;padding-bottom:1rem}.\32xl\:text-base{font-size:1rem}}*,::before,::after{box-sizing:border-box}html{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui, - -apple-system, /* Firefox supports this but not yet `system-ui` */ - 'Segoe UI', - Roboto, - Helvetica, - Arial, - sans-serif, - 'Apple Color Emoji', - 'Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, - SFMono-Regular, - Consolas, - 'Liberation Mono', - Menlo, - monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type='button']{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}pre,code,kbd,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::before,::after{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.alm{margin-top:1rem;width:100%;border-radius:0.5rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity));padding:0.5rem;--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}@media(min-width: 640px){.alm{padding:0.5rem}}@media(min-width: 768px){.alm{padding:0.5rem}}@media(min-width: 1024px){.alm{padding:0.5rem;--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.alm{padding-left:1rem;padding-right:1rem;padding-top:0.5rem;padding-bottom:0.5rem}}@media(min-width: 1536px){.alm{padding-left:1rem;padding-right:1rem;padding-top:0.5rem;padding-bottom:0.5rem}}.alm-hdr{padding-bottom:0px;text-align:center;font-size:1rem;font-weight:700;--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0px;right:0px;bottom:0px;left:0px}.top-1{top:0.25rem}.left-1{left:0.25rem}.z-10{z-index:10}.z-50{z-index:50}.m-auto{margin:auto}.mt-0{margin-top:0px}.mt-2{margin-top:0.5rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mb-0{margin-bottom:0px}.mb-2{margin-bottom:0.5rem}.ml-0{margin-left:0px}.ml-36{margin-left:9rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-2{height:0.5rem}.h-3{height:0.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-40{height:10rem}.h-80{height:20rem}.h-auto{height:auto}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-1\/12{width:8.333333%}.w-11\/12{width:91.666667%}.w-full{width:100%}.flex-1{flex:1 1 0%}.table-fixed{table-layout:fixed}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}@-webkit-keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}@keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.flex-col{flex-direction:column}.content-center{align-content:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:0.25rem}.rounded-md{border-radius:0.375rem}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.border-2{border-width:2px}.border-4{border-width:4px}.border{border-width:1px}.border-solid{border-style:solid}.border-transparent{border-color:transparent}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239, 68, 68, var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgba(96, 165, 250, var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243, 244, 246, var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgba(209, 213, 219, var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(107, 114, 128, var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75, 85, 99, var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgba(254, 242, 242, var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgba(252, 165, 165, var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220, 38, 38, var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgba(255, 251, 235, var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgba(236, 253, 245, var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgba(239, 246, 255, var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgba(37, 99, 235, var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgba(99, 102, 241, var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185, 28, 28, var(--tw-bg-opacity))}.hover\:bg-blue-200:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgba(67, 56, 202, var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-cover{background-size:cover}.p-0{padding:0px}.p-2{padding:0.5rem}.px-1{padding-left:0.25rem;padding-right:0.25rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0px;padding-bottom:0px}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.pt-0{padding-top:0px}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pb-20{padding-bottom:5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.text-xxs{font-size:.5rem}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgba(0, 0, 0, var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255, 255, 255, var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17, 24, 39, var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239, 68, 68, var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245, 158, 11, var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgba(52, 211, 153, var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16, 185, 129, var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgba(59, 130, 246, var(--tw-text-opacity))}*,::before,::after{--tw-shadow:0 0 #0000}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}*,::before,::after{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(239, 68, 68, var(--tw-ring-opacity))}.focus\:ring-indigo-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(129, 140, 248, var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(99, 102, 241, var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition{transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}@media(min-width: 640px){.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:mt-0{margin-top:0px}.sm\:ml-3{margin-left:0.75rem}.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:h-6{height:1.5rem}.sm\:h-7{height:1.75rem}.sm\:h-8{height:2rem}.sm\:h-screen{height:100vh}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-start{align-items:flex-start}.sm\:p-0{padding:0px}.sm\:p-2{padding:0.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-0{padding-top:0px;padding-bottom:0px}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem}.sm\:text-base{font-size:1rem}}@media(min-width: 768px){.md\:h-6{height:1.5rem}.md\:h-7{height:1.75rem}.md\:h-8{height:2rem}.md\:p-2{padding:0.5rem}.md\:py-0{padding-top:0px;padding-bottom:0px}.md\:text-base{font-size:1rem}}@media(min-width: 1024px){.lg\:h-6{height:1.5rem}.lg\:h-7{height:1.75rem}.lg\:h-8{height:2rem}.lg\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.lg\:p-2{padding:0.5rem}.lg\:py-1{padding-top:0.25rem;padding-bottom:0.25rem}.lg\:text-base{font-size:1rem}.lg\:shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.xl\:h-6{height:1.5rem}.xl\:h-7{height:1.75rem}.xl\:h-8{height:2rem}.xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.xl\:px-4{padding-left:1rem;padding-right:1rem}.xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.xl\:py-4{padding-top:1rem;padding-bottom:1rem}.xl\:text-base{font-size:1rem}}@media(min-width: 1536px){.\32xl\:h-6{height:1.5rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-8{height:2rem}.\32xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.\32xl\:px-4{padding-left:1rem;padding-right:1rem}.\32xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.\32xl\:py-4{padding-top:1rem;padding-bottom:1rem}.\32xl\:text-base{font-size:1rem}}*,::before,::after{box-sizing:border-box}html{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui, - -apple-system, /* Firefox supports this but not yet `system-ui` */ - 'Segoe UI', - Roboto, - Helvetica, - Arial, - sans-serif, - 'Apple Color Emoji', - 'Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace, - SFMono-Regular, - Consolas, - 'Liberation Mono', - Menlo, - monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}button,[type='button']{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}button{background-color:transparent;background-image:none}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}pre,code,kbd,samp{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::before,::after{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.crd{margin-top:1rem;width:100%;border-radius:0.5rem;border-width:1px;--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity));--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity));padding:0.5rem;--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}@media(min-width: 640px){.crd{padding:0.5rem}}@media(min-width: 768px){.crd{padding:0.5rem}}@media(min-width: 1024px){.crd{padding:0.5rem;--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.crd{padding-left:1rem;padding-right:1rem;padding-top:1rem;padding-bottom:1rem}}@media(min-width: 1536px){.crd{padding-left:1rem;padding-right:1rem;padding-top:1rem;padding-bottom:1rem}}.crd-hdr{padding-bottom:1rem;text-align:center;font-size:1.125rem;font-weight:700;--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0px;right:0px;bottom:0px;left:0px}.top-1{top:0.25rem}.left-1{left:0.25rem}.z-10{z-index:10}.z-50{z-index:50}.m-auto{margin:auto}.mt-0{margin-top:0px}.mt-2{margin-top:0.5rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mb-0{margin-bottom:0px}.mb-2{margin-bottom:0.5rem}.ml-0{margin-left:0px}.ml-36{margin-left:9rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-0{height:0px}.h-2{height:0.5rem}.h-3{height:0.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-20{height:5rem}.h-40{height:10rem}.h-80{height:20rem}.h-auto{height:auto}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-4{width:1rem}.w-6{width:1.5rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-1\/3{width:33.333333%}.w-2\/3{width:66.666667%}.w-3\/6{width:50%}.w-4\/6{width:66.666667%}.w-1\/12{width:8.333333%}.w-11\/12{width:91.666667%}.w-full{width:100%}.flex-1{flex:1 1 0%}.table-fixed{table-layout:fixed}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}@-webkit-keyframes ping{75%,100%{transform:scale(2);opacity:0}}@keyframes ping{75%,100%{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}@keyframes bounce{0%,100%{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(0.8,0,1,1);animation-timing-function:cubic-bezier(0.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,0.2,1);animation-timing-function:cubic-bezier(0,0,0.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.animate-pulse{-webkit-animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;animation:pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr))}.grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.flex-col{flex-direction:column}.content-center{align-content:center}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.gap-4{gap:1rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded{border-radius:0.25rem}.rounded-md{border-radius:0.375rem}.rounded-lg{border-radius:0.5rem}.rounded-full{border-radius:9999px}.border-2{border-width:2px}.border-4{border-width:4px}.border{border-width:1px}.border-solid{border-style:solid}.border-transparent{border-color:transparent}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243, 244, 246, var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229, 231, 235, var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209, 213, 219, var(--tw-border-opacity))}.border-red-500{--tw-border-opacity:1;border-color:rgba(239, 68, 68, var(--tw-border-opacity))}.border-blue-400{--tw-border-opacity:1;border-color:rgba(96, 165, 250, var(--tw-border-opacity))}.border-indigo-500{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgba(99, 102, 241, var(--tw-border-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243, 244, 246, var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgba(209, 213, 219, var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgba(107, 114, 128, var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75, 85, 99, var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgba(254, 242, 242, var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgba(252, 165, 165, var(--tw-bg-opacity))}.bg-red-600{--tw-bg-opacity:1;background-color:rgba(220, 38, 38, var(--tw-bg-opacity))}.bg-yellow-50{--tw-bg-opacity:1;background-color:rgba(255, 251, 235, var(--tw-bg-opacity))}.bg-green-50{--tw-bg-opacity:1;background-color:rgba(236, 253, 245, var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgba(239, 246, 255, var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgba(219, 234, 254, var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgba(37, 99, 235, var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity:1;background-color:rgba(99, 102, 241, var(--tw-bg-opacity))}.hover\:bg-gray-50:hover{--tw-bg-opacity:1;background-color:rgba(249, 250, 251, var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185, 28, 28, var(--tw-bg-opacity))}.hover\:bg-blue-200:hover{--tw-bg-opacity:1;background-color:rgba(191, 219, 254, var(--tw-bg-opacity))}.hover\:bg-indigo-700:hover{--tw-bg-opacity:1;background-color:rgba(67, 56, 202, var(--tw-bg-opacity))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgba(255, 255, 255, var(--tw-bg-opacity))}.bg-opacity-75{--tw-bg-opacity:0.75}.bg-cover{background-size:cover}.p-0{padding:0px}.p-2{padding:0.5rem}.px-1{padding-left:0.25rem;padding-right:0.25rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0{padding-top:0px;padding-bottom:0px}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.pt-0{padding-top:0px}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pr-4{padding-right:1rem}.pb-4{padding-bottom:1rem}.pb-20{padding-bottom:5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.align-bottom{vertical-align:bottom}.text-xxs{font-size:.5rem}.text-xs{font-size:.75rem}.text-sm{font-size:.875rem}.text-base{font-size:1rem}.text-lg{font-size:1.125rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgba(0, 0, 0, var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgba(255, 255, 255, var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107, 114, 128, var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55, 65, 81, var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17, 24, 39, var(--tw-text-opacity))}.text-red-400{--tw-text-opacity:1;color:rgba(248, 113, 113, var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239, 68, 68, var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245, 158, 11, var(--tw-text-opacity))}.text-green-400{--tw-text-opacity:1;color:rgba(52, 211, 153, var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16, 185, 129, var(--tw-text-opacity))}.text-blue-500{--tw-text-opacity:1;color:rgba(59, 130, 246, var(--tw-text-opacity))}*,::before,::after{--tw-shadow:0 0 #0000}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}*,::before,::after{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59, 130, 246, 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)}.focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(239, 68, 68, var(--tw-ring-opacity))}.focus\:ring-indigo-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(129, 140, 248, var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(99, 102, 241, var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition{transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;transition-property:background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms}@media(min-width: 640px){.sm\:my-8{margin-top:2rem;margin-bottom:2rem}.sm\:mt-0{margin-top:0px}.sm\:ml-3{margin-left:0.75rem}.sm\:ml-4{margin-left:1rem}.sm\:block{display:block}.sm\:inline-block{display:inline-block}.sm\:flex{display:flex}.sm\:h-6{height:1.5rem}.sm\:h-7{height:1.75rem}.sm\:h-8{height:2rem}.sm\:h-screen{height:100vh}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:max-w-lg{max-width:32rem}.sm\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.sm\:flex-row-reverse{flex-direction:row-reverse}.sm\:items-start{align-items:flex-start}.sm\:p-0{padding:0px}.sm\:p-2{padding:0.5rem}.sm\:p-6{padding:1.5rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-0{padding-top:0px;padding-bottom:0px}.sm\:pb-4{padding-bottom:1rem}.sm\:text-left{text-align:left}.sm\:align-middle{vertical-align:middle}.sm\:text-sm{font-size:.875rem}.sm\:text-base{font-size:1rem}}@media(min-width: 768px){.md\:h-6{height:1.5rem}.md\:h-7{height:1.75rem}.md\:h-8{height:2rem}.md\:p-2{padding:0.5rem}.md\:py-0{padding-top:0px;padding-bottom:0px}.md\:text-base{font-size:1rem}}@media(min-width: 1024px){.lg\:h-6{height:1.5rem}.lg\:h-7{height:1.75rem}.lg\:h-8{height:2rem}.lg\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.lg\:p-2{padding:0.5rem}.lg\:py-1{padding-top:0.25rem;padding-bottom:0.25rem}.lg\:text-base{font-size:1rem}.lg\:shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)}}@media(min-width: 1280px){.xl\:h-6{height:1.5rem}.xl\:h-7{height:1.75rem}.xl\:h-8{height:2rem}.xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.xl\:px-4{padding-left:1rem;padding-right:1rem}.xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.xl\:py-4{padding-top:1rem;padding-bottom:1rem}.xl\:text-base{font-size:1rem}}@media(min-width: 1536px){.\32xl\:h-6{height:1.5rem}.\32xl\:h-7{height:1.75rem}.\32xl\:h-8{height:2rem}.\32xl\:grid-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr))}.\32xl\:px-4{padding-left:1rem;padding-right:1rem}.\32xl\:py-2{padding-top:0.5rem;padding-bottom:0.5rem}.\32xl\:py-4{padding-top:1rem;padding-bottom:1rem}.\32xl\:text-base{font-size:1rem}} \ No newline at end of file diff --git a/data_svelte/build/bundle.css.gz b/data_svelte/build/bundle.css.gz index 54bc75e4deb475a7eea828df842ae7efa27f855e..55b3f3ce81d2d7b4b9b85d58c04a91aca1292a8c 100644 GIT binary patch delta 16 XcmeyO^+k(azMF$%j)(I`_FPc_GwKCj delta 16 XcmeyO^+k(azMF%?@VLcB_FPc_Gr$FC diff --git a/data_svelte/build/bundle.js b/data_svelte/build/bundle.js deleted file mode 100644 index 05d97824..00000000 --- a/data_svelte/build/bundle.js +++ /dev/null @@ -1,2 +0,0 @@ -var app=function(){"use strict";function t(){}function e(t){return t()}function n(){return Object.create(null)}function s(t){t.forEach(e)}function r(t){return"function"==typeof t}function l(t,e){return t!=t?e==e:t!==e||t&&"object"==typeof t||"function"==typeof t}function o(e,n,s){e.$$.on_destroy.push(function(e,...n){if(null==e)return t;const s=e.subscribe(...n);return s.unsubscribe?()=>s.unsubscribe():s}(n,s))}function c(t,e,n,s){if(t){const r=i(t,e,n,s);return t[0](r)}}function i(t,e,n,s){return t[1]&&s?function(t,e){for(const n in e)t[n]=e[n];return t}(n.ctx.slice(),t[1](s(e))):n.ctx}function a(t,e,n,s){if(t[2]&&s){const r=t[2](s(n));if(void 0===e.dirty)return r;if("object"==typeof r){const t=[],n=Math.max(e.dirty.length,r.length);for(let s=0;s32){const e=[],n=t.ctx.length/32;for(let t=0;tt.removeEventListener(e,n,s)}function y(t,e,n){null==n?t.removeAttribute(e):t.getAttribute(e)!==n&&t.setAttribute(e,n)}function k(t){return""===t?null:+t}function _(t,e){e=""+e,t.wholeText!==e&&(t.data=e)}function J(t,e){t.value=null==e?"":e}function j(t,e){for(let n=0;n{V.delete(t),s&&(n&&t.d(1),s())})),t.o(e)}}function X(t,e,n){const s=t.$$.props[e];void 0!==s&&(t.$$.bound[s]=n,n(t.$$.ctx[s]))}function tt(t){t&&t.c()}function et(t,n,l,o){const{fragment:c,on_mount:i,on_destroy:a,after_update:u}=t.$$;c&&c.m(n,l),o||D((()=>{const n=i.map(e).filter(r);a?a.push(...n):s(n),t.$$.on_mount=[]})),u.forEach(D)}function nt(t,e){const n=t.$$;null!==n.fragment&&(s(n.on_destroy),n.fragment&&n.fragment.d(e),n.on_destroy=n.fragment=null,n.ctx=[])}function st(e,r,l,o,c,i,a,u=[-1]){const d=T;M(e);const f=e.$$={fragment:null,ctx:null,props:i,update:t,not_equal:c,bound:n(),on_mount:[],on_destroy:[],on_disconnect:[],before_update:[],after_update:[],context:new Map(r.context||(d?d.$$.context:[])),callbacks:n(),dirty:u,skip_bound:!1,root:r.target||d.$$.root};a&&a(f.root);let p=!1;if(f.ctx=l?l(e,r.props||{},((t,n,...s)=>{const r=s.length?s[0]:n;return f.ctx&&c(f.ctx[t],f.ctx[t]=r)&&(!f.skip_bound&&f.bound[t]&&f.bound[t](r),p&&function(t,e){-1===t.$$.dirty[0]&&(C.push(t),B(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{const t=n.indexOf(e);-1!==t&&n.splice(t,1)}}$set(t){var e;this.$$set&&(e=t,0!==Object.keys(e).length)&&(this.$$.skip_bound=!0,this.$$set(t),this.$$.skip_bound=!1)}}const lt=[];function ot(e,n=t){let s;const r=new Set;function o(t){if(l(e,t)&&(e=t,s)){const t=!lt.length;for(const t of r)t[1](),lt.push(t,e);if(t){for(let t=0;t{r.delete(i),0===r.size&&(s(),s=null)}}}}function ct(t,e=!1){return(t=t.slice(t.startsWith("/#")?2:0,t.endsWith("/*")?-2:void 0)).startsWith("/")||(t="/"+t),"/"===t&&(t=""),e&&!t.endsWith("/")&&(t+="/"),t}function it(t,e,n){if(""===n)return t;if("/"===n[0])return n;let s=t=>t.split("/").filter((t=>""!==t)),r=s(t);return"/"+(e?s(e):[]).map(((t,e)=>r[e])).join("/")+"/"+n}function at(t,e,n,s){let r=[e,"data-"+e].reduce(((e,s)=>{let r=t.getAttribute(s);return n&&t.removeAttribute(s),null===r?e:r}),!1);return!s&&""===r||(r||s||!1)}function ut(t){let e=t.split("&").map((t=>t.split("="))).reduce(((t,e)=>{let n=e[0];if(!n)return t;let s=!(e.length>1)||e[e.length-1];return"string"==typeof s&&s.includes(",")&&(s=s.split(",")),void 0===t[n]?t[n]=[s]:t[n].push(s),t}),{});return Object.entries(e).reduce(((t,e)=>(t[e[0]]=e[1].length>1?e[1]:e[1][0],t)),{})}var dt,ft,pt={HISTORY:1,HASH:2,MEMORY:3,OFF:4,run:function(t,e,n,s){return 1===t?e&&e():2===t?n&&n():s&&s()},getDeafault:function(){return window&&"srcdoc"!==window.location.pathname?1:3}},gt=function(){let t,e=pt.getDeafault(),n=n=>t&&t(mt(e));function s(t){t&&(e=t),window.onhashchange=window.onpopstate=ft=null,e!==pt.OFF&&pt.run(e,(t=>window.onpopstate=n),(t=>window.onhashchange=n))&&n()}return{mode:t=>s(t),get:t=>mt(e),go(t,s){(function(t,e,n){let s=t=>history[n?"replaceState":"pushState"]({},"",t);pt.run(t,(t=>s(e)),(t=>s(`#${e}`)),(t=>ft=e))})(e,t,s),n()},start(e){t=e,s()},stop(){t=null,s(pt.OFF)}}}();function mt(t){let e=dt,n=dt=pt.run(t,(t=>window.location.pathname+window.location.search),(t=>String(window.location.hash.slice(1)||"/")),(t=>ft||"/")),s=n.match(/^([^?#]+)(?:\?([^#]+))?(?:\#(.+))?$/);return{url:n,from:e,path:s[1]||"",query:ut(s[2]||""),hash:s[3]||""}}function ht(t){let e=E("tinro");e&&(e.exact||e.fallback)&&function(t){throw new Error("[Tinro] "+t)}(`${t.fallback?"":``} can't be inside ${e.fallback?"":` with exact path`}`);let n=t.fallback?"fallbacks":"childs",s=ot({}),r={router:{},exact:!1,pattern:null,meta:{},parent:e,fallback:t.fallback,redirect:!1,firstmatch:!1,breadcrumb:null,matched:!1,childs:new Set,activeChilds:new Set,fallbacks:new Set,update(t){r.exact=!t.path.endsWith("/*"),r.pattern=ct(`${r.parent&&r.parent.pattern||""}${t.path}`),r.redirect=t.redirect,r.firstmatch=t.firstmatch,r.breadcrumb=t.breadcrumb,r.match()},register:()=>{if(r.parent)return r.parent[n].add(r),()=>{r.parent[n].delete(r),r.router.un&&r.router.un()}},show:()=>{t.onShow(),!r.fallback&&r.parent&&r.parent.activeChilds.add(r)},hide:()=>{t.onHide(),!r.fallback&&r.parent&&r.parent.activeChilds.delete(r)},match:async()=>{r.matched=!1;let{path:e,url:n,from:l,query:o}=r.router,c=function(t,e){t=ct(t,!0),e=ct(e,!0);let n=[],s={},r=!0,l=t.split("/").map((t=>t.startsWith(":")?(n.push(t.slice(1)),"([^\\/]+)"):t)).join("\\/"),o=e.match(new RegExp(`^${l}$`));return o||(r=!1,o=e.match(new RegExp(`^${l}`))),o?(n.forEach(((t,e)=>s[t]=o[e+1])),{exact:r,params:s,part:o[0].slice(0,-1)}):null}(r.pattern,e);if(!r.fallback&&c&&r.redirect&&(!r.exact||r.exact&&c.exact)){await z();let t=it(e,r.parent&&r.parent.pattern,r.redirect);return xt.goto(t,!0)}if(r.meta=c&&{from:l,url:n,query:o,match:c.part,pattern:r.pattern,breadcrumbs:r.parent&&r.parent.meta&&r.parent.meta.breadcrumbs.slice()||[],params:c.params,subscribe:s.subscribe},r.breadcrumb&&r.meta&&r.meta.breadcrumbs.push({name:r.breadcrumb,path:c.part}),s.set(r.meta),!c||r.fallback||!(!r.exact||r.exact&&c.exact)||r.parent&&r.parent.firstmatch&&r.parent.matched?r.hide():(t.onMeta(r.meta),r.parent&&(r.parent.matched=!0),r.show()),await z(),c&&!r.fallback&&(r.childs.size>0&&0==r.activeChilds.size||0==r.childs.size&&r.fallbacks.size>0)){let t=r;for(;0==t.fallbacks.size;)if(t=t.parent,!t)return;t&&t.fallbacks.forEach((t=>{if(t.redirect){let e=it("/",t.parent&&t.parent.pattern,t.redirect);xt.goto(e,!0)}else t.show()}))}}};return l="tinro",o=r,L().$$.context.set(l,o),q((()=>r.register())),r.router.un=xt.subscribe((t=>{r.router.path=t.path,r.router.url=t.url,r.router.query=t.query,r.router.from=t.from,null!==r.pattern&&r.match()})),r;var l,o}function $t(){return E("tinro").meta}var xt=function(){let{subscribe:t}=ot(gt.get(),(t=>{gt.start(t);let e=function(t){let e=e=>{let n=e.target.closest("a[href]"),s=n&&at(n,"target",!1,"_self"),r=n&&at(n,"tinro-ignore"),l=e.ctrlKey||e.metaKey||e.altKey||e.shiftKey;if("_self"==s&&!r&&!l&&n){let s=n.getAttribute("href").replace(/^\/#/,"");/^\/\/|^[a-zA-Z]+:/.test(s)||(e.preventDefault(),t(s.startsWith("/")?s:n.href.replace(window.location.origin,"")))}};return addEventListener("click",e),()=>removeEventListener("click",e)}(gt.go);return()=>{gt.stop(),e()}}));return{subscribe:t,goto:gt.go,params:bt,meta:$t,useHashNavigation:t=>gt.mode(t?pt.HASH:pt.HISTORY),mode:{hash:()=>gt.mode(pt.HASH),history:()=>gt.mode(pt.HISTORY),memory:()=>gt.mode(pt.MEMORY)}}}();function bt(){return E("tinro").meta.params}const wt=t=>({params:2&t,meta:4&t}),vt=t=>({params:t[1],meta:t[2]});function yt(t){let e;const n=t[9].default,s=c(n,t,t[8],vt);return{c(){s&&s.c()},m(t,n){s&&s.m(t,n),e=!0},p(t,r){s&&s.p&&(!e||262&r)&&u(s,n,t,t[8],e?a(n,t[8],r,wt):d(t[8]),vt)},i(t){e||(G(s,t),e=!0)},o(t){Z(s,t),e=!1},d(t){s&&s.d(t)}}}function kt(t){let e,n,s=t[0]&&yt(t);return{c(){s&&s.c(),e=w()},m(t,r){s&&s.m(t,r),p(t,e,r),n=!0},p(t,[n]){t[0]?s?(s.p(t,n),1&n&&G(s,1)):(s=yt(t),s.c(),G(s,1),s.m(e.parentNode,e)):s&&(K(),Z(s,1,1,(()=>{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function _t(t,e,n){let{$$slots:s={},$$scope:r}=e,{path:l="/*"}=e,{fallback:o=!1}=e,{redirect:c=!1}=e,{firstmatch:i=!1}=e,{breadcrumb:a=null}=e,u=!1,d={},f={};const p=ht({fallback:o,onShow(){n(0,u=!0)},onHide(){n(0,u=!1)},onMeta(t){n(2,f=t),n(1,d=f.params)}});return t.$$set=t=>{"path"in t&&n(3,l=t.path),"fallback"in t&&n(4,o=t.fallback),"redirect"in t&&n(5,c=t.redirect),"firstmatch"in t&&n(6,i=t.firstmatch),"breadcrumb"in t&&n(7,a=t.breadcrumb),"$$scope"in t&&n(8,r=t.$$scope)},t.$$.update=()=>{232&t.$$.dirty&&p.update({path:l,redirect:c,firstmatch:i,breadcrumb:a})},[u,d,f,l,o,c,i,a,r,s]}class Jt extends rt{constructor(t){super(),st(this,t,_t,kt,l,{path:3,fallback:4,redirect:5,firstmatch:6,breadcrumb:7})}}function jt(e){let n,s,l,o,c;return{c(){n=$("svg"),s=$("line"),l=$("line"),y(s,"x1","18"),y(s,"y1","6"),y(s,"x2","6"),y(s,"y2","18"),y(l,"x1","6"),y(l,"y1","6"),y(l,"x2","18"),y(l,"y2","18"),y(n,"class","h-6 w-6 text-red-400 cursor-pointer"),y(n,"viewBox","0 -2 24 24"),y(n,"fill","none"),y(n,"stroke","currentColor"),y(n,"stroke-width","2"),y(n,"stroke-linecap","round"),y(n,"stroke-linejoin","round")},m(t,i){p(t,n,i),f(n,s),f(n,l),o||(c=v(n,"click",(function(){r(e[0]())&&e[0]().apply(this,arguments)})),o=!0)},p(t,[n]){e=t},i:t,o:t,d(t){t&&g(n),o=!1,c()}}}function St(t,e,n){let{click:s=(()=>{})}=e;return t.$$set=t=>{"click"in t&&n(0,s=t.click)},[s]}class Tt extends rt{constructor(t){super(),st(this,t,St,jt,l,{click:0})}}function Mt(t){let e,n,s,r,l,o,c,i;return c=new Tt({props:{click:t[5]}}),{c(){e=h("div"),n=h("div"),s=h("h1"),r=x(t[0]),l=b(),o=h("div"),tt(c.$$.fragment),y(s,"class","alm-hdr"),y(n,"class","w-11/12"),y(o,"class","flex justify-end w-1/12"),y(e,"class","flex items-center")},m(t,a){p(t,e,a),f(e,n),f(n,s),f(s,r),f(e,l),f(e,o),et(c,o,null),i=!0},p(t,e){(!i||1&e)&&_(r,t[0]);const n={};4&e&&(n.click=t[5]),c.$set(n)},i(t){i||(G(c.$$.fragment,t),i=!0)},o(t){Z(c.$$.fragment,t),i=!1},d(t){t&&g(e),nt(c)}}}function Lt(e){let n,s;return{c(){n=h("h1"),s=x(e[0]),y(n,"class","alm-hdr")},m(t,e){p(t,n,e),f(n,s)},p(t,e){1&e&&_(s,t[0])},i:t,o:t,d(t){t&&g(n)}}}function qt(t){let e,n,s,r,l;const o=[Lt,Mt],i=[];function m(t,e){return t[0]&&!t[1]?0:t[0]&&t[1]?1:-1}~(n=m(t))&&(s=i[n]=o[n](t));const $=t[4].default,x=c($,t,t[3],null);return{c(){e=h("div"),s&&s.c(),r=b(),x&&x.c(),y(e,"class","alm")},m(t,s){p(t,e,s),~n&&i[n].m(e,null),f(e,r),x&&x.m(e,null),l=!0},p(t,[c]){let f=n;n=m(t),n===f?~n&&i[n].p(t,c):(s&&(K(),Z(i[f],1,1,(()=>{i[f]=null})),Q()),~n?(s=i[n],s?s.p(t,c):(s=i[n]=o[n](t),s.c()),G(s,1),s.m(e,r)):s=null),x&&x.p&&(!l||8&c)&&u(x,$,t,t[3],l?a($,t[3],c,null):d(t[3]),null)},i(t){l||(G(s),G(x,t),l=!0)},o(t){Z(s),Z(x,t),l=!1},d(t){t&&g(e),~n&&i[n].d(),x&&x.d(t)}}}function Et(t,e,n){let{$$slots:s={},$$scope:r}=e,{title:l=!1}=e,{cross:o=!1}=e,{close:c=(()=>{})}=e;return t.$$set=t=>{"title"in t&&n(0,l=t.title),"cross"in t&&n(1,o=t.cross),"close"in t&&n(2,c=t.close),"$$scope"in t&&n(3,r=t.$$scope)},[l,o,c,r,s,()=>c()]}class Ct extends rt{constructor(t){super(),st(this,t,Et,qt,l,{title:0,cross:1,close:2})}}function Ot(e){let n;return{c(){n=h("div"),n.innerHTML='
\n
',y(n,"class","fixed z-10 inset-0 overflow-y-auto"),y(n,"aria-labelledby","modal-title"),y(n,"role","dialog"),y(n,"aria-modal","true")},m(t,e){p(t,n,e)},p:t,i:t,o:t,d(t){t&&g(n)}}}class Nt extends rt{constructor(t){super(),st(this,t,null,Ot,l,{})}}function Ht(t){let e,n,s,r=t[0]&&Pt(t);const l=t[3].default,o=c(l,t,t[2],null);return{c(){e=h("div"),r&&r.c(),n=b(),o&&o.c(),y(e,"class","crd")},m(t,l){p(t,e,l),r&&r.m(e,null),f(e,n),o&&o.m(e,null),s=!0},p(t,c){t[0]?r?r.p(t,c):(r=Pt(t),r.c(),r.m(e,n)):r&&(r.d(1),r=null),o&&o.p&&(!s||4&c)&&u(o,l,t,t[2],s?a(l,t[2],c,null):d(t[2]),null)},i(t){s||(G(o,t),s=!0)},o(t){Z(o,t),s=!1},d(t){t&&g(e),r&&r.d(),o&&o.d(t)}}}function Pt(t){let e,n;return{c(){e=h("h1"),n=x(t[0]),y(e,"class","crd-hdr")},m(t,s){p(t,e,s),f(e,n)},p(t,e){1&e&&_(n,t[0])},d(t){t&&g(e)}}}function At(t){let e,n,s=t[1]&&Ht(t);return{c(){s&&s.c(),e=w()},m(t,r){s&&s.m(t,r),p(t,e,r),n=!0},p(t,[n]){t[1]?s?(s.p(t,n),2&n&&G(s,1)):(s=Ht(t),s.c(),G(s,1),s.m(e.parentNode,e)):s&&(K(),Z(s,1,1,(()=>{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function Bt(t,e,n){let{$$slots:s={},$$scope:r}=e,{title:l=!1}=e,{show:o=!0}=e;return t.$$set=t=>{"title"in t&&n(0,l=t.title),"show"in t&&n(1,o=t.show),"$$scope"in t&&n(2,r=t.$$scope)},[l,o,r,s]}class zt extends rt{constructor(t){super(),st(this,t,Bt,At,l,{title:0,show:1})}}function Dt(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"step","0.1"),y(e,"type","number")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[2]),v(e,"input",t[3])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&k(e.value)!==t[0].status&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function It(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"type","text")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[4]),v(e,"input",t[5])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&e.value!==t[0].status&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function Rt(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"type","date")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[6]),v(e,"input",t[7])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function Ft(t){let e,n,r,l;return{c(){e=h("input"),y(e,"class",n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500"),y(e,"type","time")},m(n,s){p(n,e,s),J(e,t[0].status),r||(l=[v(e,"change",t[8]),v(e,"input",t[9])],r=!0)},p(t,s){1&s&&n!==(n=t[0].sent?"ipt-rnd text-right border-red-500":"ipt-rnd text-right focus:border-indigo-500")&&y(e,"class",n),1&s&&J(e,t[0].status)},d(t){t&&g(e),r=!1,s(l)}}}function Ut(e){let n,s,r,l,o,c,i,a,u,d=(e[0].descr?e[0].descr:"")+"",m="number"==e[0].type&&Dt(e),$="text"==e[0].type&&It(e),w="date"==e[0].type&&Rt(e),v="time"==e[0].type&&Ft(e);return{c(){n=h("div"),s=h("div"),r=h("label"),l=x(d),o=b(),c=h("div"),m&&m.c(),i=b(),$&&$.c(),a=b(),w&&w.c(),u=b(),v&&v.c(),y(r,"class","wgt-dscr-stl"),y(s,"class","w-2/3"),y(c,"class","flex justify-end w-1/3"),y(n,"class","crd-itm-psn")},m(t,e){p(t,n,e),f(n,s),f(s,r),f(r,l),f(n,o),f(n,c),m&&m.m(c,null),f(c,i),$&&$.m(c,null),f(c,a),w&&w.m(c,null),f(c,u),v&&v.m(c,null)},p(t,[e]){1&e&&d!==(d=(t[0].descr?t[0].descr:"")+"")&&_(l,d),"number"==t[0].type?m?m.p(t,e):(m=Dt(t),m.c(),m.m(c,i)):m&&(m.d(1),m=null),"text"==t[0].type?$?$.p(t,e):($=It(t),$.c(),$.m(c,a)):$&&($.d(1),$=null),"date"==t[0].type?w?w.p(t,e):(w=Rt(t),w.c(),w.m(c,u)):w&&(w.d(1),w=null),"time"==t[0].type?v?v.p(t,e):(v=Ft(t),v.c(),v.m(c,null)):v&&(v.d(1),v=null)},i:t,o:t,d(t){t&&g(n),m&&m.d(),$&&$.d(),w&&w.d(),v&&v.d()}}}function Wt(t,e,n){let{widget:s}=e,{wsPush:r=((t,e,n)=>{})}=e;return t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"wsPush"in t&&n(1,r=t.wsPush)},[s,r,()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=k(this.value),n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=this.value,n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=this.value,n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status)),function(){s.status=this.value,n(0,s)}]}class Vt extends rt{constructor(t){super(),st(this,t,Wt,Ut,l,{widget:0,wsPush:1})}}function Yt(e){let n,r,l,o,c,i,a,u,d,m,$,w=(e[0].descr?e[0].descr:"")+"",k=e[0].after+"";return{c(){n=h("label"),r=x(w),l=b(),o=x(e[1]),c=b(),i=x(k),a=b(),u=h("input"),y(n,"class","wgt-dscr-stl"),y(u,"class",d="form-range range-secondary w-full h-2 p-0 rounded-lg "+(e[0].sent?"bg-red-300":"bg-gray-300")+" focus:outline-none appearance-none"),y(u,"type","range"),y(u,"min","0"),y(u,"max","1024")},m(t,s){p(t,n,s),f(n,r),f(n,l),f(n,o),f(n,c),f(n,i),p(t,a,s),p(t,u,s),J(u,e[0].status),m||($=[v(u,"change",e[3]),v(u,"input",e[3]),v(u,"change",e[4])],m=!0)},p(t,[e]){1&e&&w!==(w=(t[0].descr?t[0].descr:"")+"")&&_(r,w),2&e&&_(o,t[1]),1&e&&k!==(k=t[0].after+"")&&_(i,k),1&e&&d!==(d="form-range range-secondary w-full h-2 p-0 rounded-lg "+(t[0].sent?"bg-red-300":"bg-gray-300")+" focus:outline-none appearance-none")&&y(u,"class",d),1&e&&J(u,t[0].status)},i:t,o:t,d(t){t&&g(n),t&&g(a),t&&g(u),m=!1,s($)}}}function Kt(t,e,n){let{widget:s}=e,{wsPush:r=((t,e,n)=>{})}=e,{val:l=0}=e;function o(){n(1,l=function(t,e,n,s,r){return Math.round((t-e)*(r-s)/(n-e)+s)}(s.status,0,1024,s.min,s.max))}return t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"wsPush"in t&&n(2,r=t.wsPush),"val"in t&&n(1,l=t.val)},t.$$.update=()=>{1&t.$$.dirty&&(s.status,o())},[s,l,r,function(){s.status=k(this.value),n(0,s)},()=>(n(0,s.sent=!0,s),r(s.ws,s.topic,s.status))]}class Qt extends rt{constructor(t){super(),st(this,t,Kt,Yt,l,{widget:0,wsPush:2,val:1})}}function Gt(e){let n,r,l,o,c,i,a,u,d,m,$,w,k,J,j,S,T,M,L,q=(e[0].descr?e[0].descr:"")+"";return{c(){n=h("div"),r=h("div"),l=h("label"),o=x(q),c=b(),i=h("div"),a=h("label"),u=h("div"),d=h("input"),$=b(),w=h("div"),J=b(),j=h("div"),y(l,"class","wgt-dscr-stl"),y(r,"class","w-2/3"),y(d,"id",m=e[0].topic),y(d,"type","checkbox"),y(d,"class","sr-only"),y(w,"class",k="block "+(e[1]?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg"),y(j,"class",S="dot "+(e[0].sent?"bg-red-300":"bg-gray-100")+" absolute left-1 top-1 w-4 h-4 rounded-full transition shadow-lg"),y(u,"class","relative"),y(a,"for",T=e[0].topic),y(a,"class","items-center cursor-pointer"),y(i,"class","flex justify-end w-1/3"),y(n,"class","crd-itm-psn")},m(t,s){p(t,n,s),f(n,r),f(r,l),f(l,o),f(n,c),f(n,i),f(i,a),f(a,u),f(u,d),d.checked=e[1],f(u,$),f(u,w),f(u,J),f(u,j),M||(L=[v(d,"change",e[4]),v(d,"change",e[5])],M=!0)},p(t,[e]){1&e&&q!==(q=(t[0].descr?t[0].descr:"")+"")&&_(o,q),1&e&&m!==(m=t[0].topic)&&y(d,"id",m),2&e&&(d.checked=t[1]),2&e&&k!==(k="block "+(t[1]?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg")&&y(w,"class",k),1&e&&S!==(S="dot "+(t[0].sent?"bg-red-300":"bg-gray-100")+" absolute left-1 top-1 w-4 h-4 rounded-full transition shadow-lg")&&y(j,"class",S),1&e&&T!==(T=t[0].topic)&&y(a,"for",T)},i:t,o:t,d(t){t&&g(n),M=!1,s(L)}}}function Zt(t,e,n){let{widget:s}=e,{toggleState:r=!1}=e,{wsPush:l=((t,e,n)=>{})}=e;function o(){n(0,s.sent=!0,s),n(0,s.status=r?"1":"0",s)}return t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"toggleState"in t&&n(1,r=t.toggleState),"wsPush"in t&&n(2,l=t.wsPush)},t.$$.update=()=>{1&t.$$.dirty&&(s.status,"1"==s.status?n(1,r=!0):"0"==s.status&&n(1,r=!1))},[s,r,l,o,function(){r=this.checked,n(1,r)},()=>(o(),l(s.ws,s.topic,s.status))]}class Xt extends rt{constructor(t){super(),st(this,t,Zt,Gt,l,{widget:0,toggleState:1,wsPush:2})}}function te(e){let n,s,r,l,o,c,i,a,u,d,m,$,w=(e[0].descr?e[0].descr:"")+"",v=(e[0].status?e[0].status:"")+"",k=(e[0].after?e[0].after:"")+"";return{c(){n=h("div"),s=h("div"),r=h("label"),l=x(w),o=b(),c=h("div"),i=h("label"),a=x(v),u=b(),d=h("label"),m=x(" "),$=x(k),y(r,"class","wgt-dscr-stl"),y(s,"class","w-2/3"),y(i,"class","wgt-adt-stl"),y(d,"class","wgt-adt-stl"),y(c,"class","flex justify-end w-1/3"),y(n,"class","crd-itm-psn")},m(t,e){p(t,n,e),f(n,s),f(s,r),f(r,l),f(n,o),f(n,c),f(c,i),f(i,a),f(c,u),f(c,d),f(d,m),f(d,$)},p(t,[e]){1&e&&w!==(w=(t[0].descr?t[0].descr:"")+"")&&_(l,w),1&e&&v!==(v=(t[0].status?t[0].status:"")+"")&&_(a,v),1&e&&k!==(k=(t[0].after?t[0].after:"")+"")&&_($,k)},i:t,o:t,d(t){t&&g(n)}}}function ee(t,e,n){let{widget:s}=e,{value:r}=e;return r=r,t.$$set=t=>{"widget"in t&&n(0,s=t.widget),"value"in t&&n(1,r=t.value)},[s,r]}class ne extends rt{constructor(t){super(),st(this,t,ee,te,l,{widget:0,value:1})}}function se(t,e,n){const s=t.slice();return s[11]=e[n],s[13]=n,s}function re(t,e,n){const s=t.slice();return s[14]=e[n],s[15]=e,s[16]=n,s}function le(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function oe(t){let e,n,s,r=t[0]===[]&&ce(),l=t[1],o=[];for(let e=0;eZ(o[t],1,1,(()=>{o[t]=null}));return{c(){e=h("div"),r&&r.c(),n=b();for(let t=0;t{r=null})),Q()),11&s){let n;for(l=t[1],n=0;n{o=null})),Q()),"toggle"===t[14].widget?c?(c.p(t,l),1&l&&G(c,1)):(c=ue(t),c.c(),G(c,1),c.m(n.parentNode,n)):c&&(K(),Z(c,1,1,(()=>{c=null})),Q()),"anydata"===t[14].widget?i?(i.p(t,l),1&l&&G(i,1)):(i=de(t),i.c(),G(i,1),i.m(s.parentNode,s)):i&&(K(),Z(i,1,1,(()=>{i=null})),Q()),"range"===t[14].widget?a?(a.p(t,l),1&l&&G(a,1)):(a=fe(t),a.c(),G(a,1),a.m(r.parentNode,r)):a&&(K(),Z(a,1,1,(()=>{a=null})),Q())},i(t){l||(G(o),G(c),G(i),G(a),l=!0)},o(t){Z(o),Z(c),Z(i),Z(a),l=!1},d(t){o&&o.d(t),t&&g(e),c&&c.d(t),t&&g(n),i&&i.d(t),t&&g(s),a&&a.d(t),t&&g(r)}}}function ae(t){let e,n,s;function r(e){t[5](e,t[14])}let l={widget:t[14],wsPush:t[4]};return void 0!==t[14].status&&(l.value=t[14].status),e=new Vt({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),8&r&&(l.wsPush=t[4]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function ue(t){let e,n,s;function r(e){t[7](e,t[14])}let l={widget:t[14],wsPush:t[6]};return void 0!==t[14].status&&(l.value=t[14].status),e=new Xt({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),8&r&&(l.wsPush=t[6]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function de(t){let e,n,s;function r(e){t[8](e,t[14])}let l={widget:t[14]};return void 0!==t[14].status&&(l.value=t[14].status),e=new ne({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function fe(t){let e,n,s;function r(e){t[10](e,t[14])}let l={widget:t[14],wsPush:t[9]};return void 0!==t[14].status&&(l.value=t[14].status),e=new Qt({props:l}),O.push((()=>X(e,"value",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(s,r){t=s;const l={};1&r&&(l.widget=t[14]),8&r&&(l.wsPush=t[9]),!n&&1&r&&(n=!0,l.value=t[14].status,I((()=>n=!1))),e.$set(l)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function pe(t){let e,n,s=t[14].page===t[11].page&&ie(t);return{c(){s&&s.c(),e=w()},m(t,r){s&&s.m(t,r),p(t,e,r),n=!0},p(t,n){t[14].page===t[11].page?s?(s.p(t,n),3&n&&G(s,1)):(s=ie(t),s.c(),G(s,1),s.m(e.parentNode,e)):s&&(K(),Z(s,1,1,(()=>{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function ge(t){let e,n,s=t[0],r=[];for(let e=0;eZ(r[t],1,1,(()=>{r[t]=null}));return{c(){for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function $e(t,e,n){let{layoutJson:s}=e,{pages:r}=e,{show:l}=e,{wsPush:o=((t,e,n)=>{})}=e;return t.$$set=t=>{"layoutJson"in t&&n(0,s=t.layoutJson),"pages"in t&&n(1,r=t.pages),"show"in t&&n(2,l=t.show),"wsPush"in t&&n(3,o=t.wsPush)},[s,r,l,o,(t,e,n)=>o(t,e,n),function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))},(t,e,n)=>o(t,e,n),function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))},function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))},(t,e,n)=>o(t,e,n),function(e,r){t.$$.not_equal(r.status,e)&&(r.status=e,n(0,s))}]}class xe extends rt{constructor(t){super(),st(this,t,$e,he,l,{layoutJson:0,pages:1,show:2,wsPush:3})}}function be(e){let n,s,l,o,c,i,a;return{c(){n=$("svg"),s=$("path"),l=$("circle"),o=$("circle"),c=$("circle"),y(s,"stroke","none"),y(s,"d","M0 0h24v24H0z"),y(l,"cx","5"),y(l,"cy","12"),y(l,"r","1"),y(o,"cx","12"),y(o,"cy","12"),y(o,"r","1"),y(c,"cx","19"),y(c,"cy","12"),y(c,"r","1"),y(n,"class","h-6 w-6 text-green-400 cursor-pointer"),y(n,"width","24"),y(n,"height","24"),y(n,"viewBox","0 -2 24 24"),y(n,"stroke-width","2"),y(n,"stroke","currentColor"),y(n,"fill","none"),y(n,"stroke-linecap","round"),y(n,"stroke-linejoin","round")},m(t,u){p(t,n,u),f(n,s),f(n,l),f(n,o),f(n,c),i||(a=v(n,"click",(function(){r(e[0]())&&e[0]().apply(this,arguments)})),i=!0)},p(t,[n]){e=t},i:t,o:t,d(t){t&&g(n),i=!1,a()}}}function we(t,e,n){let{click:s=(()=>{})}=e;return t.$$set=t=>{"click"in t&&n(0,s=t.click)},[s]}class ve extends rt{constructor(t){super(),st(this,t,we,be,l,{click:0})}}function ye(t,e,n){const s=t.slice();return s[25]=e[n],s[26]=e,s[27]=n,s}function ke(t,e,n){const s=t.slice();return s[28]=e[n][0],s[29]=e[n][1],s[30]=e,s[31]=n,s}function _e(t,e,n){const s=t.slice();return s[32]=e[n],s}function Je(t,e,n){const s=t.slice();return s[35]=e[n],s}function je(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function Se(t){let e,n,s,r,l,o,c,i;return n=new zt({props:{title:"Конфигуратор",$$slots:{default:[He]},$$scope:{ctx:t}}}),r=new zt({props:{title:"Сценарии",$$slots:{default:[Pe]},$$scope:{ctx:t}}}),c=new zt({props:{$$slots:{default:[Ae]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),l=b(),o=h("div"),tt(c.$$.fragment),y(e,"class","grd-2col1"),y(o,"class","grd-1col1")},m(t,a){p(t,e,a),et(n,e,null),f(e,s),et(r,e,null),p(t,l,a),p(t,o,a),et(c,o,null),i=!0},p(t,e){const s={};398&e[0]|128&e[1]&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};513&e[0]|128&e[1]&&(l.$$scope={dirty:e,ctx:t}),r.$set(l);const o={};96&e[0]|128&e[1]&&(o.$$scope={dirty:e,ctx:t}),c.$set(o)},i(t){i||(G(n.$$.fragment,t),G(r.$$.fragment,t),G(c.$$.fragment,t),i=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),Z(c.$$.fragment,t),i=!1},d(t){t&&g(e),nt(n),nt(r),t&&g(l),t&&g(o),nt(c)}}}function Te(t){let e,n;return{c(){e=h("optgroup"),y(e,"label",n=t[35].header)},m(t,n){p(t,e,n)},p(t,s){8&s[0]&&n!==(n=t[35].header)&&y(e,"label",n)},d(t){t&&g(e)}}}function Me(t){let e,n,s,r,l=t[35].name+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[35].num,e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){8&s[0]&&l!==(l=t[35].name+"")&&_(n,l),8&s[0]&&r!==(r=t[35].num)&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function Le(t){let e,n,s=t[35].header&&Te(t),r=!t[35].header&&Me(t);return{c(){s&&s.c(),e=w(),r&&r.c(),n=w()},m(t,l){s&&s.m(t,l),p(t,e,l),r&&r.m(t,l),p(t,n,l)},p(t,l){t[35].header?s?s.p(t,l):(s=Te(t),s.c(),s.m(e.parentNode,e)):s&&(s.d(1),s=null),t[35].header?r&&(r.d(1),r=null):r?r.p(t,l):(r=Me(t),r.c(),r.m(n.parentNode,n))},d(t){s&&s.d(t),t&&g(e),r&&r.d(t),t&&g(n)}}}function qe(t){let e,n,s,r,l=t[32].label+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[32].name,e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){4&s[0]&&l!==(l=t[32].label+"")&&_(n,l),4&s[0]&&r!==(r=t[32].name)&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function Ee(t){let e,n=Object.entries(t[25]),s=[];for(let e=0;eZ(S[t],1,1,(()=>{S[t]=null}));return{c(){e=h("div"),n=h("select");for(let t=0;t<_.length;t+=1)_[t].c();r=b(),l=h("select"),o=h("option"),o.textContent="Выберите пресет",c=b(),i=h("table"),a=h("thead"),a.innerHTML='Тип \n Id \n Виджет \n Вкладка \n Название \n \n ',u=b(),d=h("tbody");for(let t=0;tt[12].call(n))),o.__value="Выберите пресет",o.value=o.__value,y(l,"class","slct-lg"),y(e,"class","grd-2col2"),y(a,"class","bg-gray-100"),y(d,"class","bg-white"),y(i,"class","tbl")},m(s,g){p(s,e,g),f(e,n);for(let t=0;t<_.length;t+=1)_[t].m(n,null);j(n,t[7]),f(e,r),f(e,l),f(l,o),p(s,c,g),p(s,i,g),f(i,a),f(i,u),f(i,d);for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function ze(t,e,n){let s,{configJson:r}=e,{widgetsJson:l}=e,{itemsJson:o}=e,{show:c}=e,{scenarioTxt:i}=e,a=0,{saveConfig:u=(()=>{})}=e,{rebootEsp:d=(()=>{})}=e,f=!0;function p(){for(let t=0;t{"configJson"in t&&n(1,r=t.configJson),"widgetsJson"in t&&n(2,l=t.widgetsJson),"itemsJson"in t&&n(3,o=t.itemsJson),"show"in t&&n(4,c=t.show),"scenarioTxt"in t&&n(0,i=t.scenarioTxt),"saveConfig"in t&&n(5,u=t.saveConfig),"rebootEsp"in t&&n(6,d=t.rebootEsp)},t.$$.update=()=>{1&t.$$.dirty[0]&&n(9,s=Math.round(i.split("\n").length)+1)},[i,r,l,o,c,u,d,a,f,s,p,g,function(){a=S(this),n(7,a),n(3,o)},()=>p(),function(t,e){t[e].id=this.value,n(1,r),n(2,l)},function(t,e){t[e].widget=S(this),n(1,r),n(2,l)},function(t,e){t[e].page=this.value,n(1,r),n(2,l)},function(t,e){t[e].descr=this.value,n(1,r),n(2,l)},()=>n(8,f=!f),t=>g(t),function(t,e,s){e[s][t]=this.value,n(1,r),n(2,l)},function(){i=this.value,n(0,i)},()=>u(),()=>d()]}class De extends rt{constructor(t){super(),st(this,t,ze,Be,l,{configJson:1,widgetsJson:2,itemsJson:3,show:4,scenarioTxt:0,saveConfig:5,rebootEsp:6},null,[-1,-1])}}function Ie(t,e,n){const s=t.slice();return s[23]=e[n][0],s[24]=e[n][1],s}function Re(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function Fe(t){let e,n,s,r,l,o,c,i;return n=new zt({props:{title:"Подключение к WiFi",$$slots:{default:[Ve]},$$scope:{ctx:t}}}),r=new zt({props:{title:"Подключение к MQTT",$$slots:{default:[Ze]},$$scope:{ctx:t}}}),c=new zt({props:{$$slots:{default:[Xe]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),l=b(),o=h("div"),tt(c.$$.fragment),y(e,"class","grd-2col1"),y(o,"class","grd-1col1")},m(t,a){p(t,e,a),et(n,e,null),f(e,s),et(r,e,null),p(t,l,a),p(t,o,a),et(c,o,null),i=!0},p(t,e){const s={};134217783&e&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};134217795&e&&(l.$$scope={dirty:e,ctx:t}),r.$set(l);const o={};134217856&e&&(o.$$scope={dirty:e,ctx:t}),c.$set(o)},i(t){i||(G(n.$$.fragment,t),G(r.$$.fragment,t),G(c.$$.fragment,t),i=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),Z(c.$$.fragment,t),i=!1},d(t){t&&g(e),nt(n),nt(r),t&&g(l),t&&g(o),nt(c)}}}function Ue(t){let e,n,s,r,l=t[24]+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[24],e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){4&s&&l!==(l=t[24]+"")&&_(n,l),4&s&&r!==(r=t[24])&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function We(t){let e,n,s;return n=new Ct({props:{title:"Введен неправильный пароль"}}),{c(){e=h("div"),tt(n.$$.fragment),y(e,"class","grd-1col1")},m(t,r){p(t,e,r),et(n,e,null),s=!0},i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){t&&g(e),nt(n)}}}function Ve(t){let e,n,r,l,o,c,i,a,u,d,$,x,w,k,_,S,T,M,L,q,E,C,O,N,H,P,A,B,z,I,R,F,U,W,V,Y,X,tt,et,nt,st,rt=Object.entries(t[2]),lt=[];for(let e=0;eНазвание устройства

',r=b(),l=h("div"),o=h("input"),c=b(),i=h("div"),a=h("div"),a.innerHTML='

Точка доступа

',u=b(),d=h("div"),$=h("input"),x=b(),w=h("div"),k=h("div"),k.innerHTML='

Пароль точки доступа

',_=b(),S=h("div"),T=h("input"),M=b(),L=h("div"),q=h("div"),q.innerHTML='

Название wifi сети

',E=b(),C=h("div"),O=h("select");for(let t=0;tПароль

',A=b(),B=h("div"),z=h("input"),I=b(),R=h("div"),F=h("div"),F.innerHTML='

Сервер обновления

',U=b(),W=h("div"),V=h("input"),Y=b(),ot&&ot.c(),X=b(),tt=h("button"),tt.textContent="Сохранить",y(n,"class","w-4/6"),y(o,"class","ipt-rnd text-left focus:border-indigo-500"),y(o,"type","text"),y(l,"class","flex justify-end w-3/6"),y(e,"class","crd-itm-psn"),y(a,"class","w-4/6"),y($,"class","ipt-rnd text-left focus:border-indigo-500"),y($,"type","text"),y(d,"class","flex justify-end w-3/6"),y(i,"class","crd-itm-psn"),y(k,"class","w-4/6"),y(T,"class","ipt-rnd text-left focus:border-indigo-500"),y(T,"type","text"),y(S,"class","flex justify-end w-3/6"),y(w,"class","crd-itm-psn"),y(q,"class","w-4/6"),y(O,"class","ipt-rnd text-left focus:border-indigo-500"),void 0===t[0].routerssid&&D((()=>t[11].call(O))),y(C,"class","flex justify-end w-3/6"),y(L,"class","crd-itm-psn"),y(P,"class","w-4/6"),y(z,"class","ipt-rnd text-left focus:border-indigo-500"),y(z,"type","text"),y(B,"class","flex justify-end w-3/6"),y(H,"class","crd-itm-psn"),y(F,"class","w-4/6"),y(V,"class","ipt-rnd text-left focus:border-indigo-500"),y(V,"type","text"),y(W,"class","flex justify-end w-3/6"),y(R,"class","crd-itm-psn"),y(tt,"class","btn-lg")},m(s,g){p(s,e,g),f(e,n),f(e,r),f(e,l),f(l,o),J(o,t[0].name),p(s,c,g),p(s,i,g),f(i,a),f(i,u),f(i,d),f(d,$),J($,t[0].apssid),p(s,x,g),p(s,w,g),f(w,k),f(w,_),f(w,S),f(S,T),J(T,t[0].appass),p(s,M,g),p(s,L,g),f(L,q),f(L,E),f(L,C),f(C,O);for(let t=0;t{ot=null})),Q())},i(t){et||(G(ot),et=!0)},o(t){Z(ot),et=!1},d(t){t&&g(e),t&&g(c),t&&g(i),t&&g(x),t&&g(w),t&&g(M),t&&g(L),m(lt,t),t&&g(N),t&&g(H),t&&g(I),t&&g(R),t&&g(Y),ot&&ot.d(t),t&&g(X),t&&g(tt),nt=!1,s(st)}}}function Ye(t){let e;return{c(){e=h("p"),e.textContent="Ошибка",y(e,"class","text-red-500 font-bold h-8 bg-red-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Ke(t){let e;return{c(){e=h("p"),e.textContent="Ожидание",y(e,"class","text-blue-500 font-bold h-8 bg-blue-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Qe(t){let e;return{c(){e=h("p"),e.textContent="Подключение",y(e,"class","text-yellow-500 font-bold h-8 bg-yellow-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Ge(t){let e;return{c(){e=h("p"),e.textContent="Подключено",y(e,"class","text-green-500 font-bold h-8 bg-green-50 border-2 border-gray-200 rounded w-full text-center")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Ze(t){let e,n,r,l,o,c,i,a,u,d,m,$,x,w,k,_,j,S,T,M,L,q,E,C,O,N,H,P,A,B,z,D,I,R,F,U,W,V;function Y(t,e){return"e5"===t[1].mqtt?Ge:"e13"===t[1].mqtt?Qe:void 0===t[1].mqtt?Ke:Ye}let K=Y(t),Q=K(t);return{c(){e=h("div"),n=h("div"),n.innerHTML='

Состояние подключения

',r=b(),l=h("div"),Q.c(),o=b(),c=h("div"),i=h("div"),i.innerHTML='

Название сервера

',a=b(),u=h("div"),d=h("input"),m=b(),$=h("div"),x=h("div"),x.innerHTML='

Порт

',w=b(),k=h("div"),_=h("input"),j=b(),S=h("div"),T=h("div"),T.innerHTML='

Префикс

',M=b(),L=h("div"),q=h("input"),E=b(),C=h("div"),O=h("div"),O.innerHTML='

Имя пользователя

',N=b(),H=h("div"),P=h("input"),A=b(),B=h("div"),z=h("div"),z.innerHTML='

Пароль

',D=b(),I=h("div"),R=h("input"),F=b(),U=h("button"),U.textContent="Сохранить",y(n,"class","w-4/6"),y(l,"class","flex justify-center w-3/6 align-baseline text-sm sm:text-sm md:text-base lg:text-base xl:text-base 2xl:text-base break-words"),y(e,"class","crd-itm-psn"),y(i,"class","w-4/6"),y(d,"class","ipt-rnd text-left focus:border-indigo-500"),y(d,"type","text"),y(u,"class","flex justify-end w-3/6"),y(c,"class","crd-itm-psn"),y(x,"class","w-4/6"),y(_,"class","ipt-rnd text-left focus:border-indigo-500"),y(_,"type","text"),y(k,"class","flex justify-end w-3/6"),y($,"class","crd-itm-psn"),y(T,"class","w-4/6"),y(q,"class","ipt-rnd text-left focus:border-indigo-500"),y(q,"type","text"),y(L,"class","flex justify-end w-3/6"),y(S,"class","crd-itm-psn"),y(O,"class","w-4/6"),y(P,"class","ipt-rnd text-left focus:border-indigo-500"),y(P,"type","text"),y(H,"class","flex justify-end w-3/6"),y(C,"class","crd-itm-psn"),y(z,"class","w-4/6"),y(R,"class","ipt-rnd text-left focus:border-indigo-500"),y(R,"type","text"),y(I,"class","flex justify-end w-3/6"),y(B,"class","crd-itm-psn"),y(U,"class","btn-lg")},m(s,g){p(s,e,g),f(e,n),f(e,r),f(e,l),Q.m(l,null),p(s,o,g),p(s,c,g),f(c,i),f(c,a),f(c,u),f(u,d),J(d,t[0].mqttServer),p(s,m,g),p(s,$,g),f($,x),f($,w),f($,k),f(k,_),J(_,t[0].mqttPort),p(s,j,g),p(s,S,g),f(S,T),f(S,M),f(S,L),f(L,q),J(q,t[0].mqttPrefix),p(s,E,g),p(s,C,g),f(C,O),f(C,N),f(C,H),f(H,P),J(P,t[0].mqttUser),p(s,A,g),p(s,B,g),f(B,z),f(B,D),f(B,I),f(I,R),J(R,t[0].mqttPass),p(s,F,g),p(s,U,g),W||(V=[v(d,"input",t[16]),v(_,"input",t[17]),v(q,"input",t[18]),v(P,"input",t[19]),v(R,"input",t[20]),v(U,"click",t[21])],W=!0)},p(t,e){K!==(K=Y(t))&&(Q.d(1),Q=K(t),Q&&(Q.c(),Q.m(l,null))),5&e&&d.value!==t[0].mqttServer&&J(d,t[0].mqttServer),5&e&&_.value!==t[0].mqttPort&&J(_,t[0].mqttPort),5&e&&q.value!==t[0].mqttPrefix&&J(q,t[0].mqttPrefix),5&e&&P.value!==t[0].mqttUser&&J(P,t[0].mqttUser),5&e&&R.value!==t[0].mqttPass&&J(R,t[0].mqttPass)},d(t){t&&g(e),Q.d(),t&&g(o),t&&g(c),t&&g(m),t&&g($),t&&g(j),t&&g(S),t&&g(E),t&&g(C),t&&g(A),t&&g(B),t&&g(F),t&&g(U),W=!1,s(V)}}}function Xe(e){let n,s,r;return{c(){n=h("button"),n.textContent="Перезагрузить устройство",y(n,"class","btn-lg")},m(t,l){p(t,n,l),s||(r=v(n,"click",e[22]),s=!0)},p:t,d(t){t&&g(n),s=!1,r()}}}function tn(t){let e,n,s,r;const l=[Fe,Re],o=[];function c(t,e){return t[3]?0:1}return e=c(t),n=o[e]=l[e](t),{c(){n.c(),s=w()},m(t,n){o[e].m(t,n),p(t,s,n),r=!0},p(t,[r]){let i=e;e=c(t),e===i?o[e].p(t,r):(K(),Z(o[i],1,1,(()=>{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function en(t,e,n){let{settingsJson:s}=e,{errorsJson:r}=e,{ssidJson:l}=e,{show:o}=e,{ssidClick:c=(()=>{})}=e,{saveSett:i=(()=>{})}=e,{saveMqtt:a=(()=>{})}=e,{rebootEsp:u=(()=>{})}=e;return t.$$set=t=>{"settingsJson"in t&&n(0,s=t.settingsJson),"errorsJson"in t&&n(1,r=t.errorsJson),"ssidJson"in t&&n(2,l=t.ssidJson),"show"in t&&n(3,o=t.show),"ssidClick"in t&&n(4,c=t.ssidClick),"saveSett"in t&&n(5,i=t.saveSett),"saveMqtt"in t&&n(6,a=t.saveMqtt),"rebootEsp"in t&&n(7,u=t.rebootEsp)},[s,r,l,o,c,i,a,u,function(){s.name=this.value,n(0,s),n(2,l)},function(){s.apssid=this.value,n(0,s),n(2,l)},function(){s.appass=this.value,n(0,s),n(2,l)},function(){s.routerssid=S(this),n(0,s),n(2,l)},()=>c(),function(){s.routerpass=this.value,n(0,s),n(2,l)},function(){s.serverip=this.value,n(0,s),n(2,l)},()=>i(),function(){s.mqttServer=this.value,n(0,s),n(2,l)},function(){s.mqttPort=this.value,n(0,s),n(2,l)},function(){s.mqttPrefix=this.value,n(0,s),n(2,l)},function(){s.mqttUser=this.value,n(0,s),n(2,l)},function(){s.mqttPass=this.value,n(0,s),n(2,l)},()=>a(),()=>u()]}class nn extends rt{constructor(t){super(),st(this,t,en,tn,l,{settingsJson:0,errorsJson:1,ssidJson:2,show:3,ssidClick:4,saveSett:5,saveMqtt:6,rebootEsp:7})}}function sn(t,e,n){const s=t.slice();return s[13]=e[n],s[15]=n,s}function rn(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function ln(t){let e,n,s,r,l;return n=new zt({props:{title:"Список устройств",$$slots:{default:[an]},$$scope:{ctx:t}}}),r=new Ct({props:{$$slots:{default:[un]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),y(e,"class","grd-1col1")},m(t,o){p(t,e,o),et(n,e,null),f(e,s),et(r,e,null),l=!0},p(t,e){const s={};65591&e&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};65536&e&&(l.$$scope={dirty:e,ctx:t}),r.$set(l)},i(t){l||(G(n.$$.fragment,t),G(r.$$.fragment,t),l=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),l=!1},d(t){t&&g(e),nt(n),nt(r)}}}function on(t){let e,n,s,r,l,o,c,i,a,u,d,m,$,w,v,k,J,j,S,T=t[13].name+"",M=t[13].ip+"",L=t[13].id+"",q=t[13].status?"online":"offline";return j=new Tt({props:{click:function(){return t[7](t[15])}}}),{c(){e=h("tr"),n=h("td"),s=x(T),r=b(),l=h("td"),o=h("a"),c=x(M),a=b(),u=h("td"),d=x(L),m=b(),$=h("td"),w=x(q),k=b(),J=h("td"),tt(j.$$.fragment),y(n,"class","tbl-bdy-lg ipt-lg w-full"),y(o,"href",i="http://"+t[13].ip),y(l,"class","tbl-bdy-lg ipt-lg w-full"),y(u,"class","tbl-bdy-lg ipt-lg w-full"),y($,"class",v="tbl-bdy-lg ipt-lg w-full "+(t[13].status?"bg-green-50":"bg-red-50")),y(J,"class","tbl-bdy-lg"),y(e,"class","txt-sz txt-pad")},m(t,i){p(t,e,i),f(e,n),f(n,s),f(e,r),f(e,l),f(l,o),f(o,c),f(e,a),f(e,u),f(u,d),f(e,m),f(e,$),f($,w),f(e,k),f(e,J),et(j,J,null),S=!0},p(e,n){t=e,(!S||1&n)&&T!==(T=t[13].name+"")&&_(s,T),(!S||1&n)&&M!==(M=t[13].ip+"")&&_(c,M),(!S||1&n&&i!==(i="http://"+t[13].ip))&&y(o,"href",i),(!S||1&n)&&L!==(L=t[13].id+"")&&_(d,L),(!S||1&n)&&q!==(q=t[13].status?"online":"offline")&&_(w,q),(!S||1&n&&v!==(v="tbl-bdy-lg ipt-lg w-full "+(t[13].status?"bg-green-50":"bg-red-50")))&&y($,"class",v)},i(t){S||(G(j.$$.fragment,t),S=!0)},o(t){Z(j.$$.fragment,t),S=!1},d(t){t&&g(e),nt(j)}}}function cn(t){let e,n,r,l,o,c,i,a,u,d,m,$,x;return{c(){e=h("tr"),n=h("td"),r=h("input"),l=b(),o=h("td"),c=h("input"),i=b(),a=h("td"),u=h("input"),d=b(),m=h("td"),y(r,"class","ipt-lg w-full"),y(r,"type","text"),y(n,"class","tbl-bdy-lg"),y(c,"class","ipt-lg w-full"),y(c,"type","text"),y(o,"class","tbl-bdy-lg"),y(u,"class","ipt-lg w-full"),y(u,"type","text"),y(a,"class","tbl-bdy-lg"),y(m,"class","tbl-bdy-lg"),y(e,"class","txt-sz txt-pad")},m(s,g){p(s,e,g),f(e,n),f(n,r),J(r,t[2].name),f(e,l),f(e,o),f(o,c),J(c,t[2].ip),f(e,i),f(e,a),f(a,u),J(u,t[2].id),f(e,d),f(e,m),$||(x=[v(r,"input",t[8]),v(c,"input",t[9]),v(u,"input",t[10])],$=!0)},p(t,e){4&e&&r.value!==t[2].name&&J(r,t[2].name),4&e&&c.value!==t[2].ip&&J(c,t[2].ip),4&e&&u.value!==t[2].id&&J(u,t[2].id)},d(t){t&&g(e),$=!1,s(x)}}}function an(t){let e,n,r,l,o,c,i,a,u,d,$,w,k,J,j=t[1]?"Сохранить":"Добавить устройство",S=t[0],T=[];for(let e=0;eZ(T[t],1,1,(()=>{T[t]=null}));let L=t[1]&&cn(t);return{c(){e=h("table"),n=h("thead"),n.innerHTML='Название устройства \n IP адрес \n Идентификатор \n Состояние \n ',r=b(),l=h("tbody");for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function fn(t,e,n){let{show:s}=e,{deviceList:r}=e,{showInput:l}=e,{newDevice:o={}}=e,{addDevInList:c=(()=>{})}=e,{sendToAllDevices:i=(t=>{})}=e;function a(t){for(let e=0;e{"show"in t&&n(3,s=t.show),"deviceList"in t&&n(0,r=t.deviceList),"showInput"in t&&n(1,l=t.showInput),"newDevice"in t&&n(2,o=t.newDevice),"addDevInList"in t&&n(4,c=t.addDevInList),"sendToAllDevices"in t&&n(5,i=t.sendToAllDevices)},[r,l,o,s,c,i,a,t=>a(t),function(){o.name=this.value,n(2,o)},function(){o.ip=this.value,n(2,o)},function(){o.id=this.value,n(2,o)},()=>(n(1,l=!l),c()),t=>i("/reboot|")]}class pn extends rt{constructor(t){super(),st(this,t,fn,dn,l,{show:3,deviceList:0,showInput:1,newDevice:2,addDevInList:4,sendToAllDevices:5})}}function gn(t,e,n){const s=t.slice();return s[21]=e[n][0],s[22]=e[n][1],s[24]=n,s}function mn(t,e,n){const s=t.slice();return s[25]=e[n],s[24]=n,s}function hn(t,e,n){const s=t.slice();return s[21]=e[n][0],s[22]=e[n][1],s}function $n(e){let n,s;return n=new Ct({props:{title:"Загрузка..."}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function xn(t){let e,n,s,r,l,o,c,i,a,u;return n=new zt({props:{title:"Системная информация",$$slots:{default:[Sn]},$$scope:{ctx:t}}}),r=new zt({props:{title:"Системные настройки",$$slots:{default:[Mn]},$$scope:{ctx:t}}}),o=new zt({props:{title:"Лог",class:"z-50",$$slots:{default:[qn]},$$scope:{ctx:t}}}),a=new zt({props:{title:"Системные ошибки",$$slots:{default:[Hn]},$$scope:{ctx:t}}}),{c(){e=h("div"),tt(n.$$.fragment),s=b(),tt(r.$$.fragment),l=b(),tt(o.$$.fragment),c=b(),i=h("div"),tt(a.$$.fragment),y(e,"class","grd-3col1"),y(i,"class","grd-1col1")},m(t,d){p(t,e,d),et(n,e,null),f(e,s),et(r,e,null),f(e,l),et(o,e,null),p(t,c,d),p(t,i,d),et(a,i,null),u=!0},p(t,e){const s={};536871001&e&&(s.$$scope={dirty:e,ctx:t}),n.$set(s);const l={};536871046&e&&(l.$$scope={dirty:e,ctx:t}),r.$set(l);const c={};536870944&e&&(c.$$scope={dirty:e,ctx:t}),o.$set(c);const i={};536871432&e&&(i.$$scope={dirty:e,ctx:t}),a.$set(i)},i(t){u||(G(n.$$.fragment,t),G(r.$$.fragment,t),G(o.$$.fragment,t),G(a.$$.fragment,t),u=!0)},o(t){Z(n.$$.fragment,t),Z(r.$$.fragment,t),Z(o.$$.fragment,t),Z(a.$$.fragment,t),u=!1},d(t){t&&g(e),nt(n),nt(r),nt(o),t&&g(c),t&&g(i),nt(a)}}}function bn(t){let e,n,s,r,l=t[22]+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[22],e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){16&s&&l!==(l=t[22]+"")&&_(n,l),16&s&&r!==(r=t[22])&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function wn(t){let e;return{c(){e=h("p"),e.textContent="не подключено",y(e,"class","text-red-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function vn(t){let e;return{c(){e=h("p"),e.textContent="нет сигнала",y(e,"class","text-red-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function yn(t){let e;return{c(){e=h("p"),e.textContent="очень низкий",y(e,"class","text-red-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function kn(t){let e;return{c(){e=h("p"),e.textContent="низкий",y(e,"class","text-yellow-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function _n(t){let e;return{c(){e=h("p"),e.textContent="хороший",y(e,"class","text-yellow-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Jn(t){let e;return{c(){e=h("p"),e.textContent="очень хороший",y(e,"class","text-green-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function jn(t){let e;return{c(){e=h("p"),e.textContent="отличный",y(e,"class","text-green-500 font-bold text-sm text-center truncate")},m(t,n){p(t,e,n)},d(t){t&&g(e)}}}function Sn(t){let e,n,r,l,o,c,i,a,u,d,$,w,k,J,S,T,M,L,q,E,C,O,N,H,P,A,B,z,I,R,F,U,W,V,Y,K,Q,G,Z,X,tt,et,nt,st,rt,lt,ot,ct,it,at,ut,dt,ft,pt,gt,mt,ht,$t,xt,bt,wt,vt,yt,kt,_t,Jt,jt,St,Tt,Mt,Lt,qt,Et,Ct,Ot,Nt,Ht,Pt,At,Bt,zt,Dt,It,Rt,Ft=t[3].bn+"",Ut=t[3].bver+"",Wt=t[3].timenow+"",Vt=t[3].upt+"",Yt=t[3].uptm+"",Kt=t[3].uptw+"",Qt=t[3].heap+"",Gt=t[3].fl+"",Zt=t[3].rst+"",Xt=Object.entries(t[4]),te=[];for(let e=0;eНазвание прошивки

',r=b(),l=h("div"),o=h("p"),c=x(Ft),i=b(),a=h("div"),u=h("div"),u.innerHTML='

Доступные версии

',d=b(),$=h("div"),w=h("select");for(let t=0;tВерсия прошивки

',T=b(),M=h("div"),L=h("p"),q=x(Ut),E=b(),C=h("div"),O=h("div"),O.innerHTML='

Время на устройстве

',N=b(),H=h("div"),P=h("p"),A=x(Wt),B=b(),z=h("div"),I=h("div"),I.innerHTML='

Uptime устройства

',R=b(),F=h("div"),U=h("p"),W=x(Vt),V=b(),Y=h("div"),K=h("div"),K.innerHTML='

Uptime сессии mqtt

',Q=b(),G=h("div"),Z=h("p"),X=x(Yt),tt=b(),et=h("div"),nt=h("div"),nt.innerHTML='

Uptime сессии wifi

',st=b(),rt=h("div"),lt=h("p"),ot=x(Kt),ct=b(),it=h("div"),at=h("div"),at.innerHTML='

Качество WiFi сигнала

',ut=b(),dt=h("div"),ee&&ee.c(),ft=b(),ne&&ne.c(),pt=b(),se&&se.c(),gt=b(),re&&re.c(),mt=b(),le&&le.c(),ht=b(),oe&&oe.c(),$t=b(),ce&&ce.c(),xt=b(),bt=h("div"),wt=h("div"),wt.innerHTML='

Остаток RAM

',vt=b(),yt=h("div"),kt=h("p"),_t=x(Qt),Jt=b(),jt=h("div"),St=h("div"),St.innerHTML='

Кол-во записей на flash

',Tt=b(),Mt=h("div"),Lt=h("p"),qt=x(Gt),Et=b(),Ct=h("div"),Ot=h("div"),Ot.innerHTML='

Причина перезагрузки

',Nt=b(),Ht=h("div"),Pt=h("p"),At=x(Zt),zt=b(),Dt=h("button"),Dt.textContent="Обновить прошивку",y(n,"class","w-2/3"),y(o,"class","text-gray-500 font-bold text-sm text-center truncate"),y(l,"class","flex justify-center w-1/3"),y(e,"class","flex mb-2 h-6 items-center"),y(u,"class","w-2/3"),y(w,"class","border border-indigo-500 border-4 text-center"),void 0===t[0]&&D((()=>t[12].call(w))),y($,"class","flex justify-center w-1/3"),y(a,"class","flex mb-2 h-6 items-center"),y(S,"class","w-2/3"),y(L,"class","text-gray-500 font-bold text-sm text-center truncate"),y(M,"class","flex justify-center w-1/3"),y(J,"class","flex mb-2 h-6 items-center"),y(O,"class","w-2/3"),y(P,"class","text-gray-500 font-bold text-sm text-center truncate"),y(H,"class","flex justify-center w-1/3"),y(C,"class","flex mb-2 h-6 items-center"),y(I,"class","w-2/3"),y(U,"class","text-gray-500 font-bold text-sm text-center truncate"),y(F,"class","flex justify-center w-1/3"),y(z,"class","flex mb-2 h-6 items-center"),y(K,"class","w-2/3"),y(Z,"class","text-gray-500 font-bold text-sm text-center truncate"),y(G,"class","flex justify-center w-1/3"),y(Y,"class","flex mb-2 h-6 items-center"),y(nt,"class","w-2/3"),y(lt,"class","text-gray-500 font-bold text-sm text-center truncate"),y(rt,"class","flex justify-center w-1/3"),y(et,"class","flex mb-2 h-6 items-center"),y(at,"class","w-2/3"),y(dt,"class","flex justify-center w-1/3 text-xs sm:text-sm md:text-base lg:text-base xl:text-base 2xl:text-base break-words"),y(it,"class","flex mb-2 h-6 items-center"),y(wt,"class","w-2/3"),y(kt,"class","text-green-500 font-bold text-center truncate"),y(yt,"class","flex justify-center w-1/3 text-sm text-center"),y(bt,"class","flex mb-2 h-6 items-center"),y(St,"class","w-2/3"),y(Lt,"class","text-green-500 font-bold text-center truncate"),y(Mt,"class","flex justify-center w-1/3 text-sm"),y(jt,"class","flex mb-2 h-6 items-center"),y(Ot,"class","w-2/3"),y(Pt,"class",Bt=(t[3].rst.toString().includes("Watchdog")||t[3].rst.toString().includes("Exception")?"text-red-500":"text-green-500")+" font-bold text-center truncate"),y(Ht,"class","flex justify-center w-1/3 text-sm"),y(Ct,"class","flex mb-2 h-6 items-center"),y(Dt,"class","btn-lg")},m(s,g){p(s,e,g),f(e,n),f(e,r),f(e,l),f(l,o),f(o,c),p(s,i,g),p(s,a,g),f(a,u),f(a,d),f(a,$),f($,w);for(let t=0;tВключить лог

',r=b(),l=h("div"),o=h("label"),c=h("div"),i=h("input"),a=b(),u=h("div"),m=b(),$=h("div"),x=b(),_=h("div"),j=h("div"),j.innerHTML='

Часовой пояс

',S=b(),T=h("div"),M=h("input"),L=b(),O&&O.c(),q=w(),y(n,"class","w-2/3"),y(i,"id","log"),y(i,"type","checkbox"),y(i,"class","sr-only"),y(u,"class",d="block "+(t[1].log?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg"),y($,"class","dot bg-gray-100 absolute left-1 top-1 w-4 h-4 rounded-full transition shadow-lg"),y(c,"class","relative"),y(o,"for","log"),y(o,"class","items-center cursor-pointer"),y(l,"class","flex justify-center w-1/3"),y(e,"class","flex mb-2 h-6 items-center"),y(j,"class","w-2/3"),y(M,"class","ipt-rnd text-center focus:border-indigo-500"),y(M,"type","number"),y(T,"class","flex justify-center w-1/3"),y(_,"class","flex mb-2 h-6 items-center")},m(s,d){p(s,e,d),f(e,n),f(e,r),f(e,l),f(l,o),f(o,c),f(c,i),i.checked=t[1].log,f(c,a),f(c,u),f(c,m),f(c,$),p(s,x,d),p(s,_,d),f(_,j),f(_,S),f(_,T),f(T,M),J(M,t[1].timezone),p(s,L,d),O&&O.m(s,d),p(s,q,d),E||(C=[v(i,"change",t[14]),v(i,"change",t[15]),v(M,"input",t[16]),v(M,"change",t[17])],E=!0)},p(t,e){2&e&&(i.checked=t[1].log),2&e&&d!==(d="block "+(t[1].log?"bg-blue-600":"bg-gray-600")+" w-10 h-6 rounded-full shadow-lg")&&y(u,"class",d),2&e&&k(M.value)!==t[1].timezone&&J(M,t[1].timezone),t[2]?O?O.p(t,e):(O=Tn(t),O.c(),O.m(q.parentNode,q)):O&&(O.d(1),O=null)},d(t){t&&g(e),t&&g(x),t&&g(_),t&&g(L),O&&O.d(t),t&&g(q),E=!1,s(C)}}}function Ln(t){let e,n,s,r=t[25].msg+"";return{c(){e=h("div"),n=x(r),y(e,"class",s=t[25].msg.toString().includes("[E]")?"text-xs text-red-500":"text-xs text-black")},m(t,s){p(t,e,s),f(e,n)},p(t,l){32&l&&r!==(r=t[25].msg+"")&&_(n,r),32&l&&s!==(s=t[25].msg.toString().includes("[E]")?"text-xs text-red-500":"text-xs text-black")&&y(e,"class",s)},d(t){t&&g(e)}}}function qn(t){let e,n=t[5],s=[];for(let e=0;e{s=null})),Q())},i(t){n||(G(s),n=!0)},o(t){Z(s),n=!1},d(t){s&&s.d(t),t&&g(e)}}}function Hn(t){let e,n,s=Object.entries(t[3]),r=[];for(let e=0;eZ(r[t],1,1,(()=>{r[t]=null}));return{c(){for(let t=0;t{o[i]=null})),Q(),n=o[e],n?n.p(t,r):(n=o[e]=l[e](t),n.c()),G(n,1),n.m(s.parentNode,s))},i(t){r||(G(n),r=!0)},o(t){Z(n),r=!1},d(t){o[e].d(t),t&&g(s)}}}function An(t,e,n){let{errorsJson:s}=e,{rebootEsp:r=(()=>{})}=e,{versionsList:l}=e,{choosingVersion:o}=e,{coreMessages:c}=e,{settingsJson:i}=e,{startUpdate:a=(()=>{})}=e,{saveSett:u=(()=>{})}=e,{show:d}=e,{paramsBeenChanged:f=!1}=e,{cancelAlarm:p=(t=>{})}=e;return t.$$set=t=>{"errorsJson"in t&&n(3,s=t.errorsJson),"rebootEsp"in t&&n(11,r=t.rebootEsp),"versionsList"in t&&n(4,l=t.versionsList),"choosingVersion"in t&&n(0,o=t.choosingVersion),"coreMessages"in t&&n(5,c=t.coreMessages),"settingsJson"in t&&n(1,i=t.settingsJson),"startUpdate"in t&&n(6,a=t.startUpdate),"saveSett"in t&&n(7,u=t.saveSett),"show"in t&&n(8,d=t.show),"paramsBeenChanged"in t&&n(2,f=t.paramsBeenChanged),"cancelAlarm"in t&&n(9,p=t.cancelAlarm)},[o,i,f,s,l,c,a,u,d,p,{mqtt:{e1:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Нет ответа от сервера",cancel:!1},e2:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Соединение было разорвано",cancel:!1},e3:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Ошибка соединения. Обычно возникает когда неверно указано название сервера MQTT",cancel:!1},e4:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Клиент был отключен",cancel:!1},e6:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Ошибка версии",cancel:!1},e7:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Отклонен идентификатор",cancel:!1},e8:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Не могу установить соединение",cancel:!1},e9:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Неправильное имя пользователя/пароль"},e10:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Не авторизован для подключения",cancel:!1},e11:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Название сервера пустое",cancel:!1},e12:{descr:"Ошибка mqtt",color:"text-red-500",txt:"Имя пользователя или пароль пустые",cancel:!1},e13:{descr:"Mqtt",color:"text-red-500",txt:"Подключение в процессе",cancel:!1}},wse1:{1:{descr:"Ошибка веб сокетов",color:"text-red-500",txt:"Слишком много клиентов было открыто. Допускается не более четырех.",cancel:!0}},jse1:{1:{descr:"Ошибка json",color:"text-red-500",txt:"Недостаточный размер буфера библиотеки Arduino Json. Устройство может вести себя непредсказуемо. Обратитесь к разработчику.",cancel:!0}},jse2:{1:{descr:"Ошибка json",color:"text-red-500",txt:"Ошибка записи/чтения json.",cancel:!0,num:!0}},jse3:{1:{descr:"Ошибка json",color:"text-red-500",txt:"Ошибка чтения json файла с виджетами",cancel:!0}}},r,function(){o=S(this),n(0,o),n(4,l)},()=>a(),function(){i.log=this.checked,n(1,i)},()=>n(2,f=!0),function(){i.timezone=k(this.value),n(1,i)},()=>n(2,f=!0),()=>(u(),n(2,f=!1)),t=>p(t)]}class Bn extends rt{constructor(t){super(),st(this,t,An,Pn,l,{errorsJson:3,rebootEsp:11,versionsList:4,choosingVersion:0,coreMessages:5,settingsJson:1,startUpdate:6,saveSett:7,show:8,paramsBeenChanged:2,cancelAlarm:9})}}function zn(e){let n,s,r,l;return{c(){n=$("svg"),s=$("path"),y(s,"d","M7 18a4.6 4.4 0 0 1 0 -9h0a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12"),y(n,"class",r="h-8 w-8 "+e[0]),y(n,"width","8"),y(n,"height","8"),y(n,"viewBox",l=e[1]+" "+e[2]+" 24 24"),y(n,"stroke-width","2"),y(n,"stroke","currentColor"),y(n,"fill","none"),y(n,"stroke-linecap","round"),y(n,"stroke-linejoin","round")},m(t,e){p(t,n,e),f(n,s)},p(t,[e]){1&e&&r!==(r="h-8 w-8 "+t[0])&&y(n,"class",r),6&e&&l!==(l=t[1]+" "+t[2]+" 24 24")&&y(n,"viewBox",l)},i:t,o:t,d(t){t&&g(n)}}}function Dn(t,e,n){let{color:s}=e,{x:r=0}=e,{y:l=0}=e;return t.$$set=t=>{"color"in t&&n(0,s=t.color),"x"in t&&n(1,r=t.x),"y"in t&&n(2,l=t.y)},[s,r,l]}class In extends rt{constructor(t){super(),st(this,t,Dn,zn,l,{color:0,x:1,y:2})}}function Rn(t,e,n){const s=t.slice();return s[118]=e[n],s}function Fn(t){let e,n;return e=new Nt({}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Un(t){let e,n,s,r,l=t[118].name+"";return{c(){e=h("option"),n=x(l),s=b(),e.__value=r=t[118].ws,e.value=e.__value},m(t,r){p(t,e,r),f(e,n),f(e,s)},p(t,s){524288&s[0]&&l!==(l=t[118].name+"")&&_(n,l),524288&s[0]&&r!==(r=t[118].ws)&&(e.__value=r,e.value=e.__value)},d(t){t&&g(e)}}}function Wn(t){let e,n,s,r,l,o,c,i,a,u;return e=new Jt({props:{path:"/",$$slots:{default:[Yn]},$$scope:{ctx:t}}}),s=new Jt({props:{path:"/config",$$slots:{default:[Kn]},$$scope:{ctx:t}}}),l=new Jt({props:{path:"/connection",$$slots:{default:[Qn]},$$scope:{ctx:t}}}),c=new Jt({props:{path:"/list",$$slots:{default:[Gn]},$$scope:{ctx:t}}}),a=new Jt({props:{path:"/system",$$slots:{default:[Zn]},$$scope:{ctx:t}}}),{c(){tt(e.$$.fragment),n=b(),tt(s.$$.fragment),r=b(),tt(l.$$.fragment),o=b(),tt(c.$$.fragment),i=b(),tt(a.$$.fragment)},m(t,d){et(e,t,d),p(t,n,d),et(s,t,d),p(t,r,d),et(l,t,d),p(t,o,d),et(c,t,d),p(t,i,d),et(a,t,d),u=!0},p(t,n){const r={};16408&n[0]|268435456&n[3]&&(r.$$scope={dirty:n,ctx:t}),e.$set(r);const o={};276512&n[0]|268435456&n[3]&&(o.$$scope={dirty:n,ctx:t}),s.$set(o);const i={};229440&n[0]|268435456&n[3]&&(i.$$scope={dirty:n,ctx:t}),l.$set(i);const u={};4718720&n[0]|268435456&n[3]&&(u.$$scope={dirty:n,ctx:t}),c.$set(u);const d={};8488704&n[0]|268435456&n[3]&&(d.$$scope={dirty:n,ctx:t}),a.$set(d)},i(t){u||(G(e.$$.fragment,t),G(s.$$.fragment,t),G(l.$$.fragment,t),G(c.$$.fragment,t),G(a.$$.fragment,t),u=!0)},o(t){Z(e.$$.fragment,t),Z(s.$$.fragment,t),Z(l.$$.fragment,t),Z(c.$$.fragment,t),Z(a.$$.fragment,t),u=!1},d(t){nt(e,t),t&&g(n),nt(s,t),t&&g(r),nt(l,t),t&&g(o),nt(c,t),t&&g(i),nt(a,t)}}}function Vn(e){let n,s;return n=new Ct({props:{title:"Нет соединения"}}),{c(){tt(n.$$.fragment)},m(t,e){et(n,t,e),s=!0},p:t,i(t){s||(G(n.$$.fragment,t),s=!0)},o(t){Z(n.$$.fragment,t),s=!1},d(t){nt(n,t)}}}function Yn(t){let e,n,s,r;return e=new xe({props:{show:t[4],layoutJson:t[14],pages:t[3],wsPush:t[40]}}),{c(){tt(e.$$.fragment),n=b(),s=w()},m(t,l){et(e,t,l),p(t,n,l),p(t,s,l),r=!0},p(t,n){const s={};16&n[0]&&(s.show=t[4]),16384&n[0]&&(s.layoutJson=t[14]),8&n[0]&&(s.pages=t[3]),e.$set(s)},i(t){r||(G(e.$$.fragment,t),r=!0)},o(t){Z(e.$$.fragment,t),r=!1},d(t){nt(e,t),t&&g(n),t&&g(s)}}}function Kn(t){let e,n;return e=new De({props:{show:t[5],configJson:t[11],widgetsJson:t[12],itemsJson:t[13],saveConfig:t[41],rebootEsp:t[42],scenarioTxt:t[18]}}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},p(t,n){const s={};32&n[0]&&(s.show=t[5]),2048&n[0]&&(s.configJson=t[11]),4096&n[0]&&(s.widgetsJson=t[12]),8192&n[0]&&(s.itemsJson=t[13]),262144&n[0]&&(s.scenarioTxt=t[18]),e.$set(s)},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Qn(t){let e,n;return e=new nn({props:{show:t[6],rebootEsp:t[43],ssidClick:t[44],saveSett:t[45],saveMqtt:t[46],settingsJson:t[15],errorsJson:t[16],ssidJson:t[17]}}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},p(t,n){const s={};64&n[0]&&(s.show=t[6]),32768&n[0]&&(s.settingsJson=t[15]),65536&n[0]&&(s.errorsJson=t[16]),131072&n[0]&&(s.ssidJson=t[17]),e.$set(s)},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Gn(t){let e,n;return e=new pn({props:{show:t[7],deviceList:t[19],showInput:ts,addDevInList:t[47],newDevice:t[22],sendToAllDevices:t[48]}}),{c(){tt(e.$$.fragment)},m(t,s){et(e,t,s),n=!0},p(t,n){const s={};128&n[0]&&(s.show=t[7]),524288&n[0]&&(s.deviceList=t[19]),4194304&n[0]&&(s.newDevice=t[22]),e.$set(s)},i(t){n||(G(e.$$.fragment,t),n=!0)},o(t){Z(e.$$.fragment,t),n=!1},d(t){nt(e,t)}}}function Zn(t){let e,n,s;function r(e){t[53](e)}let l={show:t[8],errorsJson:t[16],settingsJson:t[15],saveSett:t[49],rebootEsp:t[50],cancelAlarm:t[51],versionsList:t[9],startUpdate:t[52],coreMessages:t[23]};return void 0!==t[10]&&(l.choosingVersion=t[10]),e=new Bn({props:l}),O.push((()=>X(e,"choosingVersion",r))),{c(){tt(e.$$.fragment)},m(t,n){et(e,t,n),s=!0},p(t,s){const r={};256&s[0]&&(r.show=t[8]),65536&s[0]&&(r.errorsJson=t[16]),32768&s[0]&&(r.settingsJson=t[15]),512&s[0]&&(r.versionsList=t[9]),8388608&s[0]&&(r.coreMessages=t[23]),!n&&1024&s[0]&&(n=!0,r.choosingVersion=t[10],I((()=>n=!1))),e.$set(r)},i(t){s||(G(e.$$.fragment,t),s=!0)},o(t){Z(e.$$.fragment,t),s=!1},d(t){nt(e,t)}}}function Xn(t){let e,n,r,l,o,c,i,a,u,d,$,x,w,k,_,J,S,T,M,L,q,E,C,O,N,H,P,A,B,z,I,R,F,U,W,V,Y,X,st,rt,lt,ot,ct=t[2]&&Fn(),it=t[19],at=[];for(let e=0;e",_=b(),J=h("ul"),S=h("li"),T=h("a"),T.textContent="Управление",M=b(),L=h("li"),q=h("a"),q.textContent="Конфигуратор",E=b(),C=h("li"),O=h("a"),O.textContent="Подключение",N=b(),H=h("li"),P=h("a"),P.textContent="Устройства",A=b(),B=h("li"),z=h("a"),z.textContent="Системные",I=b(),R=h("main"),F=h("ul"),U=h("div"),V.c(),X=b(),st=h("footer"),st.innerHTML='
Developed by Dmitry Borisenko
',y(c,"class","border border-indigo-500 border-4"),void 0===t[21]&&D((()=>t[36].call(c))),y(o,"class","px-15 py-1"),y(a,"class","pl-4 pr-4 py-1"),y(l,"class","flex content-center items-center justify-end"),y(r,"class","h-10 w-full bg-gray-100 overflow-auto shadow-md"),y(x,"class","w-0 h-0"),y(x,"id","menu__toggle"),y(x,"type","checkbox"),y(k,"class","menu__btn"),y(k,"for","menu__toggle"),y(T,"class","menu__item"),y(T,"href","/"),y(q,"class","menu__item"),y(q,"href","/config"),y(O,"class","menu__item"),y(O,"href","/connection"),y(P,"class","menu__item"),y(P,"href","/list"),y(z,"class","menu__item"),y(z,"href","/system"),y(J,"class","menu__box"),y($,"class","flex"),y(U,"class","bg-cover pt-0 px-4"),y(F,"class","menu__main"),y(R,"class",Y="flex-1 overflow-y-auto p-0 "+(!0!==t[0]||t[1]?"ml-0":"ml-36")),y(st,"class","h-4 bg-gray-100 border-gray-200 shadow-lg"),y(e,"class","flex flex-col h-screen bg-gray-50")},m(s,g){p(s,e,g),ct&&ct.m(e,null),f(e,n),f(e,r),f(r,l),f(l,o),f(o,c);for(let t=0;t{ct=null})),Q()),524288&s[0]){let e;for(it=t[19],e=0;e{dt[l]=null})),Q(),V=dt[W],V?V.p(t,s):(V=dt[W]=ut[W](t),V.c()),G(V,1),V.m(U,null)),(!rt||3&s[0]&&Y!==(Y="flex-1 overflow-y-auto p-0 "+(!0!==t[0]||t[1]?"ml-0":"ml-36")))&&y(R,"class",Y)},i(t){rt||(G(ct),G(u.$$.fragment,t),G(V),rt=!0)},o(t){Z(ct),Z(u.$$.fragment,t),Z(V),rt=!1},d(t){t&&g(e),ct&&ct.d(),m(at,t),nt(u),dt[W].d(),lt=!1,s(ot)}}}let ts=!1;function es(t){try{JSON.parse(t)}catch(e){return console.log("[e]","json parce error: ",t),!1}return!0}function ns(t,e,n){let s;o(t,xt,(t=>n(82,s=t))),xt.mode.hash();let r=!1,l=!1,c=document.location.hostname,i=!0,a=!1;const u=void 0;let d,f=[],p=!1,g=!1,m=!1,h=!1,$=!1,x={},b=[],w=!1,v=!1,y=[],k=!1,_=!1,J=[],j=!1,T=!1,M=[],L={},E=!1,C={},O=!1,N={},H=!1,P={},A=[],B=!1,z="",D=!1,I=!1,R=[];R=[{name:"--",id:"--",ip:c,ws:0,status:!1}];let F,U=[],W=!1,V=0,Y=!0,K={},Q=[];var G=function(){this.parts=[]};let Z;G.prototype.append=function(t){this.parts.push(t),this.blob=void 0},G.prototype.getBlob=function(){return this.blob||(this.blob=new Blob(this.parts,{type:"binary"})),this.blob},G.prototype.clear=function(){this.parts=[]};var X=new G,tt=new G,et=new G,nt=new G,st=[];function rt(){void 0!==V&&bt(V,Z)}function lt(){Jt(V);let t=0;R.forEach((e=>{e.ws=t,e.status||(ct(t),at(t)),t++})),n(19,R)}function ot(t,e){R.forEach((n=>{n.ws===t&&(n.status=e,n.status?console.log("[i]",n.ip,"status online"):console.log("[i]",n.ip,"status offline"))})),n(19,R),Jt(V),n(20,W=F.status)}function ct(t){let e=it(t);"error"===e?console.log("[e]","device list wrong"):(U[t]=new WebSocket("ws://"+e+":81"),U.binaryType="blob",console.log("[i]",e,t,"started connecting..."))}function it(t){let e="error";return R.forEach((n=>{t===n.ws&&(e=n.ip)})),e}function at(t){if(U[t]){let e=it(t);console.log("[i]",e,t,"web socket events added"),U[t].addEventListener("open",(function(n){console.log("[i]",e,t,"completed connecting"),ot(t,!0),i&&bt(0,"/list|"),"/|"===Z?bt(t,Z):t===V&&rt()})),U[t].addEventListener("message",(function(e){if("string"==typeof e.data){let c=e.data;if(t===V){if(c.includes('devicelist":"')&&es(c)&&(A=JSON.parse(c),A=A,i?(n(19,R=A),n(19,R[0].status=!0,R)):n(19,(r=R,l=A,o=new Set(r.map((t=>t.ip))),R=[...r,...l.filter((t=>!o.has(t.ip)))])),i=!1,n(19,R),B=!0,console.log("✔","deviceList json parced"),ut(),kt(),lt()),c.includes('ssid":"')&&es(c)&&(n(17,N=JSON.parse(c)),n(17,N),console.log("✔","ssidJson parced"),H=!0,ut()),c.includes('errors":"')&&es(c)&&(n(16,C=JSON.parse(c)),n(16,C),O=!0,console.log("✔","errorsJson json parced"),ut()),c.includes('settings":"')&&es(c)&&(n(15,L=JSON.parse(c)),n(15,L),E=!0,console.log("✔","settingsJson json parced"),ut()),c.includes("/log|")&&(c=c.replace("/log|",""),console.log("",c),yt(c)),"/st/scenario.txt"===c&&(D=!0),"/end/scenario.txt"===c){D=!1;var s=nt.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{n(18,z=t.result),n(18,z),I=!0,console.log("✔","scenarioTxt parced"),ut()}}if("/st/config.json"===c&&(w=!0),"/end/config.json"===c){w=!1;s=X.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{let e=t.result;es(e)&&(n(11,b=JSON.parse(e)),n(11,b),v=!0,console.log("✔","configJson parced"),ut())}}if("/st/widgets.json"===c&&(k=!0),"/end/widgets.json"===c){k=!1;s=tt.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{let e=t.result;es(e)&&(n(12,y=JSON.parse(e)),n(12,y),_=!0,console.log("✔","widgetsJson parced"),ut())}}if("/st/items.json"===c&&(j=!0),"/end/items.json"===c){j=!1;s=et.getBlob();let t=new FileReader;t.readAsText(s),t.onload=()=>{let e=t.result;es(e)&&(n(13,J=JSON.parse(e)),n(13,J),T=!0,console.log("✔","itemsJson parced"),ut())}}}if("/end/layout.json"===c&&async function(t){var e=st[t].getBlob();let s=new FileReader;s.readAsText(e),s.onload=()=>{let e=JSON.parse(s.result);!function(t,e){for(const[n,s]of Object.entries(P))for(let r=0;r{console.log("[e]",e,"connection closed"),ot(t,!1)})),U[t].addEventListener("error",(function(n){console.log("[e]",e,"connection error"),ot(t,!1)}))}else console.log("[e]","socket not exist")}async function ut(){"/|"===Z&&(mt(),console.log("✔","dashboard packet received"),n(4,p=!0)),"/config|"===Z&&T&&_&&v&&E&&I&&(mt(),console.log("✔✔","config data parced"),n(5,g=!0)),"/connection|"===Z&&H&&E&&O&&(mt(),console.log("✔✔","connection data parced"),n(6,m=!0)),"/list|"===Z&&B&&(mt(),console.log("✔✔","list data parced"),n(7,h=!0)),"/system|"===Z&&O&&E&&(mt(),async function(){try{let t=L.serverip+"/iotm/ver.json";console.log("url",t);let e=await fetch(t,{mode:"cors",method:"GET"});e.ok?(n(9,x=await e.json()),n(9,x=x[C.bn]),n(10,d=C.bver),console.log(JSON.stringify(x))):(n(10,d=void 0),console.log("error, versions list not received",e.statusText))}catch(t){n(10,d=void 0),console.log("error, versions list not received"),console.log(t)}}(),console.log("✔✔","system data parced"),n(8,$=!0))}function dt(){n(18,z),bt(V,"/tuoyal|"+JSON.stringify(function(){let t=[];for(let e=0;e5?(!function(t,e,n,s,r){for(let l=0;l5?bt(V,"/sgnittes|"+JSON.stringify(L)):window.alert("Ошибка"),gt(),bt(V,"/mqtt|")}function gt(){n(11,b=[]),X.clear(),n(12,y=[]),tt.clear(),n(13,J=[]),et.clear(),n(14,M=[]),st=[],n(18,z=""),nt.clear(),n(15,L={}),n(16,C={}),n(23,Q=[]),n(4,p=!1),n(5,g=!1),n(6,m=!1),n(7,h=!1),n(8,$=!1),mt(),console.log("[i]","all app data cleared")}function mt(){v=!1,_=!1,T=!1,E=!1,O=!1,H=!1,B=!1,I=!1,function(){for(let t=0;t{!function(t){let e=!1;return R.forEach((n=>{t===n.ws&&(e=n.status)})),e}(t.ws)?(ct(t.ws),at(t.ws)):bt(t.ws,"/tst|")})),Y=!1)}function bt(t,e){U[t]&&1===U[t].readyState?(U[t].send(e),console.log("[i]",it(t),t,"msg send success",e)):console.log("[e]",it(t),t,"msg not send",e)}function wt(t){R.forEach((e=>{e.status&&bt(e.ws,t)}))}function vt(){M.sort((function(t,e){return t.descre.descr?1:0})),n(3,f=[]);Array.from(new Set(Array.from(M,(({page:t})=>t)))).forEach((function(t,e,s){n(3,f=[...f,JSON.parse(JSON.stringify({page:t}))])})),f.sort((function(t,e){return t.pagee.page?1:0}))}xt.subscribe((function(){console.log("[i]","handle navigation"),gt(),Z=s.path.toString(),Z+="|",console.log("[i]","user on page:",Z),"/|"===Z?wt(Z):rt()})),q((async()=>{console.log("[i]","mounted"),kt(),i=!0,lt(),$t(),vt()}));const yt=t=>{Q.length>=100&&Q.shift(),n(23,Q=[...Q,{msg:t}]),Q.sort((function(t,e){return t.time>e.time?-1:t.time_t(),function(){r=this.checked,n(0,r)},()=>St(),(t,e,n)=>ht(t,e,n),()=>dt(),()=>Mt(),()=>Mt(),()=>Tt(),()=>ft(),()=>pt(),()=>jt(),t=>wt(t),()=>ft(),()=>Mt(),t=>qt(t),()=>Et(),function(t){d=t,n(10,d)}]}return new class extends rt{constructor(t){super(),st(this,t,ns,Xn,l,{},null,[-1,-1,-1,-1])}}({target:document.body,props:{name:"world"}})}(); -//# sourceMappingURL=bundle.js.map diff --git a/data_svelte/build/bundle.js.gz b/data_svelte/build/bundle.js.gz index 271fc7959866aac673e324edc02854e7c631c83a..fe45ea11fd2ed01aee1ae2914696e70d132662fc 100644 GIT binary patch delta 45041 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%0A%iV3)8l}kR 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 delta 45646 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 diff --git a/data_svelte/index.html b/data_svelte/index.html index b4164705..9f21ca72 100644 --- a/data_svelte/index.html +++ b/data_svelte/index.html @@ -4,12 +4,12 @@ - IoT Manager 4.4.0 + IoT Manager 4.4.1 - + - + From 3581d133d4897d21dc1dc35503fc49c44851575e Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 10 Oct 2022 22:52:18 +0300 Subject: [PATCH 014/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BC=D0=B5=D0=BD=D1=8F=D1=82=D1=8C?= =?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D1=83=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20Uint=20=D0=B2=20String?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/StringUtils.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/StringUtils.cpp b/src/utils/StringUtils.cpp index 0a2b06ac..a8d4ee3b 100644 --- a/src/utils/StringUtils.cpp +++ b/src/utils/StringUtils.cpp @@ -183,9 +183,8 @@ String prettyBytes(size_t size) { return String(size / 1024.0 / 1024.0 / 1024.0) + "GB"; } -String uint64ToString(uint64_t input) { +String uint64ToString(uint64_t input, uint8_t base) { String result = ""; - uint8_t base = 10; do { char c = input % base; From f9ddffba71e97f2b9c23b4bf54207be00e877374 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 10 Oct 2022 22:53:37 +0300 Subject: [PATCH 015/107] =?UTF-8?q?=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B1=D0=B0=D0=B7=D1=83=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D0=B2=D0=B5=D1=80=D1=82=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/utils/StringUtils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/utils/StringUtils.h b/include/utils/StringUtils.h index 5a1edc41..da9468cb 100644 --- a/include/utils/StringUtils.h +++ b/include/utils/StringUtils.h @@ -40,4 +40,4 @@ boolean isDigitDotCommaStr(const String& str); String prettyBytes(size_t size); -String uint64ToString(uint64_t input); +String uint64ToString(uint64_t input, uint8_t base = 10); From 0553503e1c65ed0d276c282589773646b403c541 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 10 Oct 2022 22:54:10 +0300 Subject: [PATCH 016/107] =?UTF-8?q?=D0=92=D1=8B=D0=B2=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D0=BC=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BE=20=D1=81=D0=BA=D0=B0=D0=BD=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B8=20I2C=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=81=D0=BE=D0=BB=D1=8C=20=D0=B2=D0=B5=D0=B1=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/I2CUtils.cpp | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/utils/I2CUtils.cpp b/src/utils/I2CUtils.cpp index 9d3f9032..a8c643c6 100644 --- a/src/utils/I2CUtils.cpp +++ b/src/utils/I2CUtils.cpp @@ -4,32 +4,29 @@ void scanI2C() { byte error, address; int nDevices; - - Serial.println("Scanning..."); + String message = ""; nDevices = 0; - for(address = 8; address < 127; address++ ){ + for(address = 8; address < 127; address++ ) { Wire.beginTransmission(address); error = Wire.endTransmission(); if (error == 0){ - Serial.print("I2C device found at address 0x"); - if (address<16) - Serial.print("0"); - Serial.print(address,HEX); - Serial.println(" !"); + message += "I2C device found at address 0x"; + message += uint64ToString(address, 16); + message += " !"; nDevices++; } else if (error==4) { - Serial.print("Unknow error at address 0x"); - if (address<16) - Serial.print("0"); - Serial.println(address,HEX); + message += "Unknow error at address 0x"; + message += uint64ToString(address, 16); } } if (nDevices == 0) - Serial.println("No I2C devices found\n"); + message += "No I2C devices found\n"; else - Serial.println("done\n"); + message += "done\n"; + + SerialPrint("i", "I2C Scaner", message); } \ No newline at end of file From cc28b45851486ee72822b8dcef45ff5fdffefeb2 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 10 Oct 2022 22:54:44 +0300 Subject: [PATCH 017/107] =?UTF-8?q?=D0=94=D0=B5=D0=BB=D0=B0=D0=B5=D0=BC=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B8=D1=81=D0=BA=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81?= =?UTF-8?q?=D0=B0=20I2C=20=D0=B1=D0=BE=D0=BB=D0=B5=D0=B5=20=D1=83=D0=B4?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/display/Lcd2004/Lcd2004.cpp | 11 +++++++---- src/modules/exec/Mcp23017/Mcp23017.cpp | 17 ++++++++++------- src/modules/exec/Pcf8574/Pcf8574.cpp | 15 ++++++++++----- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/modules/display/Lcd2004/Lcd2004.cpp b/src/modules/display/Lcd2004/Lcd2004.cpp index 74f3080d..968bed0b 100644 --- a/src/modules/display/Lcd2004/Lcd2004.cpp +++ b/src/modules/display/Lcd2004/Lcd2004.cpp @@ -15,16 +15,17 @@ class Lcd2004 : public IoTItem { String _id2show; String _descr; int _prevStrSize; + String _addr; bool _isShow = true; // экран показывает public: Lcd2004(String parameters) : IoTItem(parameters) { - String addr, size, xy; + String size, xy; _prevStrSize = 0; - jsonRead(parameters, "addr", addr); - if (addr == "") { + jsonRead(parameters, "addr", _addr); + if (_addr == "") { scanI2C(); return; } @@ -33,7 +34,7 @@ class Lcd2004 : public IoTItem { int w = selectFromMarkerToMarker(size, ",", 0).toInt(); //количество столбцов int h = selectFromMarkerToMarker(size, ",", 1).toInt(); //количество строк if (LCDI2C == nullptr) { //инициализации экрана еще не было - LCDI2C = new LiquidCrystal_I2C(hexStringToUint8(addr), w, h); + LCDI2C = new LiquidCrystal_I2C(hexStringToUint8(_addr), w, h); if (LCDI2C != nullptr) { LCDI2C->init(); } @@ -62,6 +63,8 @@ class Lcd2004 : public IoTItem { //LCDI2C->print("Helloy,Manager 404 !"); //Serial.printf("ffff %s\n", _id2show); _prevStrSize = tmpStr.length(); + } else { + scanI2C(); } } diff --git a/src/modules/exec/Mcp23017/Mcp23017.cpp b/src/modules/exec/Mcp23017/Mcp23017.cpp index 657a3fde..b5fc7f06 100644 --- a/src/modules/exec/Mcp23017/Mcp23017.cpp +++ b/src/modules/exec/Mcp23017/Mcp23017.cpp @@ -3,8 +3,6 @@ #include "classes/IoTGpio.h" #include -void scanI2C(); - class Mcp23017Driver : public IoTGpio { private: Adafruit_MCP23X17 _mcp; @@ -39,14 +37,14 @@ class Mcp23017Driver : public IoTGpio { class Mcp23017 : public IoTItem { private: Mcp23017Driver* _driver; + String _addr; public: Mcp23017(String parameters) : IoTItem(parameters) { _driver = nullptr; - String addr; - jsonRead(parameters, "addr", addr); - if (addr == "") { + jsonRead(parameters, "addr", _addr); + if (_addr == "") { scanI2C(); return; } @@ -58,10 +56,15 @@ class Mcp23017 : public IoTItem { return; } - _driver = new Mcp23017Driver(index, addr); + _driver = new Mcp23017Driver(index, _addr); } - void doByInterval() {} + void doByInterval() { + if (_addr == "") { + scanI2C(); + return; + } + } //возвращает ссылку на экземпляр класса Mcp23017Driver IoTGpio* getGpioDriver() { diff --git a/src/modules/exec/Pcf8574/Pcf8574.cpp b/src/modules/exec/Pcf8574/Pcf8574.cpp index d5403752..962feea5 100644 --- a/src/modules/exec/Pcf8574/Pcf8574.cpp +++ b/src/modules/exec/Pcf8574/Pcf8574.cpp @@ -93,14 +93,14 @@ class Pcf8574Driver : public IoTGpio { class Pcf8574 : public IoTItem { private: Pcf8574Driver* _driver; + String _addr; public: Pcf8574(String parameters) : IoTItem(parameters) { _driver = nullptr; - String addr; - jsonRead(parameters, "addr", addr); - if (addr == "") { + jsonRead(parameters, "addr", _addr); + if (_addr == "") { scanI2C(); return; } @@ -112,10 +112,15 @@ class Pcf8574 : public IoTItem { return; } - _driver = new Pcf8574Driver(index, addr); + _driver = new Pcf8574Driver(index, _addr); } - void doByInterval() {} + void doByInterval() { + if (_addr == "") { + scanI2C(); + return; + } + } //возвращает ссылку на экземпляр класса Pcf8574Driver IoTGpio* getGpioDriver() { From eb1cf428071ad2c04986acd00c207c1d044edf1c Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 11 Oct 2022 21:54:20 +0300 Subject: [PATCH 018/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D0=B8=20esp8266=5F1m=20=D0=B4=D0=BB=D1=8F=20RCswit?= =?UTF-8?q?ch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/RCswitch/modinfo.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/sensors/RCswitch/modinfo.json b/src/modules/sensors/RCswitch/modinfo.json index 2fcc452a..916aa31d 100644 --- a/src/modules/sensors/RCswitch/modinfo.json +++ b/src/modules/sensors/RCswitch/modinfo.json @@ -59,6 +59,9 @@ "esp8266_1mb": [ "rc-switch @ ^2.6.4" ], + "esp8266_1mb_ota": [ + "rc-switch @ ^2.6.4" + ], "esp8266_4mb": [ "rc-switch @ ^2.6.4" ] From 3af9b457dee6567a722c38498aa1283dd85f03a6 Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 11 Oct 2022 21:57:10 +0300 Subject: [PATCH 019/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D0=B8?= =?UTF-8?q?=20=D0=BF=D0=BB=D0=B0=D1=82=20esp01=20=D1=81=20=D0=B8=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20OTA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PrepareProject.py | 29 +++-- data_svelte/settings.json | 3 +- data_svelte_lite/config.json | 1 + data_svelte_lite/dev_conf.txt | 0 data_svelte_lite/edit.htm.gz | Bin 0 -> 5286 bytes data_svelte_lite/index.html | 16 +++ data_svelte_lite/items.json | 28 +++++ data_svelte_lite/scenario.txt | 0 data_svelte_lite/settings.json | 23 ++++ data_svelte_lite/widgets.json | 216 +++++++++++++++++++++++++++++++++ myProfile.json | 3 +- platformio.ini | 54 +++++++++ 12 files changed, 361 insertions(+), 12 deletions(-) create mode 100644 data_svelte_lite/config.json create mode 100644 data_svelte_lite/dev_conf.txt create mode 100644 data_svelte_lite/edit.htm.gz create mode 100644 data_svelte_lite/index.html create mode 100644 data_svelte_lite/items.json create mode 100644 data_svelte_lite/scenario.txt create mode 100644 data_svelte_lite/settings.json create mode 100644 data_svelte_lite/widgets.json diff --git a/PrepareProject.py b/PrepareProject.py index 6f592233..601d50ad 100644 --- a/PrepareProject.py +++ b/PrepareProject.py @@ -98,8 +98,7 @@ else: # устанавливаем параметры сборки profJson['projectProp'] = { 'platformio': { - 'default_envs': 'esp8266_4mb', - 'data_dir': 'data_svelte' + 'default_envs': 'esp8266_4mb' } } # загружаем список модулей для сборки @@ -107,23 +106,35 @@ else: # сохраняем новый профиль with open(profile, "w", encoding='utf-8') as write_file: json.dump(profJson, write_file, ensure_ascii=False, indent=4, sort_keys=False) - + + +# определяем какое устройство используется в профиле +deviceName = profJson['projectProp']['platformio']['default_envs'] + +# назначаем папку с файлами прошивки в зависимости от устройства и запоминаем в профиле +dataDir = 'data_svelte' +if deviceName == 'esp8266_1mb_ota': + dataDir = 'data_svelte_lite' +profJson['projectProp'] = { + 'platformio': { + 'data_dir': dataDir + } +} # генерируем файлы проекта на основе подготовленного профиля # заполняем конфигурационный файл прошивки параметрами из профиля -with open("data_svelte/settings.json", "r", encoding='utf-8') as read_file: +with open(dataDir + "/settings.json", "r", encoding='utf-8') as read_file: iotmJson = json.load(read_file) for key, value in profJson['iotmSettings'].items(): iotmJson[key] = value -with open("data_svelte/settings.json", "w", encoding='utf-8') as write_file: +with open(dataDir + "/settings.json", "w", encoding='utf-8') as write_file: json.dump(iotmJson, write_file, ensure_ascii=False, indent=4, sort_keys=False) -# определяем какое устройство используется в профиле -deviceName = profJson['projectProp']['platformio']['default_envs'] + # собираем меню прошивки из модулей # параллельно формируем список имен активных модулей -# параллельно собираем необходимые активным модулям библиотеки для включения в компиляцию для текущего типа устройства (esp8266_4m, esp32_4mb) +# параллельно собираем необходимые активным модулям библиотеки для включения в компиляцию для текущего типа устройства (esp8266_4m, esp32_4mb, esp8266_1m, esp8266_1m_ota) activeModulesName = [] # список имен активных модулей allLibs = "" # подборка всех библиотек необходимых модулям для дальнейшей записи в конфигурацию platformio itemsCount = 1; @@ -145,7 +156,7 @@ for section, modules in profJson['modules'].items(): configItemsJson['name'] = str(itemsCount) + ". " + configItemsJson['name'] itemsCount = itemsCount + 1 itemsJson.append(configItemsJson) -with open("data_svelte/items.json", "w", encoding='utf-8') as write_file: +with open(dataDir + "/items.json", "w", encoding='utf-8') as write_file: json.dump(itemsJson, write_file, ensure_ascii=False, indent=4, sort_keys=False) diff --git a/data_svelte/settings.json b/data_svelte/settings.json index bfeb5544..f83115c2 100644 --- a/data_svelte/settings.json +++ b/data_svelte/settings.json @@ -18,5 +18,6 @@ "mqttin": 0, "pinSCL": 0, "pinSDA": 0, - "i2cFreq": 100000 + "i2cFreq": 100000, + "settings_": "" } \ No newline at end of file diff --git a/data_svelte_lite/config.json b/data_svelte_lite/config.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/data_svelte_lite/config.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/data_svelte_lite/dev_conf.txt b/data_svelte_lite/dev_conf.txt new file mode 100644 index 00000000..e69de29b diff --git a/data_svelte_lite/edit.htm.gz b/data_svelte_lite/edit.htm.gz new file mode 100644 index 0000000000000000000000000000000000000000..d41d10c1c56f308ad3cac07e98993703426b577d GIT binary patch literal 5286 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 + + + + + + IoT Manager 4.4.1 + + + + + + + + + diff --git a/data_svelte_lite/items.json b/data_svelte_lite/items.json new file mode 100644 index 00000000..88457c56 --- /dev/null +++ b/data_svelte_lite/items.json @@ -0,0 +1,28 @@ +[ + { + "name": "Выберите элемент", + "num": 0 + }, + { + "header": "Виртуальные элементы" + }, + { + "header": "Сенсоры" + }, + { + "name": "1. Сканер кнопок 433 MHz", + "num": 1, + "type": "Reading", + "subtype": "RCswitch", + "id": "rsw", + "int": 500, + "pinRx": 12, + "pinTx": 12 + }, + { + "header": "Исполнительные устройства" + }, + { + "header": "Экраны" + } +] \ No newline at end of file diff --git a/data_svelte_lite/scenario.txt b/data_svelte_lite/scenario.txt new file mode 100644 index 00000000..e69de29b diff --git a/data_svelte_lite/settings.json b/data_svelte_lite/settings.json new file mode 100644 index 00000000..f83115c2 --- /dev/null +++ b/data_svelte_lite/settings.json @@ -0,0 +1,23 @@ +{ + "name": "IoTmanagerVer4", + "apssid": "IoTmanager", + "appass": "", + "routerssid": "rise", + "routerpass": "hostel3333", + "timezone": 2, + "ntp": "pool.ntp.org", + "weblogin": "admin", + "webpass": "admin", + "mqttServer": "m2.wqtt.ru", + "mqttPort": 8021, + "mqttPrefix": "/risenew", + "mqttUser": "rise", + "mqttPass": "3hostel3", + "serverip": "http://iotmanager.org", + "log": 0, + "mqttin": 0, + "pinSCL": 0, + "pinSDA": 0, + "i2cFreq": 100000, + "settings_": "" +} \ No newline at end of file diff --git a/data_svelte_lite/widgets.json b/data_svelte_lite/widgets.json new file mode 100644 index 00000000..cab0dbe2 --- /dev/null +++ b/data_svelte_lite/widgets.json @@ -0,0 +1,216 @@ +[ + { + "name": "anydataRed", + "label": "Сообщение1", + "widget": "anydata", + "icon": "body", + "color": "red", + "descrColor": "red" + }, + { + "name": "anydataDgr", + "label": "Сообщение2", + "widget": "anydata", + "after": "", + "color": "red", + "icon": "walk" + }, + { + "name": "anydataDef", + "label": "Текст", + "widget": "anydata", + "after": "", + "icon": "" + }, + { + "name": "anydataVlt", + "label": "Вольты", + "widget": "anydata", + "after": "V", + "icon": "speedometer" + }, + { + "name": "anydataAmp", + "label": "Амперы", + "widget": "anydata", + "after": "A", + "icon": "speedometer" + }, + { + "name": "anydataWt", + "label": "Ватты", + "widget": "anydata", + "after": "Wt", + "icon": "speedometer" + }, + { + "name": "anydataWth", + "label": "Энергия", + "widget": "anydata", + "after": "kWt/Hr", + "icon": "speedometer" + }, + { + "name": "anydataHtz", + "label": "Герцы", + "widget": "anydata", + "after": "Hz", + "icon": "speedometer" + }, + { + "name": "anydataTmp", + "label": "Температура", + "widget": "anydata", + "after": "°С", + "icon": "thermometer" + }, + { + "name": "anydataMm", + "label": "Давление", + "widget": "anydata", + "after": "mm", + "icon": "speedometer" + }, + { + "name": "anydataHum", + "label": "Влажность", + "widget": "anydata", + "after": "%", + "icon": "water", + "color": "#88AADF" + }, + { + "name": "anydataTm", + "label": "Время", + "widget": "anydata", + "after": "", + "icon": "speedometer" + }, + { + "name": "button", + "label": "Кнопка", + "widget": "btn", + "size": "large", + "color": "green", + "send": "test" + }, + { + "name": "toggle", + "label": "Переключатель", + "widget": "toggle", + "icon": "", + "iconOff": "" + }, + { + "name": "chart1", + "label": "График без точек", + "widget": "chart", + "dateFormat": "HH:mm", + "maxCount": 86400, + "pointRadius": 0 + }, + { + "name": "chart2", + "label": "График с точками", + "widget": "chart", + "maxCount": 86400, + "dateFormat": "HH:mm" + }, + { + "name": "chart3", + "label": "График Дневной", + "widget": "chart", + "dateFormat": "DD.MM.YYYY", + "maxCount": 86400, + "type": "bar" + }, + { + "name": "fillgauge", + "label": "Бочка", + "widget": "fillgauge", + "circleColor": "#00FFFF", + "textColor": "#FFFFFF", + "waveTextColor": "#000000", + "waveColor": "#00FFFF" + }, + { + "name": "inputDate", + "label": "Ввод даты", + "widget": "input", + "size": "small", + "color": "orange", + "type": "date" + }, + { + "name": "inputDgt", + "label": "Ввод числа", + "widget": "input", + "color": "blue", + "type": "number" + }, + { + "name": "inputTxt", + "label": "Ввод текста", + "widget": "input", + "size": "small", + "color": "orange", + "type": "text" + }, + { + "name": "inputTm", + "label": "Ввод времени", + "widget": "input", + "color": "blue", + "type": "time" + }, + { + "name": "progressLine", + "label": "Статус линия", + "widget": "progress-line", + "icon": "sunny", + "max": "100", + "stroke": "10" + }, + { + "name": "progressRound", + "label": "Статус круг", + "widget": "progress-round", + "max": "100", + "stroke": "20", + "color": "#45ccce", + "background": "#777", + "semicircle": "1" + }, + { + "name": "range", + "label": "Ползунок", + "widget": "range", + "descrColor": "red", + "after": "%", + "k": 0.0977, + "min": 0, + "max": 100, + "debounce": 500 + }, + { + "name": "select", + "label": "Выпадающий", + "widget": "select", + "options": [ + "Выключен", + "Включен" + ], + "status": 0 + }, + { + "name": "anydataPpm", + "label": "PPM", + "widget": "anydata", + "after": "ppm", + "icon": "speedometer" + }, + { + "name": "nil", + "label": "Без виджета" + } +] \ No newline at end of file diff --git a/myProfile.json b/myProfile.json index 18e1f5bc..2ed11eec 100644 --- a/myProfile.json +++ b/myProfile.json @@ -24,8 +24,7 @@ }, "projectProp": { "platformio": { - "default_envs": "esp8266_4mb", - "data_dir": "data_svelte" + "default_envs": "esp8266_4mb" } }, "modules": { diff --git a/platformio.ini b/platformio.ini index 971984a1..476daacb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,3 +1,45 @@ +[env:esp8266_1mb_ota] +lib_deps = + ${common_env_data.lib_deps_external} + ${env:esp8266_1mb_ota_fromitems.lib_deps} + ESPAsyncUDP +build_flags = -Desp8266_4mb="esp8266_4mb" +framework = arduino +board = nodemcuv2 +board_build.ldscript = eagle.flash.1m64.ld +platform = espressif8266 @4.0.1 +monitor_filters = esp8266_exception_decoder +upload_speed = 921600 +monitor_speed = 115200 +board_build.filesystem = littlefs +build_src_filter = + +<*.cpp> + + + + + + + ${env:esp8266_1mb_ota_fromitems.build_src_filter} + +[env:esp8266_1mb] +lib_deps = + ${common_env_data.lib_deps_external} + ${env:esp8266_1mb_fromitems.lib_deps} + ESPAsyncUDP +build_flags = -Desp8266_4mb="esp8266_4mb" +framework = arduino +board = nodemcuv2 +board_build.ldscript = eagle.flash.1m256.ld +platform = espressif8266 @4.0.1 +monitor_filters = esp8266_exception_decoder +upload_speed = 921600 +monitor_speed = 115200 +board_build.filesystem = littlefs +build_src_filter = + +<*.cpp> + + + + + + + ${env:esp8266_1mb_fromitems.build_src_filter} + [env:esp8266_4mb] lib_deps = ${common_env_data.lib_deps_external} @@ -50,6 +92,18 @@ lib_deps_external = bblanchon/ArduinoJson @6.18.0 knolleary/PubSubClient +[env:esp8266_1mb_ota_fromitems] +lib_deps = + rc-switch @ ^2.6.4 +build_src_filter = + + + +[env:esp8266_1mb_fromitems] +lib_deps = + rc-switch @ ^2.6.4 +build_src_filter = + + + [env:esp8266_4mb_fromitems] lib_deps = https://github.com/enjoyneering/AHTxx.git From b61cfca3ed4bc59c588d5079fe411ef33a0b9ccc Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Tue, 11 Oct 2022 22:33:42 +0200 Subject: [PATCH 020/107] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B3=D0=B0=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=BE=D0=B2=20=D1=81=20?= =?UTF-8?q?=D0=B4=D0=B0=D1=82=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + data_svelte/items.json | 169 +++++++++++++++++++++++++------------- data_svelte/settings.json | 3 +- include/WsServer.h | 2 +- myProfile.json | 4 +- platformio.ini | 69 ++++++++-------- src/EventsAndOrders.cpp | 4 - src/WsServer.cpp | 10 ++- src/modules/API.cpp | 6 +- 9 files changed, 165 insertions(+), 104 deletions(-) diff --git a/.gitignore b/.gitignore index 65bd8853..41f063ef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch +/myProfile_wm.json +/myProfile.json data_svelte/settings.json diff --git a/data_svelte/items.json b/data_svelte/items.json index 01f1ca76..a0144f04 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -276,7 +276,58 @@ "num": 20 }, { - "name": "21. GY21 Температура", + "name": "21. Частотомер на ADC, Частота", + "type": "Reading", + "subtype": "FreqMeterF", + "id": "freq", + "widget": "anydataHtz", + "page": "Частотомер", + "descr": "Частота", + "plus": 0, + "multiply": 1, + "round": 1, + "pin-Esp32": 33, + "samples": 512, + "samplingFrequency": 5000, + "int": 5, + "num": 21 + }, + { + "name": "22. Частотомер на ADC, Процент Пульсации", + "type": "Reading", + "subtype": "FreqMeterPcFl", + "id": "pctFlicker", + "widget": "anydataPct", + "page": "Частотомер", + "descr": "Процент Пульсации", + "plus": 0, + "multiply": 1, + "round": 1, + "pin-Esp32": 33, + "samples": 512, + "samplingFrequency": 5000, + "int": 5, + "num": 22 + }, + { + "name": "23. Частотомер на ADC, Индекс Пульсации", + "type": "Reading", + "subtype": "FreqMeterFlIn", + "id": "flickerIndex", + "widget": "anydataDef", + "page": "Частотомер", + "descr": "Индекс Пульсации", + "plus": 0, + "multiply": 1, + "round": 10, + "pin-Esp32": 33, + "samples": 512, + "samplingFrequency": 5000, + "int": 5, + "num": 23 + }, + { + "name": "24. GY21 Температура", "type": "Reading", "subtype": "GY21t", "id": "tmp4", @@ -285,10 +336,10 @@ "descr": "Температура", "round": 1, "int": 15, - "num": 21 + "num": 24 }, { - "name": "22. GY21 Влажность", + "name": "25. GY21 Влажность", "type": "Reading", "subtype": "GY21h", "id": "Hum4", @@ -297,10 +348,10 @@ "descr": "Влажность", "round": 1, "int": 15, - "num": 22 + "num": 25 }, { - "name": "23. HDC1080 Температура", + "name": "26. HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", "id": "Temp1080", @@ -310,10 +361,10 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 23 + "num": 26 }, { - "name": "24. HDC1080 Влажность", + "name": "27. HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", "id": "Hum1080", @@ -323,10 +374,10 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 24 + "num": 27 }, { - "name": "25. MAX6675 Температура", + "name": "28. MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", "id": "maxtmp", @@ -337,10 +388,10 @@ "DO": 12, "CS": 13, "CLK": 14, - "num": 25 + "num": 28 }, { - "name": "26. PZEM 004t Напряжение", + "name": "29. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", "id": "v", @@ -350,10 +401,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 26 + "num": 29 }, { - "name": "27. PZEM 004t Сила тока", + "name": "30. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", "id": "a", @@ -363,10 +414,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 27 + "num": 30 }, { - "name": "28. PZEM 004t Мощность", + "name": "31. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", "id": "w", @@ -376,10 +427,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 28 + "num": 31 }, { - "name": "29. PZEM 004t Энергия", + "name": "32. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", "id": "wh", @@ -389,10 +440,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 29 + "num": 32 }, { - "name": "30. PZEM 004t Частота", + "name": "33. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", "id": "hz", @@ -402,10 +453,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 30 + "num": 33 }, { - "name": "31. PZEM 004t Косинус", + "name": "34. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", "id": "pf", @@ -415,11 +466,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 31 + "num": 34 }, { - "name": "32. Сканер кнопок 433 MHz", - "num": 32, + "name": "35. Сканер кнопок 433 MHz", + "num": 35, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -428,7 +479,7 @@ "pinTx": 12 }, { - "name": "33. Sht20 Температура", + "name": "36. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", "id": "tmp2", @@ -437,10 +488,10 @@ "descr": "Температура", "int": 15, "round": 1, - "num": 33 + "num": 36 }, { - "name": "34. Sht20 Влажность", + "name": "37. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", "id": "Hum2", @@ -449,10 +500,10 @@ "descr": "Влажность", "int": 15, "round": 1, - "num": 34 + "num": 37 }, { - "name": "35. Sht30 Температура", + "name": "38. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", "id": "tmp30", @@ -461,10 +512,10 @@ "descr": "SHT30 Температура", "int": 15, "round": 1, - "num": 35 + "num": 38 }, { - "name": "36. Sht30 Влажность", + "name": "39. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", "id": "Hum30", @@ -473,11 +524,11 @@ "descr": "SHT30 Влажность", "int": 15, "round": 1, - "num": 36 + "num": 39 }, { - "name": "37. HC-SR04 Ультразвуковой дальномер", - "num": 37, + "name": "40. HC-SR04 Ультразвуковой дальномер", + "num": 40, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -489,7 +540,7 @@ "int": 5 }, { - "name": "38. UART", + "name": "41. UART", "type": "Reading", "subtype": "UART", "page": "", @@ -499,13 +550,13 @@ "tx": 12, "rx": 13, "speed": 9600, - "num": 38 + "num": 41 }, { "header": "Исполнительные устройства" }, { - "name": "39. Кнопка подключенная к пину", + "name": "42. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -518,10 +569,10 @@ "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 39 + "num": 42 }, { - "name": "40. Управление пином", + "name": "43. Управление пином", "type": "Writing", "subtype": "ButtonOut", "id": "btn", @@ -531,10 +582,10 @@ "int": 0, "inv": 0, "pin": 2, - "num": 40 + "num": 43 }, { - "name": "41. Сервопривод", + "name": "44. Сервопривод", "type": "Writing", "subtype": "IoTServo", "id": "servo", @@ -545,10 +596,10 @@ "pin": 12, "apin": -1, "amap": "0, 4096, 0, 180", - "num": 41 + "num": 44 }, { - "name": "42. Расширитель портов Mcp23017", + "name": "45. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -558,10 +609,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 42 + "num": 45 }, { - "name": "43. MP3 плеер", + "name": "46. MP3 плеер", "type": "Reading", "subtype": "Mp3", "id": "mp3", @@ -571,10 +622,10 @@ "int": 1, "pins": "14,12", "volume": 20, - "num": 43 + "num": 46 }, { - "name": "44. Расширитель портов Pcf8574", + "name": "47. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -584,25 +635,27 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 44 + "num": 47 }, { - "name": "45. PWM ESP8266", + "name": "48. PWM ESP32", "type": "Writing", - "subtype": "Pwm8266", + "subtype": "Pwm32", "id": "pwm", "widget": "range", "page": "Кнопки", "descr": "PWM", "int": 0, - "pin": 15, + "pin": 2, "freq": 5000, + "ledChannel": 2, + "PWM_resolution": 10, "val": 0, "apin": -1, - "num": 45 + "num": 48 }, { - "name": "46. Телеграм-Лайт", + "name": "49. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -611,13 +664,13 @@ "descr": "", "token": "", "chatID": "", - "num": 46 + "num": 49 }, { "header": "Экраны" }, { - "name": "47. LCD экран 2004", + "name": "50. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -629,10 +682,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 47 + "num": 50 }, { - "name": "48. LCD экран 1602", + "name": "51. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -644,6 +697,6 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 48 + "num": 51 } ] \ No newline at end of file diff --git a/data_svelte/settings.json b/data_svelte/settings.json index bfeb5544..f83115c2 100644 --- a/data_svelte/settings.json +++ b/data_svelte/settings.json @@ -18,5 +18,6 @@ "mqttin": 0, "pinSCL": 0, "pinSDA": 0, - "i2cFreq": 100000 + "i2cFreq": 100000, + "settings_": "" } \ No newline at end of file diff --git a/include/WsServer.h b/include/WsServer.h index ff08d671..89955c13 100644 --- a/include/WsServer.h +++ b/include/WsServer.h @@ -17,5 +17,5 @@ void publishStatusWs(const String& topic, const String& data); void publishChartWs(int num, String& path); void periodicWsSend(); -void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize); +void sendFileToWsByFrames(const String& filename, const String& header, const String& json, int client_id, size_t frameSize); void sendStringToWs(const String& header, String& payload, int client_id); \ No newline at end of file diff --git a/myProfile.json b/myProfile.json index 18e1f5bc..6c54469d 100644 --- a/myProfile.json +++ b/myProfile.json @@ -24,7 +24,7 @@ }, "projectProp": { "platformio": { - "default_envs": "esp8266_4mb", + "default_envs": "esp32_4mb", "data_dir": "data_svelte" } }, @@ -90,7 +90,7 @@ }, { "path": "src/modules/sensors/FreqMeter", - "active": false + "active": true }, { "path": "src/modules/sensors/GY21", diff --git a/platformio.ini b/platformio.ini index 971984a1..cbe86766 100644 --- a/platformio.ini +++ b/platformio.ini @@ -41,7 +41,7 @@ build_src_filter = ${env:esp32_4mb_fromitems.build_src_filter} [platformio] -default_envs = esp8266_4mb +default_envs = esp32_4mb data_dir = data_svelte [common_env_data] @@ -57,6 +57,7 @@ lib_deps = adafruit/Adafruit BMP280 Library beegee-tokyo/DHT sensor library for ESPx milesburton/DallasTemperature@^3.9.1 + kosme/arduinoFFT@^1.5.6 https://github.com/JonasGMorsch/GY-21.git ClosedCube HDC1080 adafruit/MAX6675 library @@ -82,6 +83,7 @@ build_src_filter = + + + + + + + + @@ -103,15 +105,15 @@ build_src_filter = [env:esp32_4mb_fromitems] lib_deps = - Adafruit AHTX0 + https://github.com/enjoyneering/AHTxx.git adafruit/Adafruit BME280 Library adafruit/Adafruit BMP280 Library beegee-tokyo/DHT sensor library for ESPx milesburton/DallasTemperature@^3.9.1 + kosme/arduinoFFT@^1.5.6 https://github.com/JonasGMorsch/GY-21.git ClosedCube HDC1080 adafruit/MAX6675 library - mandulaj/PZEM-004T-v30 rc-switch @ ^2.6.4 robtillaart/SHT2x@^0.1.1 WEMOS SHT3x@1.0.0 @@ -120,35 +122,38 @@ lib_deps = adafruit/Adafruit MCP23017 Arduino Library@^2.1.0 adafruit/Adafruit BusIO @ ^1.13.2 dfrobot/DFRobotDFPlayerMini @ ^1.0.5 + adafruit/Adafruit BusIO @ ^1.13.2 marcoschwartz/LiquidCrystal_I2C@^1.1.4 build_src_filter = - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/EventsAndOrders.cpp b/src/EventsAndOrders.cpp index bb6db4a9..2af00a66 100644 --- a/src/EventsAndOrders.cpp +++ b/src/EventsAndOrders.cpp @@ -27,10 +27,6 @@ void handleOrder() { String id = selectToMarker(order, " "); - //это модификатор для даты графика - // if (id.endsWith("-date")) { - //} - //здесь нужно перебрать все методы execute всех векторов и выполнить те id которых совпали с id события IoTItem* item = findIoTItem(id); if (item) { diff --git a/src/WsServer.cpp b/src/WsServer.cpp index b6f03380..5ec5a937 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -292,7 +292,7 @@ void hexdump(const void* mem, uint32_t len, uint8_t cols = 16) { #endif #endif -void sendFileToWsByFrames(const String& filename, const String& header, const String& json, uint8_t client_id, size_t frameSize) { +void sendFileToWsByFrames(const String& filename, const String& header, const String& json, int client_id, size_t frameSize) { if (header.length() != 6) { SerialPrint("E", "FS", F("wrong header size")); return; @@ -305,8 +305,8 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St return; } - // size_t totalSize = file.size(); - // Serial.println("Send file '" + String(filename) + "', file size: " + String(totalSize)); + size_t totalSize = file.size(); + Serial.println("Send file '" + String(filename) + "', file size: " + String(totalSize)); char buf[32]; sprintf(buf, "%04d", json.length() + 12); @@ -346,9 +346,11 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St continuation = true; } - // Serial.println(String(i) + ") fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); + Serial.println(String(i) + ") " + "ws: " + String(client_id) + " fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); + if (client_id == -1) { standWebSocket.broadcastBIN(frameBuf, size, fin, continuation); + } else { standWebSocket.sendBIN(client_id, frameBuf, size, fin, continuation); } diff --git a/src/modules/API.cpp b/src/modules/API.cpp index ab864765..f23adb0d 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -12,6 +12,7 @@ void* getAPI_Bme280(String subtype, String params); void* getAPI_Bmp280(String subtype, String params); void* getAPI_Dht1122(String subtype, String params); void* getAPI_Ds18b20(String subtype, String params); +void* getAPI_FreqMeter(String subtype, String params); void* getAPI_GY21(String subtype, String params); void* getAPI_Hdc1080(String subtype, String params); void* getAPI_Max6675(String subtype, String params); @@ -27,7 +28,7 @@ void* getAPI_IoTServo(String subtype, String params); void* getAPI_Mcp23017(String subtype, String params); void* getAPI_Mp3(String subtype, String params); void* getAPI_Pcf8574(String subtype, String params); -void* getAPI_Pwm8266(String subtype, String params); +void* getAPI_Pwm32(String subtype, String params); void* getAPI_TelegramLT(String subtype, String params); void* getAPI_Lcd2004(String subtype, String params); @@ -45,6 +46,7 @@ if ((tmpAPI = getAPI_Bme280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Bmp280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Dht1122(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Ds18b20(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_FreqMeter(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_GY21(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Hdc1080(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Max6675(subtype, params)) != nullptr) return tmpAPI; @@ -60,7 +62,7 @@ if ((tmpAPI = getAPI_IoTServo(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mcp23017(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mp3(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pcf8574(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Pwm8266(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Pwm32(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_TelegramLT(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Lcd2004(subtype, params)) != nullptr) return tmpAPI; return nullptr; From 2c7adf468b248c1e87bb1240f5b267914aa4f1d5 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Tue, 11 Oct 2022 23:37:58 +0200 Subject: [PATCH 021/107] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B3=D0=B0=20?= =?UTF-8?q?=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/WsServer.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 5ec5a937..b0833d0d 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -134,23 +134,23 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) if (headerStr == "/sgnittes|") { writeUint8tToString(payload, length, headerLenth, settingsFlashJson); writeFileUint8tByFrames("settings.json", payload, length, headerLenth, 256); - standWebSocket.sendTXT(num, errorsHeapJson); + sendStringToWs("errors", errorsHeapJson, num); addThisDeviceToList(); } //обработка кнопки сохранить настройки mqtt if (headerStr == "/mqtt|") { - standWebSocket.sendTXT(num, settingsFlashJson); //отправляем в ответ новые полученные настройки - handleMqttStatus(false, 8); //меняем статус на неопределенный - mqttReconnect(); //начинаем переподключение - standWebSocket.sendTXT(num, errorsHeapJson); //отправляем что статус неопределен - standWebSocket.sendTXT(num, ssidListHeapJson); + sendStringToWs("settin", settingsFlashJson, num); //отправляем в ответ новые полученные настройки + handleMqttStatus(false, 8); //меняем статус на неопределенный + mqttReconnect(); //начинаем переподключение + sendStringToWs("errors", errorsHeapJson, num); //отправляем что статус неопределен + sendStringToWs("ssidli", ssidListHeapJson, num); } //запуск асинхронного сканирования wifi сетей при нажатии выпадающего списка if (headerStr == "/scan|") { RouterFind(jsonReadStr(settingsFlashJson, F("routerssid"))); - standWebSocket.sendTXT(num, ssidListHeapJson); + sendStringToWs("ssidli", ssidListHeapJson, num); } //----------------------------------------------------------------------// From 6912d5f68d5584be3d12a9897091d86eb872d37c Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Wed, 12 Oct 2022 03:14:55 +0200 Subject: [PATCH 022/107] =?UTF-8?q?=D1=83=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B0=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 163 +++++++++++++------------------------- data_svelte/settings.json | 1 + include/Const.h | 2 + include/Global.h | 2 + include/PeriodicTasks.h | 3 - myProfile.json | 5 +- platformio.ini | 4 - src/DeviceList.cpp | 9 ++- src/Global.cpp | 2 + src/Main.cpp | 78 +++++++++--------- src/MqttClient.cpp | 2 +- src/NTP.cpp | 2 + src/PeriodicTasks.cpp | 8 -- src/WsServer.cpp | 18 +++-- src/modules/API.cpp | 2 - src/utils/JsonUtils.cpp | 9 ++- 16 files changed, 126 insertions(+), 184 deletions(-) diff --git a/data_svelte/items.json b/data_svelte/items.json index a0144f04..d0759e6b 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -276,58 +276,7 @@ "num": 20 }, { - "name": "21. Частотомер на ADC, Частота", - "type": "Reading", - "subtype": "FreqMeterF", - "id": "freq", - "widget": "anydataHtz", - "page": "Частотомер", - "descr": "Частота", - "plus": 0, - "multiply": 1, - "round": 1, - "pin-Esp32": 33, - "samples": 512, - "samplingFrequency": 5000, - "int": 5, - "num": 21 - }, - { - "name": "22. Частотомер на ADC, Процент Пульсации", - "type": "Reading", - "subtype": "FreqMeterPcFl", - "id": "pctFlicker", - "widget": "anydataPct", - "page": "Частотомер", - "descr": "Процент Пульсации", - "plus": 0, - "multiply": 1, - "round": 1, - "pin-Esp32": 33, - "samples": 512, - "samplingFrequency": 5000, - "int": 5, - "num": 22 - }, - { - "name": "23. Частотомер на ADC, Индекс Пульсации", - "type": "Reading", - "subtype": "FreqMeterFlIn", - "id": "flickerIndex", - "widget": "anydataDef", - "page": "Частотомер", - "descr": "Индекс Пульсации", - "plus": 0, - "multiply": 1, - "round": 10, - "pin-Esp32": 33, - "samples": 512, - "samplingFrequency": 5000, - "int": 5, - "num": 23 - }, - { - "name": "24. GY21 Температура", + "name": "21. GY21 Температура", "type": "Reading", "subtype": "GY21t", "id": "tmp4", @@ -336,10 +285,10 @@ "descr": "Температура", "round": 1, "int": 15, - "num": 24 + "num": 21 }, { - "name": "25. GY21 Влажность", + "name": "22. GY21 Влажность", "type": "Reading", "subtype": "GY21h", "id": "Hum4", @@ -348,10 +297,10 @@ "descr": "Влажность", "round": 1, "int": 15, - "num": 25 + "num": 22 }, { - "name": "26. HDC1080 Температура", + "name": "23. HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", "id": "Temp1080", @@ -361,10 +310,10 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 26 + "num": 23 }, { - "name": "27. HDC1080 Влажность", + "name": "24. HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", "id": "Hum1080", @@ -374,10 +323,10 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 27 + "num": 24 }, { - "name": "28. MAX6675 Температура", + "name": "25. MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", "id": "maxtmp", @@ -388,10 +337,10 @@ "DO": 12, "CS": 13, "CLK": 14, - "num": 28 + "num": 25 }, { - "name": "29. PZEM 004t Напряжение", + "name": "26. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", "id": "v", @@ -401,10 +350,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 29 + "num": 26 }, { - "name": "30. PZEM 004t Сила тока", + "name": "27. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", "id": "a", @@ -414,10 +363,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 30 + "num": 27 }, { - "name": "31. PZEM 004t Мощность", + "name": "28. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", "id": "w", @@ -427,10 +376,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 31 + "num": 28 }, { - "name": "32. PZEM 004t Энергия", + "name": "29. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", "id": "wh", @@ -440,10 +389,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 32 + "num": 29 }, { - "name": "33. PZEM 004t Частота", + "name": "30. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", "id": "hz", @@ -453,10 +402,10 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 33 + "num": 30 }, { - "name": "34. PZEM 004t Косинус", + "name": "31. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", "id": "pf", @@ -466,11 +415,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 34 + "num": 31 }, { - "name": "35. Сканер кнопок 433 MHz", - "num": 35, + "name": "32. Сканер кнопок 433 MHz", + "num": 32, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -479,7 +428,7 @@ "pinTx": 12 }, { - "name": "36. Sht20 Температура", + "name": "33. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", "id": "tmp2", @@ -488,10 +437,10 @@ "descr": "Температура", "int": 15, "round": 1, - "num": 36 + "num": 33 }, { - "name": "37. Sht20 Влажность", + "name": "34. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", "id": "Hum2", @@ -500,10 +449,10 @@ "descr": "Влажность", "int": 15, "round": 1, - "num": 37 + "num": 34 }, { - "name": "38. Sht30 Температура", + "name": "35. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", "id": "tmp30", @@ -512,10 +461,10 @@ "descr": "SHT30 Температура", "int": 15, "round": 1, - "num": 38 + "num": 35 }, { - "name": "39. Sht30 Влажность", + "name": "36. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", "id": "Hum30", @@ -524,11 +473,11 @@ "descr": "SHT30 Влажность", "int": 15, "round": 1, - "num": 39 + "num": 36 }, { - "name": "40. HC-SR04 Ультразвуковой дальномер", - "num": 40, + "name": "37. HC-SR04 Ультразвуковой дальномер", + "num": 37, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -540,7 +489,7 @@ "int": 5 }, { - "name": "41. UART", + "name": "38. UART", "type": "Reading", "subtype": "UART", "page": "", @@ -550,13 +499,13 @@ "tx": 12, "rx": 13, "speed": 9600, - "num": 41 + "num": 38 }, { "header": "Исполнительные устройства" }, { - "name": "42. Кнопка подключенная к пину", + "name": "39. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -569,10 +518,10 @@ "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 42 + "num": 39 }, { - "name": "43. Управление пином", + "name": "40. Управление пином", "type": "Writing", "subtype": "ButtonOut", "id": "btn", @@ -582,10 +531,10 @@ "int": 0, "inv": 0, "pin": 2, - "num": 43 + "num": 40 }, { - "name": "44. Сервопривод", + "name": "41. Сервопривод", "type": "Writing", "subtype": "IoTServo", "id": "servo", @@ -596,10 +545,10 @@ "pin": 12, "apin": -1, "amap": "0, 4096, 0, 180", - "num": 44 + "num": 41 }, { - "name": "45. Расширитель портов Mcp23017", + "name": "42. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -609,10 +558,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 45 + "num": 42 }, { - "name": "46. MP3 плеер", + "name": "43. MP3 плеер", "type": "Reading", "subtype": "Mp3", "id": "mp3", @@ -622,10 +571,10 @@ "int": 1, "pins": "14,12", "volume": 20, - "num": 46 + "num": 43 }, { - "name": "47. Расширитель портов Pcf8574", + "name": "44. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -635,10 +584,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 47 + "num": 44 }, { - "name": "48. PWM ESP32", + "name": "45. PWM ESP32", "type": "Writing", "subtype": "Pwm32", "id": "pwm", @@ -652,10 +601,10 @@ "PWM_resolution": 10, "val": 0, "apin": -1, - "num": 48 + "num": 45 }, { - "name": "49. Телеграм-Лайт", + "name": "46. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -664,13 +613,13 @@ "descr": "", "token": "", "chatID": "", - "num": 49 + "num": 46 }, { "header": "Экраны" }, { - "name": "50. LCD экран 2004", + "name": "47. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -682,10 +631,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 50 + "num": 47 }, { - "name": "51. LCD экран 1602", + "name": "48. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -697,6 +646,6 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 51 + "num": 48 } ] \ No newline at end of file diff --git a/data_svelte/settings.json b/data_svelte/settings.json index f83115c2..affb351a 100644 --- a/data_svelte/settings.json +++ b/data_svelte/settings.json @@ -19,5 +19,6 @@ "pinSCL": 0, "pinSDA": 0, "i2cFreq": 100000, + "wg": "group1", "settings_": "" } \ No newline at end of file diff --git a/include/Const.h b/include/Const.h index 37cfccba..53535968 100644 --- a/include/Const.h +++ b/include/Const.h @@ -15,6 +15,8 @@ #define JSON_BUFFER_SIZE 2048 #define WEB_SOCKETS_FRAME_SIZE 2048 +//#define LOOP_DEBUG + //выбор сервера //#define ASYNC_WEB_SERVER //#define ASYNC_WEB_SOCKETS diff --git a/include/Global.h b/include/Global.h index 558ace8f..3f036530 100644 --- a/include/Global.h +++ b/include/Global.h @@ -137,6 +137,8 @@ extern Time_t _time_local; extern Time_t _time_utc; extern bool _time_isTrust; +extern unsigned long loopPeriod; + // extern DynamicJsonDocument settingsFlashJsonDoc; // extern DynamicJsonDocument paramsFlashJsonDoc; // extern DynamicJsonDocument paramsHeapJsonDoc; diff --git a/include/PeriodicTasks.h b/include/PeriodicTasks.h index eee9d1d5..c9a27a9e 100644 --- a/include/PeriodicTasks.h +++ b/include/PeriodicTasks.h @@ -10,8 +10,5 @@ extern void periodicTasksInit(); extern void printGlobalVarSize(); -extern void handleError(String errorId, String errorValue); -extern void handleError(String errorId, int errorValue); - extern String ESP_getResetReason(void); extern String ESP32GetResetReason(uint32_t cpu_no); \ No newline at end of file diff --git a/myProfile.json b/myProfile.json index 6c54469d..13008892 100644 --- a/myProfile.json +++ b/myProfile.json @@ -20,7 +20,8 @@ "mqttin": 0, "pinSCL": 0, "pinSDA": 0, - "i2cFreq": 100000 + "i2cFreq": 100000, + "wg": "group1" }, "projectProp": { "platformio": { @@ -90,7 +91,7 @@ }, { "path": "src/modules/sensors/FreqMeter", - "active": true + "active": false }, { "path": "src/modules/sensors/GY21", diff --git a/platformio.ini b/platformio.ini index cbe86766..847689e4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -57,7 +57,6 @@ lib_deps = adafruit/Adafruit BMP280 Library beegee-tokyo/DHT sensor library for ESPx milesburton/DallasTemperature@^3.9.1 - kosme/arduinoFFT@^1.5.6 https://github.com/JonasGMorsch/GY-21.git ClosedCube HDC1080 adafruit/MAX6675 library @@ -83,7 +82,6 @@ build_src_filter = + + + - + + + + @@ -110,7 +108,6 @@ lib_deps = adafruit/Adafruit BMP280 Library beegee-tokyo/DHT sensor library for ESPx milesburton/DallasTemperature@^3.9.1 - kosme/arduinoFFT@^1.5.6 https://github.com/JonasGMorsch/GY-21.git ClosedCube HDC1080 adafruit/MAX6675 library @@ -137,7 +134,6 @@ build_src_filter = + + + - + + + + diff --git a/src/DeviceList.cpp b/src/DeviceList.cpp index d2f97724..9e5d99c2 100644 --- a/src/DeviceList.cpp +++ b/src/DeviceList.cpp @@ -2,7 +2,8 @@ const String getThisDevice() { String thisDevice = "{}"; - jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга нужна для udp валидации может быть рабочей группой в последствии + jsonWriteStr_(thisDevice, F("devicelist_"), ""); //метка для парсинга + jsonWriteStr_(thisDevice, F("wg"), jsonReadStr(settingsFlashJson, F("wg"))); //рабочая группа jsonWriteStr_(thisDevice, F("ip"), jsonReadStr(settingsFlashJson, F("ip"))); jsonWriteStr_(thisDevice, F("id"), jsonReadStr(settingsFlashJson, F("id"))); jsonWriteStr_(thisDevice, F("name"), jsonReadStr(settingsFlashJson, F("name"))); @@ -52,7 +53,7 @@ void asyncUdpInit() { }); } - //будем отправлять каждые 30 секунд презентацию данного устройства + //будем отправлять каждые 60 секунд презентацию данного устройства ts.add( UDP, 60000, [&](void*) { // UDPP if (isNetworkActive()) { @@ -68,7 +69,9 @@ void asyncUdpInit() { } bool udpPacketValidation(String& data) { - if (data.indexOf("devicelist_") != -1) { + // SerialPrint("i", F("UDP"), data); + String workgroup = jsonReadStr(settingsFlashJson, "wg"); + if (workgroup != "" && data.indexOf(workgroup) != -1) { return true; } else { return false; diff --git a/src/Global.cpp b/src/Global.cpp index 5aa28dea..b05c868e 100644 --- a/src/Global.cpp +++ b/src/Global.cpp @@ -68,6 +68,8 @@ String mqttRootDevice = ""; unsigned long unixTime = 0; unsigned long unixTimeShort = 0; +unsigned long loopPeriod; + bool isTimeSynch = false; Time_t _time_local; Time_t _time_utc; diff --git a/src/Main.cpp b/src/Main.cpp index 16448d34..dce96513 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -116,38 +116,22 @@ void setup() { } void loop() { - // if(millis()%2000==0){ - // //watch->settimeUnix(time(&iotTimeNow)); - // Serial.println(watch->gettime("d-m-Y, H:i:s, M")); - // delay(1); - // } - - //обновление задач таскера - ts.update(); - -//отправка json -#ifdef QUEUE_FROM_STR - if (sendJsonFiles) sendJsonFiles->loop(); +#ifdef LOOP_DEBUG + unsigned long st = millis(); #endif + ts.update(); + #ifdef STANDARD_WEB_SERVER - //обработка web сервера 1 HTTP.handleClient(); #endif #ifdef STANDARD_WEB_SOCKETS - //обработка web сокетов standWebSocket.loop(); #endif - //обновление mqtt mqttLoop(); -#ifdef STANDARD_WEB_SERVER - //обработка web сервера 2 - // HTTP.handleClient(); -#endif - // передаем управление каждому элементу конфигурации для выполнения своих функций for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { (*it)->loop(); @@ -159,33 +143,43 @@ void loop() { } handleOrder(); - handleEvent(); -#ifdef STANDARD_WEB_SERVER - //обработка web сервера 3 - // HTTP.handleClient(); +#ifdef LOOP_DEBUG + loopPeriod = millis() - st; + if (loopPeriod > 2) Serial.println(loopPeriod); #endif - - // сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) - // currentMillis = millis(); - // if (currentMillis - prevMillis >= 1000) { - // prevMillis = millis(); - // volStrForSave = ""; - // for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { - // if ((*it)->needSave) { - // (*it)->needSave = false; - // volStrForSave = volStrForSave + (*it)->getID() + "=" + (*it)->getValue() + ";"; - // } - // } - // - // if (volStrForSave != "") { - // Serial.print("volStrForSave: "); - // Serial.println(volStrForSave.c_str()); - // } - //} } +//отправка json +//#ifdef QUEUE_FROM_STR +// if (sendJsonFiles) sendJsonFiles->loop(); +//#endif + +// if(millis()%2000==0){ +// //watch->settimeUnix(time(&iotTimeNow)); +// Serial.println(watch->gettime("d-m-Y, H:i:s, M")); +// delay(1); +// } + +// сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) +// currentMillis = millis(); +// if (currentMillis - prevMillis >= 1000) { +// prevMillis = millis(); +// volStrForSave = ""; +// for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { +// if ((*it)->needSave) { +// (*it)->needSave = false; +// volStrForSave = volStrForSave + (*it)->getID() + "=" + (*it)->getValue() + ";"; +// } +// } +// +// if (volStrForSave != "") { +// Serial.print("volStrForSave: "); +// Serial.println(volStrForSave.c_str()); +// } +//} + // File dir = FileFS.open("/", "r"); // String out; // printDirectory(dir, out); diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index 7fa2c7ef..90691b43 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -278,7 +278,7 @@ void publishWidgets() { DeserializationError error = deserializeJson(doc, file); if (error) { SerialPrint("E", F("MQTT"), error.f_str()); - handleError("jse3", 1); + jsonWriteInt(errorsHeapJson, F("jse3"), 1); //Ошибка чтения json файла с виджетами при отправки в mqtt } JsonArray arr = doc.as(); for (JsonVariant value : arr) { diff --git a/src/NTP.cpp b/src/NTP.cpp index 495ff9d2..e2f83178 100644 --- a/src/NTP.cpp +++ b/src/NTP.cpp @@ -13,9 +13,11 @@ void ntpInit() { if (unixTime < MIN_DATETIME) { isTimeSynch = false; // SerialPrint("E", "NTP", "Time not synched"); + jsonWriteInt(errorsHeapJson, F("tme1"), 1); synchTime(); return; } + jsonWriteInt(errorsHeapJson, F("tme1"), 0); breakEpochToTime(unixTime + jsonReadInt(settingsFlashJson, F("timezone")) * 60 * 60, _time_local); breakEpochToTime(unixTime, _time_utc); isTimeSynch = true; diff --git a/src/PeriodicTasks.cpp b/src/PeriodicTasks.cpp index 9885aafc..e44f5ecc 100644 --- a/src/PeriodicTasks.cpp +++ b/src/PeriodicTasks.cpp @@ -31,14 +31,6 @@ void periodicTasksInit() { SerialPrint("i", "Task", "Periodic tasks init"); } -void handleError(String errorId, String errorValue) { - jsonWriteStr_(errorsHeapJson, errorId, errorValue); -} - -void handleError(String errorId, int errorValue) { - jsonWriteInt_(errorsHeapJson, errorId, errorValue); -} - void printGlobalVarSize() { size_t settingsFlashJsonSize = settingsFlashJson.length(); // SerialPrint(F("i"), F("settingsFlashJson"), String(settingsFlashJsonSize)); diff --git a/src/WsServer.cpp b/src/WsServer.cpp index b0833d0d..50ad076b 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -176,10 +176,14 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) // Страница веб интерфейса dev //----------------------------------------------------------------------// if (headerStr == "/dev|") { - // standWebSocket.sendTXT(num, errorsHeapJson); - // standWebSocket.sendTXT(num, settingsFlashJson); - // sendFileToWs("/config.json", num, 1024); - // sendFileToWs("/items.json", num, 1024); + sendStringToWs("errors", errorsHeapJson, num); + sendStringToWs("settin", settingsFlashJson, num); + sendFileToWsByFrames("/config.json", "config", "", num, WEB_SOCKETS_FRAME_SIZE); + sendFileToWsByFrames("/items.json", "itemsj", "", num, WEB_SOCKETS_FRAME_SIZE); + // sendFileToWsByFrames("/layout.json", "layout", "", num, WEB_SOCKETS_FRAME_SIZE); + } + + if (headerStr == "/test|") { } //----------------------------------------------------------------------// @@ -216,8 +220,6 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) SerialPrint("i", F("=>WS"), "Msg from svelte web, WS No: " + String(num) + ", msg: " + msg); } - if (headerStr == "/test|") { - } } break; case WStype_BIN: { @@ -306,7 +308,7 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St } size_t totalSize = file.size(); - Serial.println("Send file '" + String(filename) + "', file size: " + String(totalSize)); + // Serial.println("Send file '" + String(filename) + "', file size: " + String(totalSize)); char buf[32]; sprintf(buf, "%04d", json.length() + 12); @@ -346,7 +348,7 @@ void sendFileToWsByFrames(const String& filename, const String& header, const St continuation = true; } - Serial.println(String(i) + ") " + "ws: " + String(client_id) + " fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); + // Serial.println(String(i) + ") " + "ws: " + String(client_id) + " fr sz: " + String(size) + " fin: " + String(fin) + " cnt: " + String(continuation)); if (client_id == -1) { standWebSocket.broadcastBIN(frameBuf, size, fin, continuation); diff --git a/src/modules/API.cpp b/src/modules/API.cpp index f23adb0d..2f6e4f3b 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -12,7 +12,6 @@ void* getAPI_Bme280(String subtype, String params); void* getAPI_Bmp280(String subtype, String params); void* getAPI_Dht1122(String subtype, String params); void* getAPI_Ds18b20(String subtype, String params); -void* getAPI_FreqMeter(String subtype, String params); void* getAPI_GY21(String subtype, String params); void* getAPI_Hdc1080(String subtype, String params); void* getAPI_Max6675(String subtype, String params); @@ -46,7 +45,6 @@ if ((tmpAPI = getAPI_Bme280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Bmp280(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Dht1122(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Ds18b20(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_FreqMeter(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_GY21(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Hdc1080(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Max6675(subtype, params)) != nullptr) return tmpAPI; diff --git a/src/utils/JsonUtils.cpp b/src/utils/JsonUtils.cpp index f6da97af..141bd8e6 100644 --- a/src/utils/JsonUtils.cpp +++ b/src/utils/JsonUtils.cpp @@ -318,8 +318,9 @@ String jsonWriteFloat(String& json, String name, float value, bool e) { } void jsonErrorDetected() { - jsonWriteInt(errorsHeapJson, F("jse2"), 1); - int number = jsonReadInt(errorsHeapJson, F("jse2n")); - number++; - jsonWriteInt(errorsHeapJson, F("jse2n"), number); + // пример как отправить ошибку с количеством + // jsonWriteInt(errorsHeapJson, F("jse2"), 1); + // int number = jsonReadInt(errorsHeapJson, F("jse2n")); + // number++; + // jsonWriteInt(errorsHeapJson, F("jse2n"), number); } \ No newline at end of file From 85bf00ac012c55f52cae7835bff691644859b4a9 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Wed, 12 Oct 2022 03:18:15 +0200 Subject: [PATCH 023/107] =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9=20=D0=B2=D0=B5=D0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/build/bundle.css.gz | Bin 5492 -> 5489 bytes data_svelte/build/bundle.js.gz | Bin 47064 -> 47660 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/data_svelte/build/bundle.css.gz b/data_svelte/build/bundle.css.gz index 55b3f3ce81d2d7b4b9b85d58c04a91aca1292a8c..07b838325628393fb15b33014a7fddd82b1c4112 100644 GIT binary patch delta 5140 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 OW*5sSnfnh6jQ<0Orw1tj delta 5150 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 diff --git a/data_svelte/build/bundle.js.gz b/data_svelte/build/bundle.js.gz index fe45ea11fd2ed01aee1ae2914696e70d132662fc..49e8b2f39e4771f9dadf9131a990024d14559937 100644 GIT binary patch delta 45104 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 delta 44537 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 |Mrq$PbynDDzd<>J7!+aZ!QHdd3( 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 z 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 From 0e44ba1a3eb0afd1f77d3dbb599ce46a0f4ded40 Mon Sep 17 00:00:00 2001 From: biver Date: Wed, 12 Oct 2022 15:48:05 +0300 Subject: [PATCH 024/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=BD=D0=BE=D0=B2=D1=83?= =?UTF-8?q?=D1=8E=20=D1=81=D1=85=D0=B5=D0=BC=D1=83=20=D1=81=20=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=BC=D0=B8=20=D1=83=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=B0=D0=BC=D0=B8=20esp01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 474 ++-------------------- data_svelte_lite/index.html | 37 +- data_svelte_lite/items.json | 219 +++++++++- include/Const.h | 14 +- myProfile.json | 2 +- platformio.ini | 42 +- src/UpgradeFirm.cpp | 2 +- src/modules/API.cpp | 34 -- src/modules/display/Lcd2004/modinfo.json | 6 + src/modules/exec/ButtonIn/modinfo.json | 4 +- src/modules/exec/ButtonOut/modinfo.json | 4 +- src/modules/exec/Mcp23017/modinfo.json | 8 + src/modules/exec/Pcf8574/modinfo.json | 6 + src/modules/exec/Pwm8266/modinfo.json | 4 +- src/modules/exec/TelegramLT/modinfo.json | 4 +- src/modules/sensors/Ds18b20/modinfo.json | 6 + src/modules/sensors/Sonar/modinfo.json | 4 +- src/modules/virtual/Timer/modinfo.json | 4 +- src/modules/virtual/VButton/modinfo.json | 4 +- src/modules/virtual/Variable/modinfo.json | 4 +- 20 files changed, 380 insertions(+), 502 deletions(-) diff --git a/data_svelte/items.json b/data_svelte/items.json index 01f1ca76..fd9eb626 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -7,34 +7,7 @@ "header": "Виртуальные элементы" }, { - "name": "1. График", - "type": "Writing", - "subtype": "Loging", - "id": "log", - "widget": "chart2", - "page": "Графики", - "descr": "Температура", - "num": 1, - "int": 5, - "logid": "t", - "points": 300 - }, - { - "name": "2. График дневного расхода", - "type": "Writing", - "subtype": "LogingDaily", - "id": "log", - "widget": "chart3", - "page": "Графики", - "descr": "Температура", - "num": 2, - "int": 1, - "logid": "t", - "points": 365, - "test": 0 - }, - { - "name": "3. Таймер", + "name": "1. Таймер", "type": "Writing", "subtype": "Timer", "id": "timer", @@ -46,10 +19,10 @@ "ticker": 1, "repeat": 1, "needSave": 0, - "num": 3 + "num": 1 }, { - "name": "4. Окно ввода числа (переменная)", + "name": "2. Окно ввода числа (переменная)", "type": "Reading", "subtype": "Variable", "id": "value", @@ -58,10 +31,10 @@ "descr": "Введите число", "int": "0", "val": "0.0", - "num": 4 + "num": 2 }, { - "name": "5. Окно ввода времени", + "name": "3. Окно ввода времени", "type": "Reading", "subtype": "Variable", "id": "time", @@ -70,10 +43,10 @@ "descr": "Введите время", "int": "0", "val": "02:00", - "num": 5 + "num": 3 }, { - "name": "6. Окно ввода даты", + "name": "4. Окно ввода даты", "type": "Reading", "subtype": "Variable", "id": "time", @@ -82,10 +55,10 @@ "descr": "Введите дату", "int": "0", "val": "24.05.2022", - "num": 6 + "num": 4 }, { - "name": "7. Окно ввода текста", + "name": "5. Окно ввода текста", "type": "Reading", "subtype": "Variable", "id": "txt", @@ -94,10 +67,10 @@ "descr": "Введите текст", "int": "0", "val": "текст", - "num": 7 + "num": 5 }, { - "name": "8. Виртуальная кнопка", + "name": "6. Виртуальная кнопка", "type": "Reading", "subtype": "VButton", "id": "vbtn", @@ -106,162 +79,13 @@ "descr": "Кнопка", "int": "0", "val": "0", - "num": 8 + "num": 6 }, { "header": "Сенсоры" }, { - "name": "9. Acs712 Ток", - "type": "Reading", - "subtype": "Acs712", - "id": "amp", - "widget": "anydataAmp", - "page": "Сенсоры", - "descr": "Ток", - "round": 3, - "pin": 39, - "int": 5, - "num": 9 - }, - { - "name": "10. AHTXX Температура", - "type": "Reading", - "subtype": "AhtXXt", - "id": "Temp20", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "AHTXX Температура", - "int": 15, - "addr": "0x38", - "shtType": 1, - "round": 1, - "num": 10 - }, - { - "name": "11. AHTXX Влажность", - "type": "Reading", - "subtype": "AhtXXh", - "id": "Hum20", - "widget": "anydataHum", - "page": "Сенсоры", - "descr": "AHTXX Влажность", - "int": 15, - "addr": "0x38", - "shtType": 1, - "round": 1, - "num": 11 - }, - { - "name": "12. Аналоговый сенсор", - "type": "Reading", - "subtype": "AnalogAdc", - "id": "t", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "Температура", - "map": "1,1024,1,100", - "plus": 0, - "multiply": 1, - "round": 1, - "pin": 0, - "int": 15, - "avgSteps": 1, - "num": 12 - }, - { - "name": "13. BME280 Температура", - "type": "Reading", - "subtype": "Bme280t", - "id": "tmp3", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "Температура", - "int": 15, - "addr": "0x77", - "round": 1, - "num": 13 - }, - { - "name": "14. BME280 Давление", - "type": "Reading", - "subtype": "Bme280p", - "id": "Press3", - "widget": "anydataMm", - "page": "Сенсоры", - "descr": "Давление", - "int": 15, - "addr": "0x77", - "round": 1, - "num": 14 - }, - { - "name": "15. BME280 Влажность", - "type": "Reading", - "subtype": "Bme280h", - "id": "Hum3", - "widget": "anydataHum", - "page": "Сенсоры", - "descr": "Влажность", - "int": 15, - "addr": "0x77", - "round": 1, - "num": 15 - }, - { - "name": "16. BMP280 Температура", - "type": "Reading", - "subtype": "Bmp280t", - "id": "tmp3", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "280 Температура", - "int": 15, - "addr": "0x77", - "round": 1, - "num": 16 - }, - { - "name": "17. BMP280 Давление", - "type": "Reading", - "subtype": "Bmp280p", - "id": "Press3", - "widget": "anydataMm", - "page": "Сенсоры", - "descr": "280 Давление", - "int": 15, - "addr": "0x77", - "round": 1, - "num": 17 - }, - { - "name": "18. DHT11 Температура", - "type": "Reading", - "subtype": "Dht1122t", - "id": "tmp3", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "Температура", - "int": 15, - "pin": 0, - "senstype": "dht11", - "num": 18 - }, - { - "name": "19. DHT11 Влажность", - "type": "Reading", - "subtype": "Dht1122h", - "id": "Hum3", - "widget": "anydataHum", - "page": "Сенсоры", - "descr": "Влажность", - "int": 15, - "pin": 0, - "senstype": "dht11", - "num": 19 - }, - { - "name": "20. DS18B20 Температура", + "name": "7. DS18B20 Температура", "type": "Reading", "subtype": "Ds18b20", "id": "dstmp", @@ -273,153 +97,11 @@ "index": 0, "addr": "", "round": 1, - "num": 20 + "num": 7 }, { - "name": "21. GY21 Температура", - "type": "Reading", - "subtype": "GY21t", - "id": "tmp4", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "Температура", - "round": 1, - "int": 15, - "num": 21 - }, - { - "name": "22. GY21 Влажность", - "type": "Reading", - "subtype": "GY21h", - "id": "Hum4", - "widget": "anydataHum", - "page": "Сенсоры", - "descr": "Влажность", - "round": 1, - "int": 15, - "num": 22 - }, - { - "name": "23. HDC1080 Температура", - "type": "Reading", - "subtype": "Hdc1080t", - "id": "Temp1080", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "1080 Температура", - "int": 15, - "addr": "0x40", - "round": 1, - "num": 23 - }, - { - "name": "24. HDC1080 Влажность", - "type": "Reading", - "subtype": "Hdc1080h", - "id": "Hum1080", - "widget": "anydataHum", - "page": "Сенсоры", - "descr": "1080 Влажность", - "int": 15, - "addr": "0x40", - "round": 1, - "num": 24 - }, - { - "name": "25. MAX6675 Температура", - "type": "Reading", - "subtype": "Max6675t", - "id": "maxtmp", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "MAX Температура", - "int": 15, - "DO": 12, - "CS": 13, - "CLK": 14, - "num": 25 - }, - { - "name": "26. PZEM 004t Напряжение", - "type": "Reading", - "subtype": "Pzem004v", - "id": "v", - "widget": "anydataVlt", - "page": "PZEM", - "descr": "Напряжение", - "int": 15, - "addr": "0xF8", - "round": 1, - "num": 26 - }, - { - "name": "27. PZEM 004t Сила тока", - "type": "Reading", - "subtype": "Pzem004a", - "id": "a", - "widget": "anydataAmp", - "page": "PZEM", - "descr": "Сила тока", - "int": 15, - "addr": "0xF8", - "round": 1, - "num": 27 - }, - { - "name": "28. PZEM 004t Мощность", - "type": "Reading", - "subtype": "Pzem004w", - "id": "w", - "widget": "anydataWt", - "page": "PZEM", - "descr": "Мощность", - "int": 15, - "addr": "0xF8", - "round": 1, - "num": 28 - }, - { - "name": "29. PZEM 004t Энергия", - "type": "Reading", - "subtype": "Pzem004wh", - "id": "wh", - "widget": "anydataWth", - "page": "PZEM", - "descr": "Энергия", - "int": 15, - "addr": "0xF8", - "round": 1, - "num": 29 - }, - { - "name": "30. PZEM 004t Частота", - "type": "Reading", - "subtype": "Pzem004hz", - "id": "hz", - "widget": "anydataHtz", - "page": "PZEM", - "descr": "Частота", - "int": 15, - "addr": "0xF8", - "round": 1, - "num": 30 - }, - { - "name": "31. PZEM 004t Косинус", - "type": "Reading", - "subtype": "Pzem004pf", - "id": "pf", - "widget": "anydata", - "page": "PZEM", - "descr": "Косинус F", - "int": 15, - "addr": "0xF8", - "round": 1, - "num": 31 - }, - { - "name": "32. Сканер кнопок 433 MHz", - "num": 32, + "name": "8. Сканер кнопок 433 MHz", + "num": 8, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -428,56 +110,8 @@ "pinTx": 12 }, { - "name": "33. Sht20 Температура", - "type": "Reading", - "subtype": "Sht20t", - "id": "tmp2", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "Температура", - "int": 15, - "round": 1, - "num": 33 - }, - { - "name": "34. Sht20 Влажность", - "type": "Reading", - "subtype": "Sht20h", - "id": "Hum2", - "widget": "anydataHum", - "page": "Сенсоры", - "descr": "Влажность", - "int": 15, - "round": 1, - "num": 34 - }, - { - "name": "35. Sht30 Температура", - "type": "Reading", - "subtype": "Sht30t", - "id": "tmp30", - "widget": "anydataTmp", - "page": "Сенсоры", - "descr": "SHT30 Температура", - "int": 15, - "round": 1, - "num": 35 - }, - { - "name": "36. Sht30 Влажность", - "type": "Reading", - "subtype": "Sht30h", - "id": "Hum30", - "widget": "anydataHum", - "page": "Сенсоры", - "descr": "SHT30 Влажность", - "int": 15, - "round": 1, - "num": 36 - }, - { - "name": "37. HC-SR04 Ультразвуковой дальномер", - "num": 37, + "name": "9. HC-SR04 Ультразвуковой дальномер", + "num": 9, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -488,24 +122,11 @@ "pinEcho": 4, "int": 5 }, - { - "name": "38. UART", - "type": "Reading", - "subtype": "UART", - "page": "", - "descr": "", - "widget": "nil", - "id": "u", - "tx": 12, - "rx": 13, - "speed": 9600, - "num": 38 - }, { "header": "Исполнительные устройства" }, { - "name": "39. Кнопка подключенная к пину", + "name": "10. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -518,10 +139,10 @@ "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 39 + "num": 10 }, { - "name": "40. Управление пином", + "name": "11. Управление пином", "type": "Writing", "subtype": "ButtonOut", "id": "btn", @@ -531,24 +152,10 @@ "int": 0, "inv": 0, "pin": 2, - "num": 40 + "num": 11 }, { - "name": "41. Сервопривод", - "type": "Writing", - "subtype": "IoTServo", - "id": "servo", - "widget": "range", - "page": "servo", - "descr": "угол", - "int": 1, - "pin": 12, - "apin": -1, - "amap": "0, 4096, 0, 180", - "num": 41 - }, - { - "name": "42. Расширитель портов Mcp23017", + "name": "12. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -558,23 +165,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 42 + "num": 12 }, { - "name": "43. MP3 плеер", - "type": "Reading", - "subtype": "Mp3", - "id": "mp3", - "widget": "", - "page": "", - "descr": "", - "int": 1, - "pins": "14,12", - "volume": 20, - "num": 43 - }, - { - "name": "44. Расширитель портов Pcf8574", + "name": "13. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -584,10 +178,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 44 + "num": 13 }, { - "name": "45. PWM ESP8266", + "name": "14. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -599,10 +193,10 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 45 + "num": 14 }, { - "name": "46. Телеграм-Лайт", + "name": "15. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -611,13 +205,13 @@ "descr": "", "token": "", "chatID": "", - "num": 46 + "num": 15 }, { "header": "Экраны" }, { - "name": "47. LCD экран 2004", + "name": "16. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -629,10 +223,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 47 + "num": 16 }, { - "name": "48. LCD экран 1602", + "name": "17. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -644,6 +238,6 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 48 + "num": 17 } ] \ No newline at end of file diff --git a/data_svelte_lite/index.html b/data_svelte_lite/index.html index 392246f4..38ef3291 100644 --- a/data_svelte_lite/index.html +++ b/data_svelte_lite/index.html @@ -1,3 +1,4 @@ + @@ -5,12 +6,36 @@ IoT Manager 4.4.1 - - - - - + + - + + +
+
+

Настройка WiFi


+
+ +

+
+ +


+ +
+
+ diff --git a/data_svelte_lite/items.json b/data_svelte_lite/items.json index 88457c56..fd9eb626 100644 --- a/data_svelte_lite/items.json +++ b/data_svelte_lite/items.json @@ -6,12 +6,102 @@ { "header": "Виртуальные элементы" }, + { + "name": "1. Таймер", + "type": "Writing", + "subtype": "Timer", + "id": "timer", + "widget": "anydataDef", + "page": "Таймеры", + "descr": "Таймер", + "int": 1, + "countDown": 15, + "ticker": 1, + "repeat": 1, + "needSave": 0, + "num": 1 + }, + { + "name": "2. Окно ввода числа (переменная)", + "type": "Reading", + "subtype": "Variable", + "id": "value", + "widget": "inputDgt", + "page": "Ввод", + "descr": "Введите число", + "int": "0", + "val": "0.0", + "num": 2 + }, + { + "name": "3. Окно ввода времени", + "type": "Reading", + "subtype": "Variable", + "id": "time", + "widget": "inputTm", + "page": "Ввод", + "descr": "Введите время", + "int": "0", + "val": "02:00", + "num": 3 + }, + { + "name": "4. Окно ввода даты", + "type": "Reading", + "subtype": "Variable", + "id": "time", + "widget": "inputDate", + "page": "Ввод", + "descr": "Введите дату", + "int": "0", + "val": "24.05.2022", + "num": 4 + }, + { + "name": "5. Окно ввода текста", + "type": "Reading", + "subtype": "Variable", + "id": "txt", + "widget": "inputTxt", + "page": "Ввод", + "descr": "Введите текст", + "int": "0", + "val": "текст", + "num": 5 + }, + { + "name": "6. Виртуальная кнопка", + "type": "Reading", + "subtype": "VButton", + "id": "vbtn", + "widget": "toggle", + "page": "Кнопки", + "descr": "Кнопка", + "int": "0", + "val": "0", + "num": 6 + }, { "header": "Сенсоры" }, { - "name": "1. Сканер кнопок 433 MHz", - "num": 1, + "name": "7. DS18B20 Температура", + "type": "Reading", + "subtype": "Ds18b20", + "id": "dstmp", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "DS Температура", + "int": 15, + "pin": 2, + "index": 0, + "addr": "", + "round": 1, + "num": 7 + }, + { + "name": "8. Сканер кнопок 433 MHz", + "num": 8, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -19,10 +109,135 @@ "pinRx": 12, "pinTx": 12 }, + { + "name": "9. HC-SR04 Ультразвуковой дальномер", + "num": 9, + "type": "Reading", + "subtype": "Sonar", + "id": "sonar", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "Расстояние (см)", + "pinTrig": 5, + "pinEcho": 4, + "int": 5 + }, { "header": "Исполнительные устройства" }, + { + "name": "10. Кнопка подключенная к пину", + "type": "Writing", + "subtype": "ButtonIn", + "id": "btn", + "widget": "toggle", + "page": "Кнопки", + "descr": "Освещение", + "int": 0, + "pin": 16, + "execLevel": "1", + "pinMode": "INPUT", + "debounceDelay": 50, + "fixState": 0, + "num": 10 + }, + { + "name": "11. Управление пином", + "type": "Writing", + "subtype": "ButtonOut", + "id": "btn", + "widget": "toggle", + "page": "Кнопки", + "descr": "Освещение", + "int": 0, + "inv": 0, + "pin": 2, + "num": 11 + }, + { + "name": "12. Расширитель портов Mcp23017", + "type": "Reading", + "subtype": "Mcp23017", + "id": "Mcp", + "widget": "", + "page": "", + "descr": "", + "int": "0", + "addr": "0x20", + "index": 1, + "num": 12 + }, + { + "name": "13. Расширитель портов Pcf8574", + "type": "Reading", + "subtype": "Pcf8574", + "id": "Pcf", + "widget": "", + "page": "", + "descr": "", + "int": "0", + "addr": "0x20", + "index": 1, + "num": 13 + }, + { + "name": "14. PWM ESP8266", + "type": "Writing", + "subtype": "Pwm8266", + "id": "pwm", + "widget": "range", + "page": "Кнопки", + "descr": "PWM", + "int": 0, + "pin": 15, + "freq": 5000, + "val": 0, + "apin": -1, + "num": 14 + }, + { + "name": "15. Телеграм-Лайт", + "type": "Writing", + "subtype": "TelegramLT", + "id": "tg", + "widget": "", + "page": "", + "descr": "", + "token": "", + "chatID": "", + "num": 15 + }, { "header": "Экраны" + }, + { + "name": "16. LCD экран 2004", + "type": "Reading", + "subtype": "Lcd2004", + "id": "Lcd", + "widget": "", + "page": "", + "descr": "T", + "int": 15, + "addr": "0x27", + "size": "20,4", + "coord": "0,0", + "id2show": "id датчика", + "num": 16 + }, + { + "name": "17. LCD экран 1602", + "type": "Reading", + "subtype": "Lcd2004", + "id": "Lcd", + "widget": "", + "page": "", + "descr": "T", + "int": 15, + "addr": "0x27", + "size": "16,2", + "coord": "0,0", + "id2show": "id датчика", + "num": 17 } ] \ No newline at end of file diff --git a/include/Const.h b/include/Const.h index 37cfccba..94a3a87d 100644 --- a/include/Const.h +++ b/include/Const.h @@ -3,6 +3,14 @@ //Версия прошивки #define FIRMWARE_VERSION 431 +#ifdef esp8266_1mb_ota +#define FIRMWARE_NAME "esp8266_1mb_ota" +#endif + +#ifdef esp8266_1mb +#define FIRMWARE_NAME "esp8266_1mb" +#endif + #ifdef esp8266_4mb #define FIRMWARE_NAME "esp8266_4mb" #endif @@ -30,13 +38,7 @@ #define TELEMETRY_UPDATE_INTERVAL_MIN 60 -#ifdef esp8266_4mb #define USE_LITTLEFS true -#endif - -#ifdef esp32_4mb -#define USE_LITTLEFS true -#endif #define START_DATETIME 1661990400 // 01.09.2022 00:00:00 константа для сокращения unix time diff --git a/myProfile.json b/myProfile.json index 2ed11eec..81a23898 100644 --- a/myProfile.json +++ b/myProfile.json @@ -24,7 +24,7 @@ }, "projectProp": { "platformio": { - "default_envs": "esp8266_4mb" + "default_envs": "esp8266_1mb_ota" } }, "modules": { diff --git a/platformio.ini b/platformio.ini index 476daacb..40bf409e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -3,7 +3,7 @@ lib_deps = ${common_env_data.lib_deps_external} ${env:esp8266_1mb_ota_fromitems.lib_deps} ESPAsyncUDP -build_flags = -Desp8266_4mb="esp8266_4mb" +build_flags = -Desp8266_1mb_ota="esp8266_1mb_ota" framework = arduino board = nodemcuv2 board_build.ldscript = eagle.flash.1m64.ld @@ -24,7 +24,7 @@ lib_deps = ${common_env_data.lib_deps_external} ${env:esp8266_1mb_fromitems.lib_deps} ESPAsyncUDP -build_flags = -Desp8266_4mb="esp8266_4mb" +build_flags = -Desp8266_1mb="esp8266_1mb" framework = arduino board = nodemcuv2 board_build.ldscript = eagle.flash.1m256.ld @@ -83,8 +83,8 @@ build_src_filter = ${env:esp32_4mb_fromitems.build_src_filter} [platformio] -default_envs = esp8266_4mb -data_dir = data_svelte +default_envs = esp8266_1mb_ota +data_dir = data_svelte_lite [common_env_data] upload_port = COM4 @@ -94,15 +94,49 @@ lib_deps_external = [env:esp8266_1mb_ota_fromitems] lib_deps = + milesburton/DallasTemperature@^3.9.1 rc-switch @ ^2.6.4 + adafruit/Adafruit MCP23017 Arduino Library@^2.1.0 + adafruit/Adafruit BusIO @ ^1.13.2 + adafruit/Adafruit BusIO @ ^1.13.2 + marcoschwartz/LiquidCrystal_I2C@^1.1.4 build_src_filter = + + + + + + + + + + + + + + + + + + + + + + + + + [env:esp8266_1mb_fromitems] lib_deps = + milesburton/DallasTemperature@^3.9.1 rc-switch @ ^2.6.4 + adafruit/Adafruit MCP23017 Arduino Library@^2.1.0 + adafruit/Adafruit BusIO @ ^1.13.2 + adafruit/Adafruit BusIO @ ^1.13.2 + marcoschwartz/LiquidCrystal_I2C@^1.1.4 build_src_filter = + + + + + + + + + + + + + + + + + + + + + + + + + [env:esp8266_4mb_fromitems] lib_deps = diff --git a/src/UpgradeFirm.cpp b/src/UpgradeFirm.cpp index 48c55fdf..53c6e15b 100644 --- a/src/UpgradeFirm.cpp +++ b/src/UpgradeFirm.cpp @@ -72,7 +72,7 @@ bool upgradeBuild() { handleUpdateStatus(true, PATH_ERROR); return ret; } -#ifdef esp8266_4mb +#if defined (esp8266_4mb) || defined (esp8266_1mb) || defined (esp8266_1mb_ota) ESPhttpUpdate.rebootOnUpdate(false); t_httpUpdate_return retBuild = ESPhttpUpdate.update(wifiClient, getBinPath("firmware.bin")); #endif diff --git a/src/modules/API.cpp b/src/modules/API.cpp index ab864765..4efeb752 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -1,31 +1,14 @@ #include "ESPConfiguration.h" -void* getAPI_Loging(String subtype, String params); -void* getAPI_LogingDaily(String subtype, String params); void* getAPI_Timer(String subtype, String params); void* getAPI_Variable(String subtype, String params); void* getAPI_VButton(String subtype, String params); -void* getAPI_Acs712(String subtype, String params); -void* getAPI_AhtXX(String subtype, String params); -void* getAPI_AnalogAdc(String subtype, String params); -void* getAPI_Bme280(String subtype, String params); -void* getAPI_Bmp280(String subtype, String params); -void* getAPI_Dht1122(String subtype, String params); void* getAPI_Ds18b20(String subtype, String params); -void* getAPI_GY21(String subtype, String params); -void* getAPI_Hdc1080(String subtype, String params); -void* getAPI_Max6675(String subtype, String params); -void* getAPI_Pzem004(String subtype, String params); void* getAPI_RCswitch(String subtype, String params); -void* getAPI_Sht20(String subtype, String params); -void* getAPI_Sht30(String subtype, String params); void* getAPI_Sonar(String subtype, String params); -void* getAPI_UART(String subtype, String params); void* getAPI_ButtonIn(String subtype, String params); void* getAPI_ButtonOut(String subtype, String params); -void* getAPI_IoTServo(String subtype, String params); void* getAPI_Mcp23017(String subtype, String params); -void* getAPI_Mp3(String subtype, String params); void* getAPI_Pcf8574(String subtype, String params); void* getAPI_Pwm8266(String subtype, String params); void* getAPI_TelegramLT(String subtype, String params); @@ -33,32 +16,15 @@ void* getAPI_Lcd2004(String subtype, String params); void* getAPI(String subtype, String params) { void* tmpAPI; -if ((tmpAPI = getAPI_Loging(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_LogingDaily(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Timer(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Variable(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_VButton(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Acs712(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_AhtXX(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_AnalogAdc(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Bme280(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Bmp280(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Dht1122(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Ds18b20(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_GY21(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Hdc1080(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Max6675(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Pzem004(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_RCswitch(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Sht20(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Sht30(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Sonar(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_UART(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_ButtonIn(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_ButtonOut(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_IoTServo(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mcp23017(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Mp3(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pcf8574(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pwm8266(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_TelegramLT(subtype, params)) != nullptr) return tmpAPI; diff --git a/src/modules/display/Lcd2004/modinfo.json b/src/modules/display/Lcd2004/modinfo.json index 0352ab51..0061faf4 100644 --- a/src/modules/display/Lcd2004/modinfo.json +++ b/src/modules/display/Lcd2004/modinfo.json @@ -108,6 +108,12 @@ ], "esp8266_4mb": [ "marcoschwartz/LiquidCrystal_I2C@^1.1.4" + ], + "esp8266_1mb": [ + "marcoschwartz/LiquidCrystal_I2C@^1.1.4" + ], + "esp8266_1mb_ota": [ + "marcoschwartz/LiquidCrystal_I2C@^1.1.4" ] } } \ No newline at end of file diff --git a/src/modules/exec/ButtonIn/modinfo.json b/src/modules/exec/ButtonIn/modinfo.json index d3b91c71..549eec8c 100644 --- a/src/modules/exec/ButtonIn/modinfo.json +++ b/src/modules/exec/ButtonIn/modinfo.json @@ -42,6 +42,8 @@ "defActive": true, "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } \ No newline at end of file diff --git a/src/modules/exec/ButtonOut/modinfo.json b/src/modules/exec/ButtonOut/modinfo.json index 41f854b8..160527a7 100644 --- a/src/modules/exec/ButtonOut/modinfo.json +++ b/src/modules/exec/ButtonOut/modinfo.json @@ -43,6 +43,8 @@ "defActive": true, "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } \ No newline at end of file diff --git a/src/modules/exec/Mcp23017/modinfo.json b/src/modules/exec/Mcp23017/modinfo.json index ca836d72..7311e462 100644 --- a/src/modules/exec/Mcp23017/modinfo.json +++ b/src/modules/exec/Mcp23017/modinfo.json @@ -45,6 +45,14 @@ "esp8266_4mb": [ "adafruit/Adafruit MCP23017 Arduino Library@^2.1.0", "adafruit/Adafruit BusIO @ ^1.13.2" + ], + "esp8266_1mb": [ + "adafruit/Adafruit MCP23017 Arduino Library@^2.1.0", + "adafruit/Adafruit BusIO @ ^1.13.2" + ], + "esp8266_1mb_ota": [ + "adafruit/Adafruit MCP23017 Arduino Library@^2.1.0", + "adafruit/Adafruit BusIO @ ^1.13.2" ] } } \ No newline at end of file diff --git a/src/modules/exec/Pcf8574/modinfo.json b/src/modules/exec/Pcf8574/modinfo.json index 820923d6..8e95b19e 100644 --- a/src/modules/exec/Pcf8574/modinfo.json +++ b/src/modules/exec/Pcf8574/modinfo.json @@ -37,6 +37,12 @@ ], "esp8266_4mb": [ "adafruit/Adafruit BusIO @ ^1.13.2" + ], + "esp8266_1mb": [ + "adafruit/Adafruit BusIO @ ^1.13.2" + ], + "esp8266_1mb_ota": [ + "adafruit/Adafruit BusIO @ ^1.13.2" ] } } \ No newline at end of file diff --git a/src/modules/exec/Pwm8266/modinfo.json b/src/modules/exec/Pwm8266/modinfo.json index 806e6028..e5a5f478 100644 --- a/src/modules/exec/Pwm8266/modinfo.json +++ b/src/modules/exec/Pwm8266/modinfo.json @@ -41,6 +41,8 @@ "defActive": true, "usedLibs": { - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } \ No newline at end of file diff --git a/src/modules/exec/TelegramLT/modinfo.json b/src/modules/exec/TelegramLT/modinfo.json index 598441f4..3791ed6f 100644 --- a/src/modules/exec/TelegramLT/modinfo.json +++ b/src/modules/exec/TelegramLT/modinfo.json @@ -51,6 +51,8 @@ "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } diff --git a/src/modules/sensors/Ds18b20/modinfo.json b/src/modules/sensors/Ds18b20/modinfo.json index f3ac951e..96f6af73 100644 --- a/src/modules/sensors/Ds18b20/modinfo.json +++ b/src/modules/sensors/Ds18b20/modinfo.json @@ -43,6 +43,12 @@ ], "esp8266_4mb": [ "milesburton/DallasTemperature@^3.9.1" + ], + "esp8266_1mb": [ + "milesburton/DallasTemperature@^3.9.1" + ], + "esp8266_1mb_ota": [ + "milesburton/DallasTemperature@^3.9.1" ] } } \ No newline at end of file diff --git a/src/modules/sensors/Sonar/modinfo.json b/src/modules/sensors/Sonar/modinfo.json index e944043c..ce43afcb 100644 --- a/src/modules/sensors/Sonar/modinfo.json +++ b/src/modules/sensors/Sonar/modinfo.json @@ -37,6 +37,8 @@ "defActive": true, "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } \ No newline at end of file diff --git a/src/modules/virtual/Timer/modinfo.json b/src/modules/virtual/Timer/modinfo.json index a388be9f..0485564b 100644 --- a/src/modules/virtual/Timer/modinfo.json +++ b/src/modules/virtual/Timer/modinfo.json @@ -69,6 +69,8 @@ "defActive": true, "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } \ No newline at end of file diff --git a/src/modules/virtual/VButton/modinfo.json b/src/modules/virtual/VButton/modinfo.json index fd1811b7..ee31eddf 100644 --- a/src/modules/virtual/VButton/modinfo.json +++ b/src/modules/virtual/VButton/modinfo.json @@ -34,6 +34,8 @@ "defActive": true, "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } \ No newline at end of file diff --git a/src/modules/virtual/Variable/modinfo.json b/src/modules/virtual/Variable/modinfo.json index 9aec85e8..c6607ccc 100644 --- a/src/modules/virtual/Variable/modinfo.json +++ b/src/modules/virtual/Variable/modinfo.json @@ -71,6 +71,8 @@ "defActive": true, "usedLibs": { "esp32_4mb": [], - "esp8266_4mb": [] + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] } } \ No newline at end of file From 051550ad47d0b28004b56ce8ffc0c1e9b3b10518 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Thu, 13 Oct 2022 20:55:01 +0200 Subject: [PATCH 025/107] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/build/bundle.css.gz | Bin 5489 -> 5489 bytes data_svelte/build/bundle.js.gz | Bin 47660 -> 47837 bytes src/WsServer.cpp | 4 ++++ 3 files changed, 4 insertions(+) diff --git a/data_svelte/build/bundle.css.gz b/data_svelte/build/bundle.css.gz index 07b838325628393fb15b33014a7fddd82b1c4112..d1b51f4558c68f0e7ea7ca003db9fc18f2026330 100644 GIT binary patch delta 16 XcmeyU^-+smzMF$%Vw}fD_AF5VGuj1a delta 16 XcmeyU^-+smzMF$1UDRzOdzL5wF!TiR diff --git a/data_svelte/build/bundle.js.gz b/data_svelte/build/bundle.js.gz index 49e8b2f39e4771f9dadf9131a990024d14559937..982ec4265ec98055c4ced4d5479f68ffc4fcb353 100644 GIT binary patch delta 14229 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

o9aS>*#h1V@uF@aOQ5ClW?sc(|@I04N+D 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 delta 14026 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 diff --git a/src/WsServer.cpp b/src/WsServer.cpp index 50ad076b..e2bde5f7 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -220,6 +220,10 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) SerialPrint("i", F("=>WS"), "Msg from svelte web, WS No: " + String(num) + ", msg: " + msg); } + if (headerStr == "/tst|") { + standWebSocket.sendTXT(num, "/tstr|"); + } + } break; case WStype_BIN: { From 0080854a5363fbf6e40223a6cc26d625c717d2c2 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <49808844+DmitryBorisenko33@users.noreply.github.com> Date: Thu, 13 Oct 2022 21:07:52 +0200 Subject: [PATCH 026/107] 8266 --- data_svelte/items.json | 8 +++----- myProfile.json | 2 +- platformio.ini | 2 +- src/modules/API.cpp | 4 ++-- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/data_svelte/items.json b/data_svelte/items.json index d0759e6b..01f1ca76 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -587,18 +587,16 @@ "num": 44 }, { - "name": "45. PWM ESP32", + "name": "45. PWM ESP8266", "type": "Writing", - "subtype": "Pwm32", + "subtype": "Pwm8266", "id": "pwm", "widget": "range", "page": "Кнопки", "descr": "PWM", "int": 0, - "pin": 2, + "pin": 15, "freq": 5000, - "ledChannel": 2, - "PWM_resolution": 10, "val": 0, "apin": -1, "num": 45 diff --git a/myProfile.json b/myProfile.json index 13008892..83945fc4 100644 --- a/myProfile.json +++ b/myProfile.json @@ -25,7 +25,7 @@ }, "projectProp": { "platformio": { - "default_envs": "esp32_4mb", + "default_envs": "esp8266_4mb", "data_dir": "data_svelte" } }, diff --git a/platformio.ini b/platformio.ini index 847689e4..25ee3a54 100644 --- a/platformio.ini +++ b/platformio.ini @@ -41,7 +41,7 @@ build_src_filter = ${env:esp32_4mb_fromitems.build_src_filter} [platformio] -default_envs = esp32_4mb +default_envs = esp8266_4mb data_dir = data_svelte [common_env_data] diff --git a/src/modules/API.cpp b/src/modules/API.cpp index 2f6e4f3b..ab864765 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -27,7 +27,7 @@ void* getAPI_IoTServo(String subtype, String params); void* getAPI_Mcp23017(String subtype, String params); void* getAPI_Mp3(String subtype, String params); void* getAPI_Pcf8574(String subtype, String params); -void* getAPI_Pwm32(String subtype, String params); +void* getAPI_Pwm8266(String subtype, String params); void* getAPI_TelegramLT(String subtype, String params); void* getAPI_Lcd2004(String subtype, String params); @@ -60,7 +60,7 @@ if ((tmpAPI = getAPI_IoTServo(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mcp23017(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mp3(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pcf8574(subtype, params)) != nullptr) return tmpAPI; -if ((tmpAPI = getAPI_Pwm32(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Pwm8266(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_TelegramLT(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Lcd2004(subtype, params)) != nullptr) return tmpAPI; return nullptr; From 9326cc0aff14dc602c6ee32fe84472fb39f22330 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 14 Oct 2022 17:59:10 +0300 Subject: [PATCH 027/107] =?UTF-8?q?=D0=92=D0=B5=D1=80=D0=BD=D1=83=D0=BB=20?= =?UTF-8?q?Settigs=5F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myProfile.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/myProfile.json b/myProfile.json index 81a23898..b122eff2 100644 --- a/myProfile.json +++ b/myProfile.json @@ -20,7 +20,8 @@ "mqttin": 0, "pinSCL": 0, "pinSDA": 0, - "i2cFreq": 100000 + "i2cFreq": 100000, + "settings_": "" }, "projectProp": { "platformio": { From f763e27b1c47a2a3dcacb6f81958d18dc29b0a58 Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 18 Oct 2022 10:03:43 +0300 Subject: [PATCH 028/107] =?UTF-8?q?=D0=A3=D0=B1=D0=B8=D1=80=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BE=D1=82=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=A7=D0=B0=D1=82=D0=98?= =?UTF-8?q?=D0=94=20=D0=B2=20TelegramLT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/TelegramLT/TelegramLT.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/exec/TelegramLT/TelegramLT.cpp b/src/modules/exec/TelegramLT/TelegramLT.cpp index f57ba04b..19751d69 100644 --- a/src/modules/exec/TelegramLT/TelegramLT.cpp +++ b/src/modules/exec/TelegramLT/TelegramLT.cpp @@ -7,7 +7,7 @@ class TelegramLT : public IoTItem public: String _prevMsg = ""; String _token; - unsigned long _chatID; + String _chatID; TelegramLT(String parameters) : IoTItem(parameters) { jsonRead(parameters, "token", _token); @@ -21,11 +21,11 @@ public: HTTPClient http; http.begin(client, "http://live-control.com/iotm/telegram.php"); http.addHeader("Content-Type", "application/x-www-form-urlencoded"); - String httpRequestData = "url=https://api.telegram.org/bot" + _token + "/sendmessage?chat_id=" + uint64ToString(_chatID) + "&text=" + msg; + String httpRequestData = "url=https://api.telegram.org/bot" + _token + "/sendmessage?chat_id=" + _chatID + "&text=" + msg; int httpResponseCode = http.POST(httpRequestData); String payload = http.getString(); - SerialPrint("<-", F("Telegram"), "chat ID: " + uint64ToString(_chatID) + ", msg: " + msg); - SerialPrint("->", F("Telegram"), "chat ID: " + uint64ToString(_chatID) + ", server: " + httpResponseCode); + SerialPrint("<-", F("Telegram"), "chat ID: " + _chatID + ", msg: " + msg); + SerialPrint("->", F("Telegram"), "chat ID: " + _chatID + ", server: " + httpResponseCode); if (!strstr(payload.c_str(), "{\"ok\":true")) { value.valD = 1; From f3389c472188c2c9aa83106465069f7ca52160a9 Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <67171972+IoTManagerProject@users.noreply.github.com> Date: Tue, 18 Oct 2022 14:25:46 +0200 Subject: [PATCH 029/107] =?UTF-8?q?=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myProfile.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/myProfile.json b/myProfile.json index f6656391..ca456f09 100644 --- a/myProfile.json +++ b/myProfile.json @@ -1,6 +1,5 @@ { "iotmSettings": { - "settings_": "", "name": "IoTmanagerVer4", "apssid": "IoTmanager", "appass": "", @@ -21,7 +20,6 @@ "pinSCL": 0, "pinSDA": 0, "i2cFreq": 100000, - "settings_": "" "wg": "group1" }, "projectProp": { From 6c0a456fd0d372eefff35171e70044bad7448dea Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 20 Oct 2022 00:15:08 +0300 Subject: [PATCH 030/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=83=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B5=D1=80=D0=B0=20?= =?UTF-8?q?=D1=81=20=D1=83=D1=87=D0=B5=D1=82=D0=BE=D0=BC=20=D0=B2=D0=BE?= =?UTF-8?q?=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/Timer/Timer.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/modules/virtual/Timer/Timer.cpp b/src/modules/virtual/Timer/Timer.cpp index e5cc7039..f843206f 100644 --- a/src/modules/virtual/Timer/Timer.cpp +++ b/src/modules/virtual/Timer/Timer.cpp @@ -9,7 +9,6 @@ class Timer : public IoTItem { bool _unfin = false; bool _ticker = false; bool _repeat = false; - bool _needSave = false; bool _pause = false; int _initValue; @@ -17,20 +16,20 @@ class Timer : public IoTItem { Timer(String parameters): IoTItem(parameters) { jsonRead(parameters, "countDown", _initValue); _unfin = !_initValue; - value.valD = _initValue; - if (_initValue) value.valD = value.valD + 1; // +1 - компенсируем ранний вычет счетчика, ранний вычет, чтоб после события значение таймера не исказилось. - // +0 - если изначально установили бесконечный счет + + if (!_needSave) { + value.valD = _initValue; + if (_initValue) value.valD = value.valD + 1; // +1 - компенсируем ранний вычет счетчика, ранний вычет, чтоб после события значение таймера не исказилось. + } jsonRead(parameters, "ticker", _ticker); jsonRead(parameters, "repeat", _repeat); - jsonRead(parameters, "needSave", _needSave); // нужно сохранять счетчик в постоянную память } void doByInterval() { if (!_unfin && value.valD >= 0 && !_pause) { if (_repeat && value.valD == 0) value.valD = _initValue; value.valD--; - if (_needSave) needSave = true; if (value.valD == 0) { regEvent(value.valD, "Time's up"); } From c89127c067fdd578940915736c16c27b7426442d Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 20 Oct 2022 00:16:02 +0300 Subject: [PATCH 031/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=20=D0=B1=D0=B0=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D1=83=20=D1=81=D0=BE?= =?UTF-8?q?=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/ButtonIn/modinfo.json | 1 + src/modules/exec/ButtonOut/modinfo.json | 1 + src/modules/virtual/VButton/modinfo.json | 1 + src/modules/virtual/Variable/modinfo.json | 6 +++++- 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modules/exec/ButtonIn/modinfo.json b/src/modules/exec/ButtonIn/modinfo.json index 549eec8c..7ab286b7 100644 --- a/src/modules/exec/ButtonIn/modinfo.json +++ b/src/modules/exec/ButtonIn/modinfo.json @@ -9,6 +9,7 @@ "widget": "toggle", "page": "Кнопки", "descr": "Освещение", + "needSave": 0, "int": 0, "pin": 16, "execLevel": "1", diff --git a/src/modules/exec/ButtonOut/modinfo.json b/src/modules/exec/ButtonOut/modinfo.json index 160527a7..abfdac70 100644 --- a/src/modules/exec/ButtonOut/modinfo.json +++ b/src/modules/exec/ButtonOut/modinfo.json @@ -5,6 +5,7 @@ "name": "Управление пином", "type": "Writing", "subtype": "ButtonOut", + "needSave": 0, "id": "btn", "widget": "toggle", "page": "Кнопки", diff --git a/src/modules/virtual/VButton/modinfo.json b/src/modules/virtual/VButton/modinfo.json index ee31eddf..80ed2d53 100644 --- a/src/modules/virtual/VButton/modinfo.json +++ b/src/modules/virtual/VButton/modinfo.json @@ -6,6 +6,7 @@ "type": "Reading", "subtype": "VButton", "id": "vbtn", + "needSave": 0, "widget": "toggle", "page": "Кнопки", "descr": "Кнопка", diff --git a/src/modules/virtual/Variable/modinfo.json b/src/modules/virtual/Variable/modinfo.json index c6607ccc..940ca4c0 100644 --- a/src/modules/virtual/Variable/modinfo.json +++ b/src/modules/virtual/Variable/modinfo.json @@ -6,6 +6,7 @@ "type": "Reading", "subtype": "Variable", "id": "value", + "needSave": 0, "widget": "inputDgt", "page": "Ввод", "descr": "Введите число", @@ -18,6 +19,7 @@ "type": "Reading", "subtype": "Variable", "id": "time", + "needSave": 0, "widget": "inputTm", "page": "Ввод", "descr": "Введите время", @@ -30,6 +32,7 @@ "type": "Reading", "subtype": "Variable", "id": "time", + "needSave": 0, "widget": "inputDate", "page": "Ввод", "descr": "Введите дату", @@ -42,6 +45,7 @@ "type": "Reading", "subtype": "Variable", "id": "txt", + "needSave": 0, "widget": "inputTxt", "page": "Ввод", "descr": "Введите текст", @@ -65,7 +69,7 @@ "moduleDesc": "Специальный системный модуль для использования переменных в процессе автоматизации как элементов конфигурации.", "propInfo": { "int": "Не используется", - "val": "Не используется" + "val": "Значение при старте" } }, "defActive": true, From b8e76e19b89e410fa528fd3de5d38cea7c430d92 Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 20 Oct 2022 00:18:41 +0300 Subject: [PATCH 032/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2=20=D0=BD?= =?UTF-8?q?=D0=B0=20flash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 481 ++++++++++++++++++++++++++++++--- data_svelte/values.json | 3 + data_svelte_lite/settings.json | 3 +- data_svelte_lite/values.json | 3 + include/EspFileSystem.h | 1 + include/Global.h | 2 + include/classes/IoTItem.h | 9 +- myProfile.json | 2 +- platformio.ini | 5 +- src/EspFileSystem.cpp | 8 + src/Global.cpp | 6 +- src/Main.cpp | 26 +- src/PeriodicTasks.cpp | 4 +- src/classes/IoTItem.cpp | 32 ++- src/modules/API.cpp | 34 +++ 15 files changed, 540 insertions(+), 79 deletions(-) create mode 100644 data_svelte/values.json create mode 100644 data_svelte_lite/values.json diff --git a/data_svelte/items.json b/data_svelte/items.json index fd9eb626..7ec9b010 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -7,7 +7,34 @@ "header": "Виртуальные элементы" }, { - "name": "1. Таймер", + "name": "1. График", + "type": "Writing", + "subtype": "Loging", + "id": "log", + "widget": "chart2", + "page": "Графики", + "descr": "Температура", + "num": 1, + "int": 5, + "logid": "t", + "points": 300 + }, + { + "name": "2. График дневного расхода", + "type": "Writing", + "subtype": "LogingDaily", + "id": "log", + "widget": "chart3", + "page": "Графики", + "descr": "Температура", + "num": 2, + "int": 1, + "logid": "t", + "points": 365, + "test": 0 + }, + { + "name": "3. Таймер", "type": "Writing", "subtype": "Timer", "id": "timer", @@ -19,73 +46,227 @@ "ticker": 1, "repeat": 1, "needSave": 0, - "num": 1 + "num": 3 }, { - "name": "2. Окно ввода числа (переменная)", + "name": "4. Окно ввода числа (переменная)", "type": "Reading", "subtype": "Variable", "id": "value", + "needSave": 0, "widget": "inputDgt", "page": "Ввод", "descr": "Введите число", "int": "0", "val": "0.0", - "num": 2 + "num": 4 }, { - "name": "3. Окно ввода времени", + "name": "5. Окно ввода времени", "type": "Reading", "subtype": "Variable", "id": "time", + "needSave": 0, "widget": "inputTm", "page": "Ввод", "descr": "Введите время", "int": "0", "val": "02:00", - "num": 3 + "num": 5 }, { - "name": "4. Окно ввода даты", + "name": "6. Окно ввода даты", "type": "Reading", "subtype": "Variable", "id": "time", + "needSave": 0, "widget": "inputDate", "page": "Ввод", "descr": "Введите дату", "int": "0", "val": "24.05.2022", - "num": 4 + "num": 6 }, { - "name": "5. Окно ввода текста", + "name": "7. Окно ввода текста", "type": "Reading", "subtype": "Variable", "id": "txt", + "needSave": 0, "widget": "inputTxt", "page": "Ввод", "descr": "Введите текст", "int": "0", "val": "текст", - "num": 5 + "num": 7 }, { - "name": "6. Виртуальная кнопка", + "name": "8. Виртуальная кнопка", "type": "Reading", "subtype": "VButton", "id": "vbtn", + "needSave": 0, "widget": "toggle", "page": "Кнопки", "descr": "Кнопка", "int": "0", "val": "0", - "num": 6 + "num": 8 }, { "header": "Сенсоры" }, { - "name": "7. DS18B20 Температура", + "name": "9. Acs712 Ток", + "type": "Reading", + "subtype": "Acs712", + "id": "amp", + "widget": "anydataAmp", + "page": "Сенсоры", + "descr": "Ток", + "round": 3, + "pin": 39, + "int": 5, + "num": 9 + }, + { + "name": "10. AHTXX Температура", + "type": "Reading", + "subtype": "AhtXXt", + "id": "Temp20", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "AHTXX Температура", + "int": 15, + "addr": "0x38", + "shtType": 1, + "round": 1, + "num": 10 + }, + { + "name": "11. AHTXX Влажность", + "type": "Reading", + "subtype": "AhtXXh", + "id": "Hum20", + "widget": "anydataHum", + "page": "Сенсоры", + "descr": "AHTXX Влажность", + "int": 15, + "addr": "0x38", + "shtType": 1, + "round": 1, + "num": 11 + }, + { + "name": "12. Аналоговый сенсор", + "type": "Reading", + "subtype": "AnalogAdc", + "id": "t", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "Температура", + "map": "1,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 1, + "pin": 0, + "int": 15, + "avgSteps": 1, + "num": 12 + }, + { + "name": "13. BME280 Температура", + "type": "Reading", + "subtype": "Bme280t", + "id": "tmp3", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "Температура", + "int": 15, + "addr": "0x77", + "round": 1, + "num": 13 + }, + { + "name": "14. BME280 Давление", + "type": "Reading", + "subtype": "Bme280p", + "id": "Press3", + "widget": "anydataMm", + "page": "Сенсоры", + "descr": "Давление", + "int": 15, + "addr": "0x77", + "round": 1, + "num": 14 + }, + { + "name": "15. BME280 Влажность", + "type": "Reading", + "subtype": "Bme280h", + "id": "Hum3", + "widget": "anydataHum", + "page": "Сенсоры", + "descr": "Влажность", + "int": 15, + "addr": "0x77", + "round": 1, + "num": 15 + }, + { + "name": "16. BMP280 Температура", + "type": "Reading", + "subtype": "Bmp280t", + "id": "tmp3", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "280 Температура", + "int": 15, + "addr": "0x77", + "round": 1, + "num": 16 + }, + { + "name": "17. BMP280 Давление", + "type": "Reading", + "subtype": "Bmp280p", + "id": "Press3", + "widget": "anydataMm", + "page": "Сенсоры", + "descr": "280 Давление", + "int": 15, + "addr": "0x77", + "round": 1, + "num": 17 + }, + { + "name": "18. DHT11 Температура", + "type": "Reading", + "subtype": "Dht1122t", + "id": "tmp3", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "Температура", + "int": 15, + "pin": 0, + "senstype": "dht11", + "num": 18 + }, + { + "name": "19. DHT11 Влажность", + "type": "Reading", + "subtype": "Dht1122h", + "id": "Hum3", + "widget": "anydataHum", + "page": "Сенсоры", + "descr": "Влажность", + "int": 15, + "pin": 0, + "senstype": "dht11", + "num": 19 + }, + { + "name": "20. DS18B20 Температура", "type": "Reading", "subtype": "Ds18b20", "id": "dstmp", @@ -97,11 +278,153 @@ "index": 0, "addr": "", "round": 1, - "num": 7 + "num": 20 }, { - "name": "8. Сканер кнопок 433 MHz", - "num": 8, + "name": "21. GY21 Температура", + "type": "Reading", + "subtype": "GY21t", + "id": "tmp4", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "Температура", + "round": 1, + "int": 15, + "num": 21 + }, + { + "name": "22. GY21 Влажность", + "type": "Reading", + "subtype": "GY21h", + "id": "Hum4", + "widget": "anydataHum", + "page": "Сенсоры", + "descr": "Влажность", + "round": 1, + "int": 15, + "num": 22 + }, + { + "name": "23. HDC1080 Температура", + "type": "Reading", + "subtype": "Hdc1080t", + "id": "Temp1080", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "1080 Температура", + "int": 15, + "addr": "0x40", + "round": 1, + "num": 23 + }, + { + "name": "24. HDC1080 Влажность", + "type": "Reading", + "subtype": "Hdc1080h", + "id": "Hum1080", + "widget": "anydataHum", + "page": "Сенсоры", + "descr": "1080 Влажность", + "int": 15, + "addr": "0x40", + "round": 1, + "num": 24 + }, + { + "name": "25. MAX6675 Температура", + "type": "Reading", + "subtype": "Max6675t", + "id": "maxtmp", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "MAX Температура", + "int": 15, + "DO": 12, + "CS": 13, + "CLK": 14, + "num": 25 + }, + { + "name": "26. PZEM 004t Напряжение", + "type": "Reading", + "subtype": "Pzem004v", + "id": "v", + "widget": "anydataVlt", + "page": "PZEM", + "descr": "Напряжение", + "int": 15, + "addr": "0xF8", + "round": 1, + "num": 26 + }, + { + "name": "27. PZEM 004t Сила тока", + "type": "Reading", + "subtype": "Pzem004a", + "id": "a", + "widget": "anydataAmp", + "page": "PZEM", + "descr": "Сила тока", + "int": 15, + "addr": "0xF8", + "round": 1, + "num": 27 + }, + { + "name": "28. PZEM 004t Мощность", + "type": "Reading", + "subtype": "Pzem004w", + "id": "w", + "widget": "anydataWt", + "page": "PZEM", + "descr": "Мощность", + "int": 15, + "addr": "0xF8", + "round": 1, + "num": 28 + }, + { + "name": "29. PZEM 004t Энергия", + "type": "Reading", + "subtype": "Pzem004wh", + "id": "wh", + "widget": "anydataWth", + "page": "PZEM", + "descr": "Энергия", + "int": 15, + "addr": "0xF8", + "round": 1, + "num": 29 + }, + { + "name": "30. PZEM 004t Частота", + "type": "Reading", + "subtype": "Pzem004hz", + "id": "hz", + "widget": "anydataHtz", + "page": "PZEM", + "descr": "Частота", + "int": 15, + "addr": "0xF8", + "round": 1, + "num": 30 + }, + { + "name": "31. PZEM 004t Косинус", + "type": "Reading", + "subtype": "Pzem004pf", + "id": "pf", + "widget": "anydata", + "page": "PZEM", + "descr": "Косинус F", + "int": 15, + "addr": "0xF8", + "round": 1, + "num": 31 + }, + { + "name": "32. Сканер кнопок 433 MHz", + "num": 32, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -110,8 +433,56 @@ "pinTx": 12 }, { - "name": "9. HC-SR04 Ультразвуковой дальномер", - "num": 9, + "name": "33. Sht20 Температура", + "type": "Reading", + "subtype": "Sht20t", + "id": "tmp2", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "Температура", + "int": 15, + "round": 1, + "num": 33 + }, + { + "name": "34. Sht20 Влажность", + "type": "Reading", + "subtype": "Sht20h", + "id": "Hum2", + "widget": "anydataHum", + "page": "Сенсоры", + "descr": "Влажность", + "int": 15, + "round": 1, + "num": 34 + }, + { + "name": "35. Sht30 Температура", + "type": "Reading", + "subtype": "Sht30t", + "id": "tmp30", + "widget": "anydataTmp", + "page": "Сенсоры", + "descr": "SHT30 Температура", + "int": 15, + "round": 1, + "num": 35 + }, + { + "name": "36. Sht30 Влажность", + "type": "Reading", + "subtype": "Sht30h", + "id": "Hum30", + "widget": "anydataHum", + "page": "Сенсоры", + "descr": "SHT30 Влажность", + "int": 15, + "round": 1, + "num": 36 + }, + { + "name": "37. HC-SR04 Ультразвуковой дальномер", + "num": 37, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -122,29 +493,44 @@ "pinEcho": 4, "int": 5 }, + { + "name": "38. UART", + "type": "Reading", + "subtype": "UART", + "page": "", + "descr": "", + "widget": "nil", + "id": "u", + "tx": 12, + "rx": 13, + "speed": 9600, + "num": 38 + }, { "header": "Исполнительные устройства" }, { - "name": "10. Кнопка подключенная к пину", + "name": "39. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", "widget": "toggle", "page": "Кнопки", "descr": "Освещение", + "needSave": 0, "int": 0, "pin": 16, "execLevel": "1", "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 10 + "num": 39 }, { - "name": "11. Управление пином", + "name": "40. Управление пином", "type": "Writing", "subtype": "ButtonOut", + "needSave": 0, "id": "btn", "widget": "toggle", "page": "Кнопки", @@ -152,10 +538,24 @@ "int": 0, "inv": 0, "pin": 2, - "num": 11 + "num": 40 }, { - "name": "12. Расширитель портов Mcp23017", + "name": "41. Сервопривод", + "type": "Writing", + "subtype": "IoTServo", + "id": "servo", + "widget": "range", + "page": "servo", + "descr": "угол", + "int": 1, + "pin": 12, + "apin": -1, + "amap": "0, 4096, 0, 180", + "num": 41 + }, + { + "name": "42. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -165,10 +565,23 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 12 + "num": 42 }, { - "name": "13. Расширитель портов Pcf8574", + "name": "43. MP3 плеер", + "type": "Reading", + "subtype": "Mp3", + "id": "mp3", + "widget": "", + "page": "", + "descr": "", + "int": 1, + "pins": "14,12", + "volume": 20, + "num": 43 + }, + { + "name": "44. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -178,10 +591,10 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 13 + "num": 44 }, { - "name": "14. PWM ESP8266", + "name": "45. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -193,10 +606,10 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 14 + "num": 45 }, { - "name": "15. Телеграм-Лайт", + "name": "46. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -205,13 +618,13 @@ "descr": "", "token": "", "chatID": "", - "num": 15 + "num": 46 }, { "header": "Экраны" }, { - "name": "16. LCD экран 2004", + "name": "47. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -223,10 +636,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 16 + "num": 47 }, { - "name": "17. LCD экран 1602", + "name": "48. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -238,6 +651,6 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 17 + "num": 48 } ] \ No newline at end of file diff --git a/data_svelte/values.json b/data_svelte/values.json new file mode 100644 index 00000000..0e0dcd23 --- /dev/null +++ b/data_svelte/values.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/data_svelte_lite/settings.json b/data_svelte_lite/settings.json index f83115c2..e6d79494 100644 --- a/data_svelte_lite/settings.json +++ b/data_svelte_lite/settings.json @@ -19,5 +19,6 @@ "pinSCL": 0, "pinSDA": 0, "i2cFreq": 100000, - "settings_": "" + "settings_": "", + "wg": "group1" } \ No newline at end of file diff --git a/data_svelte_lite/values.json b/data_svelte_lite/values.json new file mode 100644 index 00000000..0e0dcd23 --- /dev/null +++ b/data_svelte_lite/values.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/include/EspFileSystem.h b/include/EspFileSystem.h index 5554dd49..81a6a660 100644 --- a/include/EspFileSystem.h +++ b/include/EspFileSystem.h @@ -38,6 +38,7 @@ extern void globalVarsSync(); extern String getParamsJson(); extern void syncSettingsFlashJson(); +extern void syncValuesFlashJson(); extern const String getChipId(); extern void setChipId(); diff --git a/include/Global.h b/include/Global.h index 3f036530..a8ae38d9 100644 --- a/include/Global.h +++ b/include/Global.h @@ -83,7 +83,9 @@ extern WebSocketsServer standWebSocket; ***********************************************глобальные переменные************************************************** **********************************************************************************************************************/ extern String settingsFlashJson; +extern String valuesFlashJson; extern String errorsHeapJson; +extern bool needSaveValues; // buf extern String orderBuf; diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index 555c3a35..62884956 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -6,9 +6,6 @@ struct IoTValue { String valS = ""; bool isDecimal = true; - - uint8_t* extBinInfo = NULL; // дополнительные бинарные данные из модуля - size_t extBinInfoSize = 0; // размер дополнительных данных в байтах }; class IoTItem { @@ -38,12 +35,11 @@ class IoTItem { bool iAmDead = false; // признак необходимости удалить объект из базы bool iAmLocal = true; // признак того, что айтем был создан локально - bool needSave = false; bool enableDoByInt = true; virtual IoTGpio* getGpioDriver(); - virtual void setValue(IoTValue Value); - virtual void setValue(String valStr); + virtual void setValue(IoTValue Value, bool generateEvent = true); + virtual void setValue(String valStr, bool generateEvent = true); String getRoundValue(); //методы для графиков @@ -54,6 +50,7 @@ class IoTItem { virtual void setTodayDate(); protected: + bool _needSave = false; // признак необходимости сохранять и загружать значение элемента на flash String _subtype; String _id; unsigned long _interval; diff --git a/myProfile.json b/myProfile.json index ca456f09..3c0ddde3 100644 --- a/myProfile.json +++ b/myProfile.json @@ -24,7 +24,7 @@ }, "projectProp": { "platformio": { - "default_envs": "esp8266_1mb_ota" + "default_envs": "esp8266_4mb" } }, "modules": { diff --git a/platformio.ini b/platformio.ini index 4660c6a7..311b037d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -83,11 +83,10 @@ build_src_filter = ${env:esp32_4mb_fromitems.build_src_filter} [platformio] -default_envs = esp8266_1mb_ota -data_dir = data_svelte_lite +default_envs = esp8266_4mb +data_dir = data_svelte [common_env_data] -upload_port = COM4 lib_deps_external = bblanchon/ArduinoJson @6.18.0 knolleary/PubSubClient diff --git a/src/EspFileSystem.cpp b/src/EspFileSystem.cpp index 9ede484a..5f8619e8 100644 --- a/src/EspFileSystem.cpp +++ b/src/EspFileSystem.cpp @@ -14,6 +14,10 @@ void globalVarsSync() { settingsFlashJson = readFile(F("settings.json"), 4096); settingsFlashJson.replace("\r\n", ""); + valuesFlashJson = readFile(F("values.json"), 4096); + valuesFlashJson.replace("\r\n", ""); + + mqttPrefix = jsonReadStr(settingsFlashJson, F("mqttPrefix")); mqttRootDevice = mqttPrefix + "/" + chipId; jsonWriteStr_(settingsFlashJson, "root", mqttRootDevice); @@ -34,6 +38,10 @@ void syncSettingsFlashJson() { writeFile(F("settings.json"), settingsFlashJson); } +void syncValuesFlashJson() { + writeFile(F("values.json"), valuesFlashJson); +} + const String getChipId() { return String(ESP_getChipId()) + "-" + String(ESP_getFlashChipId()); } diff --git a/src/Global.cpp b/src/Global.cpp index b05c868e..2be45040 100644 --- a/src/Global.cpp +++ b/src/Global.cpp @@ -31,8 +31,10 @@ WebSocketsServer standWebSocket = WebSocketsServer(81); **********************************************************************************************************************/ IoTGpio IoTgpio(0); -String settingsFlashJson = "{}"; //переменная в которой хранятся все настройки, находится в оперативной памяти и синхронизированна с flash памятью -String errorsHeapJson = "{}"; //переменная в которой хранятся все ошибки, находится в оперативной памяти только +String settingsFlashJson = "{}"; // переменная в которой хранятся все настройки, находится в оперативной памяти и синхронизированна с flash памятью +String valuesFlashJson = "{}"; // переменная в которой хранятся все значения элементов, которые необходимо сохранить на flash. Находится в оперативной памяти и синхронизированна с flash памятью +String errorsHeapJson = "{}"; // переменная в которой хранятся все ошибки, находится в оперативной памяти только +bool needSaveValues = false; // признак необходимости сбросить значения элементов на flash // buf String orderBuf = ""; diff --git a/src/Main.cpp b/src/Main.cpp index dce96513..4c7a8dd8 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -149,6 +149,14 @@ void loop() { loopPeriod = millis() - st; if (loopPeriod > 2) Serial.println(loopPeriod); #endif + + // сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) + if (needSaveValues && millis()%1000 == 0) { + syncValuesFlashJson(); + needSaveValues = false; + delay(1); + Serial.println("syncValuesFlashJson()"); + } } //отправка json @@ -162,24 +170,6 @@ void loop() { // delay(1); // } -// сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) -// currentMillis = millis(); -// if (currentMillis - prevMillis >= 1000) { -// prevMillis = millis(); -// volStrForSave = ""; -// for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { -// if ((*it)->needSave) { -// (*it)->needSave = false; -// volStrForSave = volStrForSave + (*it)->getID() + "=" + (*it)->getValue() + ";"; -// } -// } -// -// if (volStrForSave != "") { -// Serial.print("volStrForSave: "); -// Serial.println(volStrForSave.c_str()); -// } -//} - // File dir = FileFS.open("/", "r"); // String out; // printDirectory(dir, out); diff --git a/src/PeriodicTasks.cpp b/src/PeriodicTasks.cpp index e44f5ecc..73e14dcc 100644 --- a/src/PeriodicTasks.cpp +++ b/src/PeriodicTasks.cpp @@ -34,16 +34,18 @@ void periodicTasksInit() { void printGlobalVarSize() { size_t settingsFlashJsonSize = settingsFlashJson.length(); // SerialPrint(F("i"), F("settingsFlashJson"), String(settingsFlashJsonSize)); + size_t valuesFlashJsonSize = valuesFlashJson.length(); size_t errorsHeapJsonSize = errorsHeapJson.length(); // SerialPrint(F("i"), F("errorsHeapJson"), String(errorsHeapJsonSize)); size_t devListHeapJsonSize = devListHeapJson.length(); // SerialPrint(F("i"), F("devListHeapJson"), String(devListHeapJsonSize)); - SerialPrint(F("i"), F("Var summ sz"), String(settingsFlashJsonSize + errorsHeapJsonSize + devListHeapJsonSize)); + SerialPrint(F("i"), F("Var summ sz"), String(settingsFlashJsonSize + valuesFlashJsonSize + errorsHeapJsonSize + devListHeapJsonSize)); size_t halfBuffer = JSON_BUFFER_SIZE / 2; if (settingsFlashJsonSize > halfBuffer || + valuesFlashJsonSize > halfBuffer || errorsHeapJsonSize > halfBuffer || devListHeapJsonSize > halfBuffer) { SerialPrint(F("EE"), F("Json"), F("Insufficient buffer size!!!")); diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 4f3cb1a4..02061f9d 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -20,11 +20,11 @@ IoTItem::IoTItem(String parameters) { String valAsStr; if (jsonRead(parameters, F("val"), valAsStr, false)) // значение переменной или датчика при инициализации если есть в конфигурации - if (value.isDecimal = isDigitDotCommaStr(valAsStr)) { - value.valD = valAsStr.toFloat(); - } else { - value.valS = valAsStr; - } + setValue(valAsStr, false); + + jsonRead(parameters, F("needSave"), _needSave, false); + if (_needSave && jsonRead(valuesFlashJson, _id, valAsStr, false)) // пробуем достать из сохранения значение элемента, если указано, что нужно сохранять + setValue(valAsStr, false); String map; jsonRead(parameters, F("map"), map, false); @@ -58,27 +58,33 @@ String IoTItem::getValue() { } //определяем тип прилетевшей величины -void IoTItem::setValue(String valStr) { +void IoTItem::setValue(String valStr, bool generateEvent) { if (value.isDecimal = isDigitDotCommaStr(valStr)) { value.valD = valStr.toFloat(); } else { value.valS = valStr; } - setValue(value); + if (generateEvent) setValue(value, generateEvent); } // -void IoTItem::setValue(IoTValue Value) { +void IoTItem::setValue(IoTValue Value, bool generateEvent) { value = Value; - if (value.isDecimal) { - regEvent(value.valD, ""); - } else { - regEvent(value.valS, ""); - } + if (generateEvent) + if (value.isDecimal) { + regEvent(value.valD, ""); + } else { + regEvent(value.valS, ""); + } } //когда событие случилось void IoTItem::regEvent(String value, String consoleInfo = "") { + if (_needSave) { + jsonWriteStr_(valuesFlashJson, _id, value); + needSaveValues = true; + } + generateEvent(_id, value); publishStatusMqtt(_id, value); diff --git a/src/modules/API.cpp b/src/modules/API.cpp index 4efeb752..ab864765 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -1,14 +1,31 @@ #include "ESPConfiguration.h" +void* getAPI_Loging(String subtype, String params); +void* getAPI_LogingDaily(String subtype, String params); void* getAPI_Timer(String subtype, String params); void* getAPI_Variable(String subtype, String params); void* getAPI_VButton(String subtype, String params); +void* getAPI_Acs712(String subtype, String params); +void* getAPI_AhtXX(String subtype, String params); +void* getAPI_AnalogAdc(String subtype, String params); +void* getAPI_Bme280(String subtype, String params); +void* getAPI_Bmp280(String subtype, String params); +void* getAPI_Dht1122(String subtype, String params); void* getAPI_Ds18b20(String subtype, String params); +void* getAPI_GY21(String subtype, String params); +void* getAPI_Hdc1080(String subtype, String params); +void* getAPI_Max6675(String subtype, String params); +void* getAPI_Pzem004(String subtype, String params); void* getAPI_RCswitch(String subtype, String params); +void* getAPI_Sht20(String subtype, String params); +void* getAPI_Sht30(String subtype, String params); void* getAPI_Sonar(String subtype, String params); +void* getAPI_UART(String subtype, String params); void* getAPI_ButtonIn(String subtype, String params); void* getAPI_ButtonOut(String subtype, String params); +void* getAPI_IoTServo(String subtype, String params); void* getAPI_Mcp23017(String subtype, String params); +void* getAPI_Mp3(String subtype, String params); void* getAPI_Pcf8574(String subtype, String params); void* getAPI_Pwm8266(String subtype, String params); void* getAPI_TelegramLT(String subtype, String params); @@ -16,15 +33,32 @@ void* getAPI_Lcd2004(String subtype, String params); void* getAPI(String subtype, String params) { void* tmpAPI; +if ((tmpAPI = getAPI_Loging(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_LogingDaily(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Timer(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Variable(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_VButton(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Acs712(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_AhtXX(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_AnalogAdc(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Bme280(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Bmp280(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Dht1122(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Ds18b20(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_GY21(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Hdc1080(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Max6675(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Pzem004(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_RCswitch(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Sht20(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Sht30(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Sonar(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_UART(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_ButtonIn(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_ButtonOut(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_IoTServo(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mcp23017(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Mp3(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pcf8574(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pwm8266(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_TelegramLT(subtype, params)) != nullptr) return tmpAPI; From 2a27b1d9bf8f4adaa18f439f8f2ace021a78d583 Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 20 Oct 2022 09:28:35 +0300 Subject: [PATCH 033/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83?= =?UTF-8?q?=20=D1=81=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9=20setValue=20=D1=83=20=D0=B4=D0=BE=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=BD=D0=B8=D1=85=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/ButtonIn/ButtonIn.cpp | 2 +- src/modules/exec/ButtonOut/ButtonOut.cpp | 2 +- src/modules/exec/IoTServo/IoTServo.cpp | 2 +- src/modules/exec/Pwm32/Pwm32.cpp | 2 +- src/modules/exec/Pwm8266/Pwm8266.cpp | 2 +- src/modules/virtual/Loging/Loging.cpp | 2 +- src/modules/virtual/VButton/VButton.cpp | 2 +- src/modules/virtual/Variable/Variable.cpp | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/exec/ButtonIn/ButtonIn.cpp b/src/modules/exec/ButtonIn/ButtonIn.cpp index 1a00f59b..6d2fe354 100644 --- a/src/modules/exec/ButtonIn/ButtonIn.cpp +++ b/src/modules/exec/ButtonIn/ButtonIn.cpp @@ -68,7 +68,7 @@ class ButtonIn : public IoTItem { _lastButtonState = _reading; } - void setValue(IoTValue Value) { + void setValue(IoTValue Value, bool generateEvent = true) { value = Value; regEvent((String)(int)value.valD, "ButtonIn"); } diff --git a/src/modules/exec/ButtonOut/ButtonOut.cpp b/src/modules/exec/ButtonOut/ButtonOut.cpp index 6587275f..5509d985 100644 --- a/src/modules/exec/ButtonOut/ButtonOut.cpp +++ b/src/modules/exec/ButtonOut/ButtonOut.cpp @@ -39,7 +39,7 @@ class ButtonOut : public IoTItem { return {}; // команда поддерживает возвращаемое значения. Т.е. по итогу выполнения команды или общения с внешней системой, можно вернуть значение в сценарий для дальнейшей обработки } - void setValue(IoTValue Value) { + void setValue(IoTValue Value, bool generateEvent = true) { value = Value; IoTgpio.digitalWrite(_pin, _inv?!value.valD:value.valD); regEvent((String)(int)value.valD, "ButtonOut"); diff --git a/src/modules/exec/IoTServo/IoTServo.cpp b/src/modules/exec/IoTServo/IoTServo.cpp index 8c7cc357..00c68f23 100644 --- a/src/modules/exec/IoTServo/IoTServo.cpp +++ b/src/modules/exec/IoTServo/IoTServo.cpp @@ -52,7 +52,7 @@ class IoTServo : public IoTItem { return {}; } - void setValue(IoTValue Value) { + void setValue(IoTValue Value, bool generateEvent = true) { value = Value; if (value.isDecimal & (_oldValue != value.valD)) { _oldValue = value.valD; diff --git a/src/modules/exec/Pwm32/Pwm32.cpp b/src/modules/exec/Pwm32/Pwm32.cpp index 6798da3f..f6351c47 100644 --- a/src/modules/exec/Pwm32/Pwm32.cpp +++ b/src/modules/exec/Pwm32/Pwm32.cpp @@ -54,7 +54,7 @@ class Pwm32 : public IoTItem { } } - void setValue(IoTValue Value) { + void setValue(IoTValue Value, bool generateEvent = true) { value = Value; ledcWrite(_ledChannel, value.valD); regEvent(value.valD, "Pwm32"); diff --git a/src/modules/exec/Pwm8266/Pwm8266.cpp b/src/modules/exec/Pwm8266/Pwm8266.cpp index 54307881..e52ab9a9 100644 --- a/src/modules/exec/Pwm8266/Pwm8266.cpp +++ b/src/modules/exec/Pwm8266/Pwm8266.cpp @@ -44,7 +44,7 @@ class Pwm8266 : public IoTItem { } } - void setValue(IoTValue Value) { + void setValue(IoTValue Value, bool generateEvent = true) { value = Value; IoTgpio.analogWrite(_pin, value.valD); regEvent(value.valD, "Pwm8266"); diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index c0aea13b..11193b85 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -311,7 +311,7 @@ class Date : public IoTItem { setValue(value); } - void setValue(IoTValue Value) { + void setValue(IoTValue Value, bool generateEvent = true) { value = Value; regEvent(value.valS, ""); //отправка данных при изменении даты diff --git a/src/modules/virtual/VButton/VButton.cpp b/src/modules/virtual/VButton/VButton.cpp index 2bd19cb8..ad0bfcd4 100644 --- a/src/modules/virtual/VButton/VButton.cpp +++ b/src/modules/virtual/VButton/VButton.cpp @@ -6,7 +6,7 @@ class VButton : public IoTItem { public: VButton(String parameters): IoTItem(parameters) { } - void setValue(IoTValue Value) { + void setValue(IoTValue Value, bool generateEvent = true) { value = Value; regEvent((String)(int)value.valD, "VButton"); } diff --git a/src/modules/virtual/Variable/Variable.cpp b/src/modules/virtual/Variable/Variable.cpp index c921bb64..0ecd533e 100644 --- a/src/modules/virtual/Variable/Variable.cpp +++ b/src/modules/virtual/Variable/Variable.cpp @@ -9,7 +9,7 @@ class Variable : public IoTItem { } // особенность данного модуля - просто хранение значения для сценария, нет событий - // void setValue(IoTValue Value) { + // void setValue(IoTValue Value, bool generateEvent = true) { // value = Value; // } From cc925000082b15466b9899cb2c6aeb33e19c8f6a Mon Sep 17 00:00:00 2001 From: avaksru Date: Thu, 20 Oct 2022 10:09:16 +0300 Subject: [PATCH 034/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B5=20=D0=BE?= =?UTF-8?q?=D0=B1=20=D1=83=D1=81=D0=BF=D0=B5=D1=88=D0=BD=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/TelegramLT/TelegramLT.cpp | 23 +++++++++++------- src/modules/exec/TelegramLT/modinfo.json | 28 ++++++++++++---------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/modules/exec/TelegramLT/TelegramLT.cpp b/src/modules/exec/TelegramLT/TelegramLT.cpp index 19751d69..3c177e7a 100644 --- a/src/modules/exec/TelegramLT/TelegramLT.cpp +++ b/src/modules/exec/TelegramLT/TelegramLT.cpp @@ -9,14 +9,16 @@ public: String _token; String _chatID; - TelegramLT(String parameters) : IoTItem(parameters) { + TelegramLT(String parameters) : IoTItem(parameters) + { jsonRead(parameters, "token", _token); jsonRead(parameters, "chatID", _chatID); } void sendTelegramMsg(bool often, String msg) - { - if (WiFi.status() == WL_CONNECTED && (often || !often && _prevMsg != msg)) { + { + if (WiFi.status() == WL_CONNECTED && (often || !often && _prevMsg != msg)) + { WiFiClient client; HTTPClient http; http.begin(client, "http://live-control.com/iotm/telegram.php"); @@ -27,12 +29,16 @@ public: SerialPrint("<-", F("Telegram"), "chat ID: " + _chatID + ", msg: " + msg); SerialPrint("->", F("Telegram"), "chat ID: " + _chatID + ", server: " + httpResponseCode); - if (!strstr(payload.c_str(), "{\"ok\":true")) { - value.valD = 1; + if (!strstr(payload.c_str(), "{\"ok\":true")) + { + value.valD = 0; Serial.printf("Telegram error, msg from server: %s\n", payload.c_str()); regEvent(value.valD, payload); - } else { - value.valD = 0; + } + else + { + value.valD = 1; + regEvent(value.valD, payload); } http.end(); _prevMsg = msg; @@ -41,7 +47,8 @@ public: IoTValue execute(String command, std::vector ¶m) { - if (param.size() == 1) { + if (param.size() == 1) + { String strTmp; if (param[0].isDecimal && param[0].valS == "") strTmp = param[0].valD; diff --git a/src/modules/exec/TelegramLT/modinfo.json b/src/modules/exec/TelegramLT/modinfo.json index 3791ed6f..95414c98 100644 --- a/src/modules/exec/TelegramLT/modinfo.json +++ b/src/modules/exec/TelegramLT/modinfo.json @@ -21,11 +21,11 @@ "authorGit": "https://github.com/avaksru", "specialThanks": "", "moduleName": "TelegramLT", - "moduleVersion": "1.0", + "moduleVersion": "2", "usedRam": { - "esp32_4mb": 15, - "esp8266_4mb": 15 - }, + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, "title": "Телеграм-извещатель", "moduleDesc": "Только отправка уведомлений в телеграм о событиях. Модуль занимает значительно меньше памяти в ESP по сравнению со стандартным. Внимание! для отправки сообщений используется промежуточный сервер http://live-control.com", "propInfo": { @@ -34,15 +34,19 @@ }, "retInfo": "Элемент данного модуля может иметь два значения 0 - все хорошо, 1 - произошла ошибка отправки сообщения, подробности в консоли. Данный статус можно использовать в сценарии для совершения экстренных действий при ошибке.", "funcInfo": [ - { - "name": "sendMsg", - "descr": "Отправить сообщение без повторений.", - "params": ["Сообщение, может быть строкой, числом или ИД другого элемента для получения значения"] + { + "name": "sendMsg", + "descr": "Отправить сообщение без повторений.", + "params": [ + "Сообщение, может быть строкой, числом или ИД другого элемента для получения значения" + ] }, - { - "name": "sendOftenMsg", - "descr": "Отправить сообщение в любом случае, даж если отправляли такое ранее.", - "params": ["Сообщение, может быть строкой, числом или ИД другого элемента для получения значения"] + { + "name": "sendOftenMsg", + "descr": "Отправить сообщение в любом случае, даж если отправляли такое ранее.", + "params": [ + "Сообщение, может быть строкой, числом или ИД другого элемента для получения значения" + ] } ] }, From 7f5ebb768d5dc3dfea5d2d278775efe6d3f87f04 Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 20 Oct 2022 10:54:04 +0300 Subject: [PATCH 035/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=B0=D0=BB=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B?= =?UTF-8?q?=20ButtonOut=20=D1=81=20=D1=83=D1=87=D0=B5=D1=82=D0=BE=D0=BC=20?= =?UTF-8?q?=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/ButtonOut/ButtonOut.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modules/exec/ButtonOut/ButtonOut.cpp b/src/modules/exec/ButtonOut/ButtonOut.cpp index 5509d985..cce94d06 100644 --- a/src/modules/exec/ButtonOut/ButtonOut.cpp +++ b/src/modules/exec/ButtonOut/ButtonOut.cpp @@ -14,9 +14,7 @@ class ButtonOut : public IoTItem { jsonRead(parameters, "inv", _inv); IoTgpio.pinMode(_pin, OUTPUT); - //TODO: прочитать состояние из памяти - IoTgpio.digitalWrite(_pin, _inv?HIGH:LOW); // пока нет памяти, устанавливаем значение в ноль - value.valD = 0; + IoTgpio.digitalWrite(_pin, value.valD?HIGH:LOW); } void doByInterval() { From 6f5caec5406fb71fc7228f4ed619ad188b88ce34 Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 20 Oct 2022 11:47:29 +0300 Subject: [PATCH 036/107] =?UTF-8?q?=D0=9E=D1=87=D0=B8=D1=89=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=20=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D0=BB=D0=B8=D1=89=D0=B5?= =?UTF-8?q?=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D1=80=D0=B5=D0=BA=D0=BE=D0=BD=D1=84=D0=B8?= =?UTF-8?q?=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ESPConfiguration.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ESPConfiguration.cpp b/src/ESPConfiguration.cpp index 0a70e979..1ab3a874 100644 --- a/src/ESPConfiguration.cpp +++ b/src/ESPConfiguration.cpp @@ -46,4 +46,6 @@ void clearConfigure() { if (*it) delete *it; } IoTItems.clear(); + + valuesFlashJson.clear(); } \ No newline at end of file From dafe486784935ca2f117109e788b13de8a7c6001 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 21 Oct 2022 08:01:20 +0300 Subject: [PATCH 037/107] =?UTF-8?q?=D0=A3=D0=B1=D0=B8=D1=80=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4=D0=BA=D1=83=20=D0=B2=20?= =?UTF-8?q?=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82=D0=BC=D0=B5=20=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Main.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index 4c7a8dd8..bc5c2142 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -154,8 +154,6 @@ void loop() { if (needSaveValues && millis()%1000 == 0) { syncValuesFlashJson(); needSaveValues = false; - delay(1); - Serial.println("syncValuesFlashJson()"); } } From ccfb04d4a7cd5a8e92dd9f60b1496e683e76f511 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 21 Oct 2022 08:03:06 +0300 Subject: [PATCH 038/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=BD=D1=8B=D0=BC=D0=B8=20=D1=81?= =?UTF-8?q?=D0=B2=D0=B5=D1=82=D0=BE=D0=B4=D0=B8=D0=BE=D0=B4=D0=B0=D0=BC?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 19 +++ myProfile.json | 4 + platformio.ini | 2 + src/modules/API.cpp | 2 + src/modules/display/Ws2812b/Ws2181b.cpp | 198 +++++++++++++++++++++++ src/modules/display/Ws2812b/modinfo.json | 96 +++++++++++ 6 files changed, 321 insertions(+) create mode 100644 src/modules/display/Ws2812b/Ws2181b.cpp create mode 100644 src/modules/display/Ws2812b/modinfo.json diff --git a/data_svelte/items.json b/data_svelte/items.json index 7ec9b010..4275b34b 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -652,5 +652,24 @@ "coord": "0,0", "id2show": "id датчика", "num": 48 + }, + { + "name": "49. Strip ws2812b", + "type": "Reading", + "subtype": "Ws2812b", + "id": "strip", + "widget": "range", + "page": "Кнопки", + "descr": "Лента", + "int": 15, + "needSave": 0, + "pin": "4", + "numLeds": "8", + "brightness": "100", + "mode": "1", + "min": "15", + "max": "30", + "idshow": "t", + "num": 49 } ] \ No newline at end of file diff --git a/myProfile.json b/myProfile.json index 3c0ddde3..695b2faf 100644 --- a/myProfile.json +++ b/myProfile.json @@ -198,6 +198,10 @@ { "path": "src/modules/display/Lcd2004", "active": true + }, + { + "path": "src/modules/display/Ws2812b", + "active": true } ] } diff --git a/platformio.ini b/platformio.ini index 311b037d..fc750421 100644 --- a/platformio.ini +++ b/platformio.ini @@ -156,6 +156,7 @@ lib_deps = dfrobot/DFRobotDFPlayerMini @ ^1.0.5 adafruit/Adafruit BusIO @ ^1.13.2 marcoschwartz/LiquidCrystal_I2C@^1.1.4 + adafruit/Adafruit NeoPixel@^1.10.6 build_src_filter = + + @@ -187,6 +188,7 @@ build_src_filter = + + + + + [env:esp32_4mb_fromitems] lib_deps = diff --git a/src/modules/API.cpp b/src/modules/API.cpp index ab864765..7523aff7 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -30,6 +30,7 @@ void* getAPI_Pcf8574(String subtype, String params); void* getAPI_Pwm8266(String subtype, String params); void* getAPI_TelegramLT(String subtype, String params); void* getAPI_Lcd2004(String subtype, String params); +void* getAPI_Ws2812b(String subtype, String params); void* getAPI(String subtype, String params) { void* tmpAPI; @@ -63,5 +64,6 @@ if ((tmpAPI = getAPI_Pcf8574(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pwm8266(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_TelegramLT(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Lcd2004(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Ws2812b(subtype, params)) != nullptr) return tmpAPI; return nullptr; } \ No newline at end of file diff --git a/src/modules/display/Ws2812b/Ws2181b.cpp b/src/modules/display/Ws2812b/Ws2181b.cpp new file mode 100644 index 00000000..8cd90a29 --- /dev/null +++ b/src/modules/display/Ws2812b/Ws2181b.cpp @@ -0,0 +1,198 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include "ESPConfiguration.h" +#include + + +class Ws2812b : public IoTItem +{ +private: + Adafruit_NeoPixel *_strip; + int _pin; + int _numLeds; + int _brightness; + int correctLed; + int _min = 0; + int _max = 100; + int PrevValidShow = 0; + int FlagFN = 1; + int PreFlagFN = 1; + String idshow; + +public: + Ws2812b(String parameters) : IoTItem(parameters) { + jsonRead(parameters, F("pin"), _pin); + jsonRead(parameters, F("numLeds"), _numLeds); + jsonRead(parameters, F("idshow"), idshow); + jsonRead(parameters, F("brightness"), _brightness); + jsonRead(parameters, F("min"), _min); + jsonRead(parameters, F("max"), _max); + + + _strip = new Adafruit_NeoPixel(_numLeds, _pin, NEO_GRB + NEO_KHZ800); + if (_strip != nullptr) { + _strip->begin(); + SerialPrint("E", "Strip Ws2812b:" + _id, "begin"); + correctLed = correctPixel(_numLeds); + _strip->setBrightness(_brightness); + _strip->clear(); + } + } + + // void loop() { + // if (enableDoByInt) { + // currentMillis = millis(); + // difference = currentMillis - prevMillis; + // if (difference >= interval || PreFlagFN != FlagFN) { + // prevMillis = millis(); + // this->doByInterval(); + // } + // } + // } + + void doByInterval() { + if (!_strip) return; + + if (!isItemExist(idshow)) { + SerialPrint("E", F("Ws2812b"), "'" + idshow + "' detector object not exist"); + }else if (getItemValue(idshow) == ""){ + SerialPrint("E", F("Ws2812b"), "'" + idshow + "' detector value is empty"); + }else if (_min >= _max){ + SerialPrint("E", F("Ws2812b"), " the minimum (" + String(_min) + ") value must be greater than the maximum (" + String(_max) + ")"); + }else if(isItemExist(idshow) && getItemValue(idshow) != "" && _min < _max && FlagFN == 1){ + SerialPrint("E", "Ws2812b:" + String(correctLed), " work"); + String value = getItemValue(idshow); + if(PrevValidShow == 0 || PrevValidShow > value.toInt() ){ + noShow(); + } + int t = map(value.toInt(), _min, _max, 0, _numLeds); + for(uint16_t L = 0; LsetPixelColor(L,wheel(((205+(L*correctLed)) & 255))); + } + PrevValidShow = value.toInt(); + _strip->show(); + } + } + + int correctPixel(int _numLeds){ + if(_numLeds <= 65 && _numLeds > 60){ + correctLed = 0; + }else if(_numLeds <= 60 && _numLeds > 55){ + correctLed = 1; + }else if(_numLeds <= 55 && _numLeds > 50){ + correctLed = 2; + }else if(_numLeds <= 50 && _numLeds > 40){ + correctLed = 3; + }else if(_numLeds <= 40 && _numLeds > 35){ + correctLed = 4; + }else if(_numLeds <= 35 && _numLeds > 24){ + correctLed = 5; + }else if(_numLeds <= 24 && _numLeds > 16){ + correctLed = 6; + }else if(_numLeds <= 16 && _numLeds > 12){ + correctLed = 8; + }else if(_numLeds <= 12 && _numLeds > 8){ + correctLed = 12; + }else if(_numLeds <= 8 && _numLeds > 4){ + correctLed = 16; + }else{ + correctLed = 0; + } + return correctLed; + } + + uint32_t wheel(byte WheelPos) { + if(WheelPos < 85) { + return _strip->Color(WheelPos * 3, 255 - WheelPos * 3, 0); + } + else if(WheelPos < 205) { + WheelPos -= 85; + return _strip->Color(255 - WheelPos * 3, 0, WheelPos * 3); + } + else { + WheelPos -= 205; + return _strip->Color(0, WheelPos * 3, 255 - WheelPos * 3); + } + } + + void noShow(){ + if (!_strip) return; + + _strip->clear(); + for(int i=0; i<_numLeds; i++) { + _strip->setPixelColor(i, _strip->Color(0, 0, 0)); + _strip->show(); + } + } + + IoTValue execute(String command, std::vector ¶m) { + if (!_strip) return {}; + + if (command == "test") { + for(int i=0; i<_numLeds; i++) { + _strip->setPixelColor(i, _strip->Color(20+(i*2), 20+(i*2), 20+(i*2))); + _strip->show(); + } + SerialPrint("E", "Strip Ws2812b", "demo"); + } else if (command == "noShow"){ + noShow(); + SerialPrint("E", "Strip Ws2812b", "noShow"); + } else if(command == "noShowOne"){ + if (param.size() == 1) { + _strip->setPixelColor(param[0].valD, _strip->Color(0, 0, 0)); + _strip->show(); + SerialPrint("E", "Strip Ws2812b", "noShowOne"); + } + } else if (command == "showLed"){ + if (param.size() == 4) { + _strip->setPixelColor( param[0].valD, _strip->Color(param[1].valD, param[2].valD, param[3].valD)); + _strip->show(); + SerialPrint("E", "Strip Ws2812b", "showLed:" + param[0].valS + " red:" + param[1].valS + " green:" + param[2].valS + " blue:" + param[3].valS); + } + } else if (command == "showLedAll"){ + if (param.size() == 3) { + for(int i=0; i<_numLeds; i++) { + _strip->setPixelColor(i, _strip->Color(param[0].valD, param[1].valD, param[2].valD)); + _strip->show(); + } + SerialPrint("E", "Strip Ws2812b", "showLedAll - red:" + param[0].valS + " green:" + param[1].valS + " blue:" + param[2].valS); + } + } else if (command == "disableIndication"){ + FlagFN = 0; + PreFlagFN = 1; + SerialPrint("E", "Strip Ws2812b", "disableIndication"); + } else if (command == "enableIndication"){ + FlagFN = 1; + PreFlagFN = 0; + doByInterval(); + SerialPrint("E", "Strip Ws2812b", "enableIndication"); + } + doByInterval(); + return {}; + } + + void setValue(IoTValue Value, bool generateEvent = true){ + if (!_strip) return; + + value = Value; + int b = map(value.valD, 1,1024,1,255); + _strip->setBrightness(b); + _strip->show(); + regEvent(value.valD, "Ws2812b"); + } + + ~Ws2812b(){}; +}; + +void *getAPI_Ws2812b(String subtype, String param) +{ + if (subtype == F("Ws2812b")) { + return new Ws2812b(param); + } else { + return nullptr; + } +} + + + + diff --git a/src/modules/display/Ws2812b/modinfo.json b/src/modules/display/Ws2812b/modinfo.json new file mode 100644 index 00000000..280d3e26 --- /dev/null +++ b/src/modules/display/Ws2812b/modinfo.json @@ -0,0 +1,96 @@ +{ + "menuSection": "Экраны", + "configItem": [ + { + "name": "Strip ws2812b", + "type": "Reading", + "subtype": "Ws2812b", + "id": "strip", + "widget": "range", + "page": "Кнопки", + "descr": "Лента", + "int": 15, + "needSave": 0, + "pin": "4", + "numLeds": "8", + "brightness": "100", + "mode": "1", + "min": "15", + "max": "30", + "idshow": "t" + }], + + "about": { + "authorName": "Yuriy Kuneev", + "authorContact": "https://t.me/Kuneev07", + "authorGit": "", + "exampleURL": "https://iotmanager.org/wiki", + "specialThanks": "", + "moduleName": "Ws2812b", + "moduleVersion": "1.0.1", + "moduleDesc": "Позволяет визуализировать наполнение бака или температуру нагрева. В зависимост от показаний которые везуализируем нужно редактировать min и max.", + "propInfo": { + "int": "Период времени в секундах обновления.", + "pin": "Пин к которому подключена лента.", + "numLeds": "Количество пикселей в ленте.", + "needSave": "Запись яркости в энергонезависимую память", + "brightness": "Яркость ленты можно менять из сценария.", + "min": "Минимальный порог индикатора на который реагировать.", + "max": "Максимальный порог индикатора на который реагировать.", + "idshow": "id элемента конфигурации который нужно повесить индикацию." + }, + "funcInfo": [ + { + "name": "noShow", + "descr": "Выключить ленту", + "params": ["номер пикселя"] + }, + { + "name": "noShowOne", + "descr": "Выключить один светодиод на ленте", + "params": [] + }, + { + "name": "test", + "descr": "для проверки всех светодиодов ленты", + "params": [] + }, + { + "name": "showLed", + "descr": "Зажечь один диод", + "params": ["номер пикселя","цвет 255,255,255 или red,green"] + }, + { + "name": "showLedAll", + "descr": "Зажечь все диоды", + "params": ["Цвет красного светодиода от 0 до 255","Цвет зеленого светодиода от 0 до 255","Цвет синего светодиода от 0 до 255"] + }, + { + "name": "Brightness", + "descr": "Устанавливает общую яркость ленты от 0 до 255", + "params": ["яркость от 0 до 255"] + }, + { + "name": "enableIndication", + "descr": "Включает работу индикации по idshow по дэфолту включено всегда", + "params": [] + }, + { + "name": "disableIndication", + "descr": "Выключает работу индикации по idshow", + "params": [] + } + ] + }, + + "defActive": true, + + "usedLibs": { + "esp32_4mb": [ + "adafruit/Adafruit NeoPixel@^1.10.6" + ], + "esp8266_4mb": [ + "adafruit/Adafruit NeoPixel@^1.10.6" + ] + } +} \ No newline at end of file From 55c0cfd718195662f79b2c27c25caa8b05927909 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 07:59:26 +0300 Subject: [PATCH 039/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=B0=D0=BB=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B?= =?UTF-8?q?=20=D1=81=20=D0=BD=D0=B0=D1=81=D0=BB=D0=B5=D0=B4=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC=D0=B8=20setValue=20=D0=B2=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/display/Ws2812b/Ws2181b.cpp | 2 +- src/modules/exec/ButtonIn/ButtonIn.cpp | 2 +- src/modules/exec/ButtonOut/ButtonOut.cpp | 2 +- src/modules/exec/IoTServo/IoTServo.cpp | 2 +- src/modules/exec/Pwm32/Pwm32.cpp | 2 +- src/modules/exec/Pwm8266/Pwm8266.cpp | 2 +- src/modules/virtual/Loging/Loging.cpp | 2 +- src/modules/virtual/VButton/VButton.cpp | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/display/Ws2812b/Ws2181b.cpp b/src/modules/display/Ws2812b/Ws2181b.cpp index 8cd90a29..9ddac234 100644 --- a/src/modules/display/Ws2812b/Ws2181b.cpp +++ b/src/modules/display/Ws2812b/Ws2181b.cpp @@ -178,7 +178,7 @@ public: int b = map(value.valD, 1,1024,1,255); _strip->setBrightness(b); _strip->show(); - regEvent(value.valD, "Ws2812b"); + if (generateEvent) regEvent(value.valD, "Ws2812b"); } ~Ws2812b(){}; diff --git a/src/modules/exec/ButtonIn/ButtonIn.cpp b/src/modules/exec/ButtonIn/ButtonIn.cpp index 6d2fe354..e968eb18 100644 --- a/src/modules/exec/ButtonIn/ButtonIn.cpp +++ b/src/modules/exec/ButtonIn/ButtonIn.cpp @@ -70,7 +70,7 @@ class ButtonIn : public IoTItem { void setValue(IoTValue Value, bool generateEvent = true) { value = Value; - regEvent((String)(int)value.valD, "ButtonIn"); + if (generateEvent) regEvent((String)(int)value.valD, "ButtonIn"); } String getValue() { diff --git a/src/modules/exec/ButtonOut/ButtonOut.cpp b/src/modules/exec/ButtonOut/ButtonOut.cpp index cce94d06..d041f748 100644 --- a/src/modules/exec/ButtonOut/ButtonOut.cpp +++ b/src/modules/exec/ButtonOut/ButtonOut.cpp @@ -40,7 +40,7 @@ class ButtonOut : public IoTItem { void setValue(IoTValue Value, bool generateEvent = true) { value = Value; IoTgpio.digitalWrite(_pin, _inv?!value.valD:value.valD); - regEvent((String)(int)value.valD, "ButtonOut"); + if (generateEvent) regEvent((String)(int)value.valD, "ButtonOut"); } String getValue() { diff --git a/src/modules/exec/IoTServo/IoTServo.cpp b/src/modules/exec/IoTServo/IoTServo.cpp index 00c68f23..d995918b 100644 --- a/src/modules/exec/IoTServo/IoTServo.cpp +++ b/src/modules/exec/IoTServo/IoTServo.cpp @@ -57,7 +57,7 @@ class IoTServo : public IoTItem { if (value.isDecimal & (_oldValue != value.valD)) { _oldValue = value.valD; servObj.write(_oldValue); - regEvent(value.valD, "IoTServo"); + if (generateEvent) regEvent(value.valD, "IoTServo"); } } diff --git a/src/modules/exec/Pwm32/Pwm32.cpp b/src/modules/exec/Pwm32/Pwm32.cpp index f6351c47..6ad209e8 100644 --- a/src/modules/exec/Pwm32/Pwm32.cpp +++ b/src/modules/exec/Pwm32/Pwm32.cpp @@ -57,7 +57,7 @@ class Pwm32 : public IoTItem { void setValue(IoTValue Value, bool generateEvent = true) { value = Value; ledcWrite(_ledChannel, value.valD); - regEvent(value.valD, "Pwm32"); + if (generateEvent) regEvent(value.valD, "Pwm32"); } //======================================================================================================= diff --git a/src/modules/exec/Pwm8266/Pwm8266.cpp b/src/modules/exec/Pwm8266/Pwm8266.cpp index e52ab9a9..73c4493d 100644 --- a/src/modules/exec/Pwm8266/Pwm8266.cpp +++ b/src/modules/exec/Pwm8266/Pwm8266.cpp @@ -47,7 +47,7 @@ class Pwm8266 : public IoTItem { void setValue(IoTValue Value, bool generateEvent = true) { value = Value; IoTgpio.analogWrite(_pin, value.valD); - regEvent(value.valD, "Pwm8266"); + if (generateEvent) regEvent(value.valD, "Pwm8266"); } //======================================================================================================= diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index 11193b85..d9ccb976 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -313,7 +313,7 @@ class Date : public IoTItem { void setValue(IoTValue Value, bool generateEvent = true) { value = Value; - regEvent(value.valS, ""); + if (generateEvent) regEvent(value.valS, ""); //отправка данных при изменении даты for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getSubtype() == "Loging") { diff --git a/src/modules/virtual/VButton/VButton.cpp b/src/modules/virtual/VButton/VButton.cpp index ad0bfcd4..44f846ad 100644 --- a/src/modules/virtual/VButton/VButton.cpp +++ b/src/modules/virtual/VButton/VButton.cpp @@ -8,7 +8,7 @@ class VButton : public IoTItem { void setValue(IoTValue Value, bool generateEvent = true) { value = Value; - regEvent((String)(int)value.valD, "VButton"); + if (generateEvent) regEvent((String)(int)value.valD, "VButton"); } String getValue() { From 538257208d3a5e9f3c7453dad9d7520dcfe8f164 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 08:00:48 +0300 Subject: [PATCH 040/107] =?UTF-8?q?=D0=92=D0=BA=D0=BB=D1=8E=D1=87=D0=B0?= =?UTF-8?q?=D0=B5=D0=BC=20=D1=81=D0=B5=D1=82=D0=B5=D0=B2=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D0=B8=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=B1=D0=B0=D0=B7=D0=B5=20MQTT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MqttClient.cpp | 50 ++++++++++++++++++++++------------------- src/classes/IoTItem.cpp | 18 +++++++-------- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index 90691b43..72884352 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -161,29 +161,33 @@ void mqttCallback(char* topic, uint8_t* payload, size_t length) { else if (topicStr.indexOf("event") != -1) { //пока не работает сетевой обмен этот код будет закомментирован - // if (!jsonReadBool(settingsFlashJson, "mqttin")) { - // return; - // } - // if (topicStr.indexOf(chipId) == -1) { - // String devId = selectFromMarkerToMarker(topicStr, "/", 2); - // String id = selectFromMarkerToMarker(topicStr, "/", 3); - // IoTItem* itemExist = findIoTItem(id); - // if (itemExist) { - // String valAsStr; - // if (jsonRead(payloadStr, F("val"), valAsStr, false)) { - // itemExist->setValue(valAsStr); - // unsigned long interval; - // jsonRead(payloadStr, F("int"), interval); - // itemExist->setInterval(interval); - // } - // } else { - // //добавим событие в базу - // // IoTItems.push_back((IoTItem*)new externalVariable(payloadStr)); - // } - // //запустим проверку его в сценариях - // generateEvent(id, payloadStr); - // SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + payloadStr); - // } + if (!jsonReadBool(settingsFlashJson, "mqttin")) { + return; + } + if (topicStr.indexOf(chipId) == -1) { + String devId = selectFromMarkerToMarker(topicStr, "/", 2); + String id = selectFromMarkerToMarker(topicStr, "/", 3); + String valAsStr; + jsonRead(payloadStr, F("val"), valAsStr, false); + + IoTItem* itemExist = findIoTItem(id); + if (itemExist) { + unsigned long interval; + jsonRead(payloadStr, F("int"), interval); + itemExist->setInterval(interval); + itemExist->setValue(valAsStr, false); + } else { + //добавим событие в базу + itemExist = (IoTItem*)new externalVariable(payloadStr); + IoTItems.push_back(itemExist); + } + //запустим проверку его в сценариях + generateEvent(id, valAsStr); + publishStatusMqtt(id, valAsStr); + publishStatusWs(id, valAsStr); + //itemExist->regEvent(valAsStr, ""); + SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + valAsStr); + } } //здесь мы получаем прямые команды которые сразу выполнятся на этом устройстве diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 02061f9d..0c50e7f0 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -64,7 +64,7 @@ void IoTItem::setValue(String valStr, bool generateEvent) { } else { value.valS = valStr; } - if (generateEvent) setValue(value, generateEvent); + setValue(value, generateEvent); } // @@ -96,14 +96,14 @@ void IoTItem::regEvent(String value, String consoleInfo = "") { // SerialPrint("i", F("=>ALLMQTT"), "Broadcast event: "); // } //отправка события другим устройствам в сети============================== - // if (jsonReadBool(settingsFlashJson, "mqttin")) { - // String json = "{}"; - // jsonWriteStr_(json, "id", _id); - // jsonWriteStr_(json, "val", value); - // jsonWriteInt_(json, "int", _interval + 5000); // 5 секунд про запас - // publishEvent(_id, json); - // SerialPrint("i", F("<=MQTT"), "Broadcast event: " + json); - //} + if (jsonReadBool(settingsFlashJson, "mqttin")) { + String json = "{}"; + jsonWriteStr_(json, "id", _id); + jsonWriteStr_(json, "val", value); + jsonWriteInt_(json, "int", _interval/1000 + 5); // 5 секунд про запас + publishEvent(_id, json); + SerialPrint("i", F("<=MQTT"), "Broadcast event: " + json); + } //======================================================================== } From b89610ae0aeead98873e9ca9422a4b6b361fe927 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 08:01:33 +0300 Subject: [PATCH 041/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D1=87=D0=B8=D1=81=D0=B5=D0=BB=20=D0=B2=20mqttPu?= =?UTF-8?q?b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/classes/IoTScenario.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index 5d44c832..0779d816 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -411,7 +411,9 @@ IoTValue sysExecute(SysOp command, std::vector ¶m) { case sysop_mqttPub: if (param.size() == 2) { // Serial.printf("Call from sysExecute %s %s\n", param[0].valS.c_str(), param[1].valS.c_str()); - value.valD = mqtt.publish(param[0].valS.c_str(), param[1].valS.c_str(), false); + String tmpStr = param[1].valS; + if (param[1].isDecimal) tmpStr = param[1].valD; + value.valD = mqtt.publish(param[0].valS.c_str(), tmpStr.c_str(), false); } break; case sysop_getUptime: From c403755dd500f16650d697f3f996178c95b8c323 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 11:34:52 +0300 Subject: [PATCH 042/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20RSS?= =?UTF-8?q?I=20=D0=B2=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/classes/IoTScenario.cpp | 7 +++++++ src/modules/sceninfo.json | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index 0779d816..cb60fde6 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -321,6 +321,7 @@ enum SysOp { sysop_gethhmm, sysop_gethhmmss, sysop_getTime, + sysop_getRSSI, sysop_getIP, sysop_mqttPub, sysop_getUptime @@ -404,6 +405,10 @@ IoTValue sysExecute(SysOp command, std::vector ¶m) { #endif } break; + case sysop_getRSSI: + value.valD = WiFi.RSSI(); + value.isDecimal = true; + break; case sysop_getIP: value.valS = jsonReadStr(settingsFlashJson, F("ip")); value.isDecimal = false; @@ -460,6 +465,8 @@ class SysCallExprAST : public ExprAST { operation = sysop_getMonth; else if (Callee == "getDay") operation = sysop_getDay; + else if (Callee == "getRSSI") + operation = sysop_getRSSI; else if (Callee == "getIP") operation = sysop_getIP; else if (Callee == "mqttPub") diff --git a/src/modules/sceninfo.json b/src/modules/sceninfo.json index b358033e..6f26cc47 100644 --- a/src/modules/sceninfo.json +++ b/src/modules/sceninfo.json @@ -44,6 +44,11 @@ "descr": "Погрузить ESP в глубокий сон. Вывод из сна с перезагрузкой. Для ESP8266 необходимо соединить gpio 16 и RST", "params": ["ЧислоСекунд"] }, + { + "name": "getRSSI", + "descr": "Получить величину уровня принимаемого сигнала WI-FI.", + "params": [] + }, { "name": "getIP", "descr": "Получить строку IP ESP", From 1501b5660ec8a82961235b9ca938fbab6ef8a2ca Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 11:35:21 +0300 Subject: [PATCH 043/107] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BD=D0=B5=D1=81=D1=83=D1=89=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B5=D0=BC=20subType,=20=D1=82.?= =?UTF-8?q?=D0=BA.=20=D0=BF=D1=80=D0=B8=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BA=D0=B5=20=D0=BF=D0=BE=20=D1=81=D0=B5=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BE=D0=BD=20=D0=BE=D1=82=D1=81=D1=83=D1=82=D1=81=D1=82=D0=B2?= =?UTF-8?q?=D1=83=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/classes/IoTItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 0c50e7f0..88794444 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -10,7 +10,7 @@ IoTItem::IoTItem(String parameters) { jsonRead(parameters, F("int"), _interval); if (_interval == 0) enableDoByInt = false; _interval = _interval * 1000; - jsonRead(parameters, F("subtype"), _subtype); + jsonRead(parameters, F("subtype"), _subtype, false); jsonRead(parameters, F("id"), _id); if (!jsonRead(parameters, F("multiply"), _multiply, false)) _multiply = 1; if (!jsonRead(parameters, F("plus"), _plus, false)) _plus = 0; From d2485228eb2a07cd8efe16437fe1a07764049bc0 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 14:13:23 +0300 Subject: [PATCH 044/107] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D1=80=D0=B5=D1=88?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D1=83=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20=D0=B2=20?= =?UTF-8?q?=D1=81=D0=B5=D1=82=D1=8C=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20=D1=81=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=BE=D0=BC=20global=20=3D=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 2 +- src/classes/IoTItem.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index 62884956..4979e796 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -69,7 +69,7 @@ class IoTItem { IoTItem* findIoTItem(String name); // поиск экземпляра элемента модуля по имени String getItemValue(String name); // поиск плюс получение значения bool isItemExist(String name); // существует ли айтем -StaticJsonDocument* getLocalItemsAsJSON(); // сбор всех локальных занчений Items +StaticJsonDocument* getLocalItemsAsJSON(); // сбор всех локальных значений Items class externalVariable : IoTItem { // объект, создаваемый при получении информации о событии на другом контроллере для хранения информации о событии указанное время diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 88794444..dcd3701e 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -96,7 +96,7 @@ void IoTItem::regEvent(String value, String consoleInfo = "") { // SerialPrint("i", F("=>ALLMQTT"), "Broadcast event: "); // } //отправка события другим устройствам в сети============================== - if (jsonReadBool(settingsFlashJson, "mqttin")) { + if (jsonReadBool(settingsFlashJson, "mqttin") && _global) { String json = "{}"; jsonWriteStr_(json, "id", _id); jsonWriteStr_(json, "val", value); From 728fcb385e647951382055836aed28f361d75a45 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 22:49:18 +0300 Subject: [PATCH 045/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=BE=D1=82=20?= =?UTF-8?q?=D0=BB=D1=8E=D0=B1=D1=8B=D1=85=20=D1=81=D0=B8=D0=BC=D0=B2=D0=BE?= =?UTF-8?q?=D0=BB=D0=BE=D0=B2=20=D0=BA=D1=80=D0=BE=D0=BC=D0=B5=20=D0=B0?= =?UTF-8?q?=D0=BB=D1=84=D0=B0=D0=B2=D0=B8=D1=82=D0=B0=20=D0=B8=20=D1=86?= =?UTF-8?q?=D0=B8=D1=84=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/StringUtils.cpp | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/utils/StringUtils.cpp b/src/utils/StringUtils.cpp index a8d4ee3b..eb869812 100644 --- a/src/utils/StringUtils.cpp +++ b/src/utils/StringUtils.cpp @@ -128,18 +128,18 @@ size_t itemsCount2(String str, const String& separator) { return cnt; } -size_t itemsCount(String& str, const char* delim) { - size_t cnt = 0; - char* cstr = new char[str.length() + 1]; - strcpy(cstr, str.c_str()); - char* token; - while ((token = strtok_r(cstr, delim, &cstr))) { - cnt++; - // printf("%s\n", token); - } - delete[] cstr; - return cnt; -} +// size_t itemsCount(String& str, const char* delim) { +// size_t cnt = 0; +// char* cstr = new char[str.length() + 1]; +// strcpy(cstr, str.c_str()); +// char* token; +// while ((token = strtok_r(cstr, delim, &cstr))) { +// cnt++; +// // printf("%s\n", token); +// } +// delete[] cstr; +// return cnt; +// } char* stringToChar(String& str) { char* mychar = new char[str.length() + 1]; @@ -198,3 +198,12 @@ String uint64ToString(uint64_t input, uint8_t base) { } while (input); return result; } + +String cleanString(String str) { + String clearStr = ""; + const String allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя.!-+ "; + for (size_t i = 0; i < str.length(); i++) { + if (allowedChars.indexOf(str.charAt(i)) != -1) clearStr += str.charAt(i); + } + return clearStr; +} \ No newline at end of file From 480e20a62e01034aaca511fea68ce16ebc38b6d5 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 22:50:40 +0300 Subject: [PATCH 046/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20=D0=B2?= =?UTF-8?q?=D0=BD=D1=83=D1=82=D1=80=D0=B8=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BF=D0=B5=D1=87=D0=B0=D1=82=D0=B8=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/SerialPrint.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/utils/SerialPrint.cpp b/src/utils/SerialPrint.cpp index 6287029c..22e8b96c 100644 --- a/src/utils/SerialPrint.cpp +++ b/src/utils/SerialPrint.cpp @@ -1,14 +1,23 @@ #include "utils/SerialPrint.h" -void SerialPrint(String errorLevel, String module, String msg) { - String tosend; - - tosend = prettyMillis(millis()); - +void SerialPrint(String errorLevel, String module, String msg, String itemId) { + String tosend = prettyMillis(millis()); tosend = tosend + " [" + errorLevel + "] [" + module + "] " + msg; Serial.println(tosend); + if (errorLevel == "E") { + msg = cleanString(msg); + // создаем событие об ошибке для возможной реакции в сценарии + if (itemId != "") { + IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"" + itemId + "_onError\",\"val\":\"" + msg + "\",\"int\":1}")); + generateEvent(itemId + "_onError", "1"); + } else { + IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"onError\",\"val\":\"" + module + " " + msg + "\",\"int\":1}")); + generateEvent("onError", "1"); + } + } + if (isNetworkActive()) { if (jsonReadInt(settingsFlashJson, F("log")) != 0) { // String pl = "/log|" + tosend; From e5a6a7a0c439fa98a998d1fd4299c57c76ecc4ae Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 22:53:49 +0300 Subject: [PATCH 047/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=20=D0=BD=D0=B5=D0=BD=D0=B0=D0=B9=D0=B4?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D0=98=D0=94=20=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=B1?= =?UTF-8?q?=D0=B0=D0=B3=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/classes/IoTScenario.cpp | 150 ++++++++++++++++++------------------ 1 file changed, 77 insertions(+), 73 deletions(-) diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index cb60fde6..7964852a 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -104,6 +104,7 @@ class VariableExprAST : public ExprAST { return &(Item->value); } + SerialPrint("E", Name, "Элемент не найден или соединение потеряно", Name); return nullptr; // Item не найден. } }; @@ -145,90 +146,92 @@ class BinaryExprAST : public ExprAST { if (RHS == nullptr || LHS == nullptr) return nullptr; IoTValue *rhs = RHS->exec(); // получаем значение правого операнда для возможного использования в операции присваивания + if (rhs == nullptr) return nullptr; if (Op == '=' && LHS->setValue(rhs)) { // если установка значения не поддерживается, т.е. слева не переменная, то работаем по другим комбинациям далее return rhs; // иначе возвращаем присвоенное значение справа } IoTValue *lhs = LHS->exec(); // если присваивания не произошло, значит операция иная и необходимо значение левого операнда + if (lhs == nullptr) return nullptr; - if (lhs != nullptr && rhs != nullptr) { - if (lhs->isDecimal && rhs->isDecimal) { - switch (Op) { - case '>': - val.valD = lhs->valD > rhs->valD; - break; - case '<': - val.valD = lhs->valD < rhs->valD; - break; - case tok_lesseq: - val.valD = lhs->valD <= rhs->valD; - break; - case tok_greateq: - val.valD = lhs->valD >= rhs->valD; - break; - case tok_equal: - val.valD = lhs->valD == rhs->valD; - break; - case tok_notequal: - val.valD = lhs->valD != rhs->valD; - break; + + if (lhs->isDecimal && rhs->isDecimal) { + switch (Op) { + case '>': + val.valD = lhs->valD > rhs->valD; + break; + case '<': + val.valD = lhs->valD < rhs->valD; + break; + case tok_lesseq: + val.valD = lhs->valD <= rhs->valD; + break; + case tok_greateq: + val.valD = lhs->valD >= rhs->valD; + break; + case tok_equal: + val.valD = lhs->valD == rhs->valD; + break; + case tok_notequal: + val.valD = lhs->valD != rhs->valD; + break; - case '+': - val.valD = lhs->valD + rhs->valD; - break; - case '-': - val.valD = lhs->valD - rhs->valD; - break; - case '*': - val.valD = lhs->valD * rhs->valD; - break; - case '/': - if (rhs->valD != 0) - val.valD = lhs->valD / rhs->valD; - else - val.valD = 3.4E+38; - break; + case '+': + val.valD = lhs->valD + rhs->valD; + break; + case '-': + val.valD = lhs->valD - rhs->valD; + break; + case '*': + val.valD = lhs->valD * rhs->valD; + break; + case '/': + if (rhs->valD != 0) + val.valD = lhs->valD / rhs->valD; + else + val.valD = 3.4E+38; + break; - case '|': - val.valD = lhs->valD || rhs->valD; - break; - case '&': - val.valD = lhs->valD && rhs->valD; - break; + case '|': + val.valD = lhs->valD || rhs->valD; + break; + case '&': + val.valD = lhs->valD && rhs->valD; + break; - default: - break; - } - return &val; - } - - if (!lhs->isDecimal || !rhs->isDecimal) { - if (lhs->isDecimal) - lhsStr = (String)lhs->valD; - else - lhsStr = lhs->valS; - if (rhs->isDecimal) - rhsStr = (String)rhs->valD; - else - rhsStr = rhs->valS; - switch (Op) { - case tok_equal: - val.valD = compStr(lhsStr, rhsStr); - break; - - case '+': - val.valS = lhsStr + rhsStr; - val.valD = 1; - val.isDecimal = false; - break; - - default: - break; - } - return &val; + default: + break; } + return &val; } + + if (!lhs->isDecimal || !rhs->isDecimal) { + if (lhs->isDecimal) + lhsStr = (String)lhs->valD; + else + lhsStr = lhs->valS; + if (rhs->isDecimal) + rhsStr = (String)rhs->valD; + else + rhsStr = rhs->valS; + switch (Op) { + case tok_equal: + val.valD = compStr(lhsStr, rhsStr); + break; + + case '+': + val.valS = lhsStr + rhsStr; + val.valD = 1; + val.isDecimal = false; + break; + + default: + break; + } + return &val; + } + return &val; } @@ -544,7 +547,8 @@ class IfExprAST : public ExprAST { return nullptr; //&zeroIotVal; } - if (cond_ret->isDecimal && cond_ret->valD) { + // если число больше нуля или строка не равна пустой, то считаем условие выполненным + if (cond_ret->isDecimal && cond_ret->valD || !(cond_ret->isDecimal) && cond_ret->valS != "") { if (Then == nullptr) return nullptr; res_ret = Then->exec(); } else { From dca907dbaa12e971f2ede52ec10ce7df78aaacf6 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 22:54:50 +0300 Subject: [PATCH 048/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B4=D0=BB=D1=8F=20=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D1=85=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/utils/SerialPrint.h | 3 ++- include/utils/StringUtils.h | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/include/utils/SerialPrint.h b/include/utils/SerialPrint.h index 62efd173..ee3a2d82 100644 --- a/include/utils/SerialPrint.h +++ b/include/utils/SerialPrint.h @@ -1,5 +1,6 @@ #pragma once #include "Global.h" #include "utils/TimeUtils.h" +#include "classes/IoTItem.h" -void SerialPrint(String errorLevel, String module, String msg); \ No newline at end of file +void SerialPrint(String errorLevel, String module, String msg, String itemId = ""); \ No newline at end of file diff --git a/include/utils/StringUtils.h b/include/utils/StringUtils.h index da9468cb..9f2f9d75 100644 --- a/include/utils/StringUtils.h +++ b/include/utils/StringUtils.h @@ -32,7 +32,7 @@ size_t itemsCount2(String str, const String& separator); char* stringToChar(String& str); -size_t itemsCount(String& str, const char* delim); +//size_t itemsCount(String& str, const char* delim); boolean isDigitStr(const String& str); @@ -41,3 +41,5 @@ boolean isDigitDotCommaStr(const String& str); String prettyBytes(size_t size); String uint64ToString(uint64_t input, uint8_t base = 10); + +String cleanString(String str); From 01d6b0904a394b43288228d3fcbe29456d4561d4 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 22:55:50 +0300 Subject: [PATCH 049/107] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20=D0=BE=D1=81=D1=82=D0=B0=D1=82=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=B5=D0=B1=D0=B0=D0=B3=D0=B8=D0=BD=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/classes/IoTItem.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index dcd3701e..074d7526 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -134,7 +134,6 @@ void IoTItem::doByInterval() {} IoTValue IoTItem::execute(String command, std::vector& param) { return {}; } -//захрена эта хрень? - самому пригодилась сорян Илья String IoTItem::getSubtype() { return _subtype; } @@ -166,7 +165,7 @@ IoTGpio* IoTItem::getGpioDriver() { externalVariable::externalVariable(String parameters) : IoTItem(parameters) { prevMillis = millis(); // запоминаем текущее значение таймера для выполения doByInterval после int сек iAmLocal = false; // указываем, что это сущность прилетела из сети - Serial.printf("Call from externalVariable: parameters %s %d\n", parameters.c_str(), _interval); + //Serial.printf("Call from externalVariable: parameters %s %d\n", parameters.c_str(), _interval); } externalVariable::~externalVariable() { From cf041494c4aab41568870632f3be465e4d6bf031 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 22 Oct 2022 23:10:09 +0300 Subject: [PATCH 050/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B4=D0=BE=D0=BF=20=D0=B8=D0=BD=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BC=D0=B5=D0=BD=D1=8B=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B4=D1=80=D1=83=D0=B3=D1=83=D1=8E=20=D0=95?= =?UTF-8?q?=D0=A1=D0=9F=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=B1=D0=BE=D0=B5=20?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 4 ++-- src/classes/IoTItem.cpp | 10 +++++----- src/modules/virtual/Loging/Loging.cpp | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index 4979e796..00d4a68c 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -16,8 +16,8 @@ class IoTItem { virtual void doByInterval(); virtual IoTValue execute(String command, std::vector& param); - virtual void regEvent(String value, String consoleInfo); - virtual void regEvent(float value, String consoleInfo); + virtual void regEvent(String value, String consoleInfo, bool error = false); + virtual void regEvent(float value, String consoleInfo, bool error = false); String getSubtype(); diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 074d7526..37a59850 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -79,7 +79,7 @@ void IoTItem::setValue(IoTValue Value, bool generateEvent) { } //когда событие случилось -void IoTItem::regEvent(String value, String consoleInfo = "") { +void IoTItem::regEvent(String value, String consoleInfo, bool error) { if (_needSave) { jsonWriteStr_(valuesFlashJson, _id, value); needSaveValues = true; @@ -95,8 +95,8 @@ void IoTItem::regEvent(String value, String consoleInfo = "") { // if (_global) { // SerialPrint("i", F("=>ALLMQTT"), "Broadcast event: "); // } - //отправка события другим устройствам в сети============================== - if (jsonReadBool(settingsFlashJson, "mqttin") && _global) { + //отправка события другим устройствам в сети если не было ошибки============================== + if (jsonReadBool(settingsFlashJson, "mqttin") && _global && !error) { String json = "{}"; jsonWriteStr_(json, "id", _id); jsonWriteStr_(json, "val", value); @@ -120,14 +120,14 @@ String IoTItem::getRoundValue() { } } -void IoTItem::regEvent(float regvalue, String consoleInfo = "") { +void IoTItem::regEvent(float regvalue, String consoleInfo, bool error) { value.valD = regvalue; if (_multiply) value.valD = value.valD * _multiply; if (_plus) value.valD = value.valD + _plus; if (_map1 != _map2) value.valD = map(value.valD, _map1, _map2, _map3, _map4); - regEvent(getRoundValue(), consoleInfo); + regEvent(getRoundValue(), consoleInfo, error); } void IoTItem::doByInterval() {} diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index d9ccb976..d9f5fdde 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -263,7 +263,7 @@ class Loging : public IoTItem { } } - void regEvent(String value, String consoleInfo = "") { + void regEvent(String value, String consoleInfo, bool error = false) { String userDate = getItemValue(id + "-date"); String currentDate = getTodayDateDotFormated(); //отправляем в график данные только когда выбран сегодняшний день From 08db9bd9e3b276c2cbce49b4ee97ab4a8bcaf821 Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 23 Oct 2022 11:19:41 +0300 Subject: [PATCH 051/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=20global=20=D0=B2=D0=BE=20=D0=B2=D1=81=D0=B5=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/display/Lcd2004/modinfo.json | 1 + src/modules/display/Ws2812b/modinfo.json | 3 ++- src/modules/exec/ButtonIn/modinfo.json | 1 + src/modules/exec/ButtonOut/modinfo.json | 1 + src/modules/exec/EspCam/modinfo.json | 1 + src/modules/exec/IoTServo/modinfo.json | 1 + src/modules/exec/Mcp23017/modinfo.json | 1 + src/modules/exec/Mp3/modinfo.json | 1 + src/modules/exec/Pcf8574/modinfo.json | 1 + src/modules/exec/Pwm32/modinfo.json | 1 + src/modules/exec/Pwm8266/modinfo.json | 1 + src/modules/exec/SDcard/modinfo.json | 1 + src/modules/exec/SysExt/modinfo.json | 1 + src/modules/exec/Telegram/modinfo.json | 1 + src/modules/exec/TelegramLT/modinfo.json | 1 + src/modules/sensors/Ads1115/modinfo.json | 1 + src/modules/sensors/AhtXX/modinfo.json | 2 ++ src/modules/sensors/AnalogAdc/modinfo.json | 1 + src/modules/sensors/Bme280/modinfo.json | 3 +++ src/modules/sensors/Bmp280/modinfo.json | 2 ++ src/modules/sensors/Dht1122/modinfo.json | 2 ++ src/modules/sensors/Ds18b20/modinfo.json | 1 + src/modules/sensors/Emon/modinfo.json | 2 ++ src/modules/sensors/FreqMeter/modinfo.json | 3 +++ src/modules/sensors/GY21/modinfo.json | 2 ++ src/modules/sensors/Hdc1080/modinfo.json | 2 ++ src/modules/sensors/IoTWiegand/modinfo.json | 1 + src/modules/sensors/Max6675/modinfo.json | 1 + src/modules/sensors/Mhz19/modinfo.json | 3 +++ src/modules/sensors/Pzem004t/modinfo.json | 6 ++++++ src/modules/sensors/RCswitch/modinfo.json | 1 + src/modules/sensors/Sds011/modinfo.json | 2 ++ src/modules/sensors/Sht20/modinfo.json | 2 ++ src/modules/sensors/Sht30/modinfo.json | 2 ++ src/modules/sensors/Sonar/modinfo.json | 1 + src/modules/sensors/UART/modinfo.json | 1 + src/modules/virtual/Loging/modinfo.json | 1 + src/modules/virtual/LogingDaily/modinfo.json | 1 + src/modules/virtual/Timer/modinfo.json | 1 + src/modules/virtual/VButton/modinfo.json | 1 + src/modules/virtual/Variable/modinfo.json | 4 ++++ 41 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/modules/display/Lcd2004/modinfo.json b/src/modules/display/Lcd2004/modinfo.json index 0061faf4..914d9047 100644 --- a/src/modules/display/Lcd2004/modinfo.json +++ b/src/modules/display/Lcd2004/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Экраны", "configItem": [{ + "global": 0, "name": "LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", diff --git a/src/modules/display/Ws2812b/modinfo.json b/src/modules/display/Ws2812b/modinfo.json index 280d3e26..b7565646 100644 --- a/src/modules/display/Ws2812b/modinfo.json +++ b/src/modules/display/Ws2812b/modinfo.json @@ -1,7 +1,8 @@ { "menuSection": "Экраны", "configItem": [ - { + { + "global": 0, "name": "Strip ws2812b", "type": "Reading", "subtype": "Ws2812b", diff --git a/src/modules/exec/ButtonIn/modinfo.json b/src/modules/exec/ButtonIn/modinfo.json index 7ab286b7..df7c6a63 100644 --- a/src/modules/exec/ButtonIn/modinfo.json +++ b/src/modules/exec/ButtonIn/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [ { + "global": 0, "name": "Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", diff --git a/src/modules/exec/ButtonOut/modinfo.json b/src/modules/exec/ButtonOut/modinfo.json index abfdac70..c47c3bf5 100644 --- a/src/modules/exec/ButtonOut/modinfo.json +++ b/src/modules/exec/ButtonOut/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [ { + "global": 0, "name": "Управление пином", "type": "Writing", "subtype": "ButtonOut", diff --git a/src/modules/exec/EspCam/modinfo.json b/src/modules/exec/EspCam/modinfo.json index 1cb8f2d9..ec35a2a2 100644 --- a/src/modules/exec/EspCam/modinfo.json +++ b/src/modules/exec/EspCam/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "Camera OV2640 (ESPcam)", "type": "Reading", "subtype": "EspCam", diff --git a/src/modules/exec/IoTServo/modinfo.json b/src/modules/exec/IoTServo/modinfo.json index 89c28a02..b9a16e1b 100644 --- a/src/modules/exec/IoTServo/modinfo.json +++ b/src/modules/exec/IoTServo/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "Сервопривод", "type": "Writing", "subtype": "IoTServo", diff --git a/src/modules/exec/Mcp23017/modinfo.json b/src/modules/exec/Mcp23017/modinfo.json index 7311e462..3c63dad8 100644 --- a/src/modules/exec/Mcp23017/modinfo.json +++ b/src/modules/exec/Mcp23017/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", diff --git a/src/modules/exec/Mp3/modinfo.json b/src/modules/exec/Mp3/modinfo.json index 7b9dd98b..efd4f6f8 100644 --- a/src/modules/exec/Mp3/modinfo.json +++ b/src/modules/exec/Mp3/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "MP3 плеер", "type": "Reading", "subtype": "Mp3", diff --git a/src/modules/exec/Pcf8574/modinfo.json b/src/modules/exec/Pcf8574/modinfo.json index 8e95b19e..7f1f3817 100644 --- a/src/modules/exec/Pcf8574/modinfo.json +++ b/src/modules/exec/Pcf8574/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", diff --git a/src/modules/exec/Pwm32/modinfo.json b/src/modules/exec/Pwm32/modinfo.json index 37a60ff8..dda8786b 100644 --- a/src/modules/exec/Pwm32/modinfo.json +++ b/src/modules/exec/Pwm32/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "PWM ESP32", "type": "Writing", "subtype": "Pwm32", diff --git a/src/modules/exec/Pwm8266/modinfo.json b/src/modules/exec/Pwm8266/modinfo.json index e5a5f478..494a2fbf 100644 --- a/src/modules/exec/Pwm8266/modinfo.json +++ b/src/modules/exec/Pwm8266/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", diff --git a/src/modules/exec/SDcard/modinfo.json b/src/modules/exec/SDcard/modinfo.json index 4f6b4867..ef917963 100644 --- a/src/modules/exec/SDcard/modinfo.json +++ b/src/modules/exec/SDcard/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "SD карта", "type": "Writing", "subtype": "SDcard", diff --git a/src/modules/exec/SysExt/modinfo.json b/src/modules/exec/SysExt/modinfo.json index 72d394c3..3f4313dc 100644 --- a/src/modules/exec/SysExt/modinfo.json +++ b/src/modules/exec/SysExt/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "Доп. функции системы", "type": "Reading", "subtype": "SysExt", diff --git a/src/modules/exec/Telegram/modinfo.json b/src/modules/exec/Telegram/modinfo.json index 9104bb52..b28a2478 100644 --- a/src/modules/exec/Telegram/modinfo.json +++ b/src/modules/exec/Telegram/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Исполнительные устройства", "configItem": [{ + "global": 0, "name": "Телеграм-Бот", "type": "Writing", "subtype": "Telegram", diff --git a/src/modules/exec/TelegramLT/modinfo.json b/src/modules/exec/TelegramLT/modinfo.json index 95414c98..28bcade9 100644 --- a/src/modules/exec/TelegramLT/modinfo.json +++ b/src/modules/exec/TelegramLT/modinfo.json @@ -3,6 +3,7 @@ "configItem": [ { + "global": 0, "name": "Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", diff --git a/src/modules/sensors/Ads1115/modinfo.json b/src/modules/sensors/Ads1115/modinfo.json index bf406e26..6395fbff 100644 --- a/src/modules/sensors/Ads1115/modinfo.json +++ b/src/modules/sensors/Ads1115/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "ADS1115 Напряжение", "type": "Reading", "subtype": "Ads1115", diff --git a/src/modules/sensors/AhtXX/modinfo.json b/src/modules/sensors/AhtXX/modinfo.json index 87527032..7b180bec 100644 --- a/src/modules/sensors/AhtXX/modinfo.json +++ b/src/modules/sensors/AhtXX/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "AHTXX Температура", "type": "Reading", "subtype": "AhtXXt", @@ -15,6 +16,7 @@ "round": 1 }, { + "global": 0, "name": "AHTXX Влажность", "type": "Reading", "subtype": "AhtXXh", diff --git a/src/modules/sensors/AnalogAdc/modinfo.json b/src/modules/sensors/AnalogAdc/modinfo.json index 55ab6360..31b99f0a 100644 --- a/src/modules/sensors/AnalogAdc/modinfo.json +++ b/src/modules/sensors/AnalogAdc/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [{ + "global": 0, "name": "Аналоговый сенсор", "type": "Reading", "subtype": "AnalogAdc", diff --git a/src/modules/sensors/Bme280/modinfo.json b/src/modules/sensors/Bme280/modinfo.json index 62d8d17c..d2b05beb 100644 --- a/src/modules/sensors/Bme280/modinfo.json +++ b/src/modules/sensors/Bme280/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "BME280 Температура", "type": "Reading", "subtype": "Bme280t", @@ -14,6 +15,7 @@ "round": 1 }, { + "global": 0, "name": "BME280 Давление", "type": "Reading", "subtype": "Bme280p", @@ -26,6 +28,7 @@ "round": 1 }, { + "global": 0, "name": "BME280 Влажность", "type": "Reading", "subtype": "Bme280h", diff --git a/src/modules/sensors/Bmp280/modinfo.json b/src/modules/sensors/Bmp280/modinfo.json index 50b84a82..4bfb5c27 100644 --- a/src/modules/sensors/Bmp280/modinfo.json +++ b/src/modules/sensors/Bmp280/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "BMP280 Температура", "type": "Reading", "subtype": "Bmp280t", @@ -14,6 +15,7 @@ "round": 1 }, { + "global": 0, "name": "BMP280 Давление", "type": "Reading", "subtype": "Bmp280p", diff --git a/src/modules/sensors/Dht1122/modinfo.json b/src/modules/sensors/Dht1122/modinfo.json index 85089458..7e5dc5be 100644 --- a/src/modules/sensors/Dht1122/modinfo.json +++ b/src/modules/sensors/Dht1122/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "DHT11 Температура", "type": "Reading", "subtype": "Dht1122t", @@ -14,6 +15,7 @@ "senstype": "dht11" }, { + "global": 0, "name": "DHT11 Влажность", "type": "Reading", "subtype": "Dht1122h", diff --git a/src/modules/sensors/Ds18b20/modinfo.json b/src/modules/sensors/Ds18b20/modinfo.json index 96f6af73..708708ba 100644 --- a/src/modules/sensors/Ds18b20/modinfo.json +++ b/src/modules/sensors/Ds18b20/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "DS18B20 Температура", "type": "Reading", "subtype": "Ds18b20", diff --git a/src/modules/sensors/Emon/modinfo.json b/src/modules/sensors/Emon/modinfo.json index 9f4086a8..e254359f 100644 --- a/src/modules/sensors/Emon/modinfo.json +++ b/src/modules/sensors/Emon/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "EMON Ток", "type": "Reading", "subtype": "I", @@ -16,6 +17,7 @@ "multiply": 1 }, { + "global": 0, "name": "EMON Напряжение", "type": "Reading", "subtype": "U", diff --git a/src/modules/sensors/FreqMeter/modinfo.json b/src/modules/sensors/FreqMeter/modinfo.json index 2bc0ac8d..91eb8c82 100644 --- a/src/modules/sensors/FreqMeter/modinfo.json +++ b/src/modules/sensors/FreqMeter/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "Частотомер на ADC, Частота", "type": "Reading", "subtype": "FreqMeterF", @@ -18,6 +19,7 @@ "int": 5 }, { + "global": 0, "name": "Частотомер на ADC, Процент Пульсации", "type": "Reading", "subtype": "FreqMeterPcFl", @@ -34,6 +36,7 @@ "int": 5 }, { + "global": 0, "name": "Частотомер на ADC, Индекс Пульсации", "type": "Reading", "subtype": "FreqMeterFlIn", diff --git a/src/modules/sensors/GY21/modinfo.json b/src/modules/sensors/GY21/modinfo.json index d81dd21c..7878e037 100644 --- a/src/modules/sensors/GY21/modinfo.json +++ b/src/modules/sensors/GY21/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "GY21 Температура", "type": "Reading", "subtype": "GY21t", @@ -13,6 +14,7 @@ "int": 15 }, { + "global": 0, "name": "GY21 Влажность", "type": "Reading", "subtype": "GY21h", diff --git a/src/modules/sensors/Hdc1080/modinfo.json b/src/modules/sensors/Hdc1080/modinfo.json index 7a873738..3250c7a9 100644 --- a/src/modules/sensors/Hdc1080/modinfo.json +++ b/src/modules/sensors/Hdc1080/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", @@ -14,6 +15,7 @@ "round": 1 }, { + "global": 0, "name": "HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", diff --git a/src/modules/sensors/IoTWiegand/modinfo.json b/src/modules/sensors/IoTWiegand/modinfo.json index 6e375f55..ff7457e2 100644 --- a/src/modules/sensors/IoTWiegand/modinfo.json +++ b/src/modules/sensors/IoTWiegand/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "Wiegand Считыватель", "type": "Reading", "subtype": "IoTWiegand", diff --git a/src/modules/sensors/Max6675/modinfo.json b/src/modules/sensors/Max6675/modinfo.json index 8e091483..2523a69d 100644 --- a/src/modules/sensors/Max6675/modinfo.json +++ b/src/modules/sensors/Max6675/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", diff --git a/src/modules/sensors/Mhz19/modinfo.json b/src/modules/sensors/Mhz19/modinfo.json index 57b00496..638fe131 100644 --- a/src/modules/sensors/Mhz19/modinfo.json +++ b/src/modules/sensors/Mhz19/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "MHZ-19 CO2 UART", "type": "Reading", "subtype": "Mhz19uart", @@ -20,6 +21,7 @@ "ABC": 1 }, { + "global": 0, "name": "MHZ-19 CO2 PWM", "type": "Reading", "subtype": "Mhz19pwm", @@ -36,6 +38,7 @@ "int": 300 }, { + "global": 0, "name": "MHZ-19 Температура UART", "type": "Reading", "subtype": "Mhz19temp", diff --git a/src/modules/sensors/Pzem004t/modinfo.json b/src/modules/sensors/Pzem004t/modinfo.json index 3d75cce0..d5a31eaf 100644 --- a/src/modules/sensors/Pzem004t/modinfo.json +++ b/src/modules/sensors/Pzem004t/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", @@ -14,6 +15,7 @@ "round": 1 }, { + "global": 0, "name": "PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", @@ -26,6 +28,7 @@ "round": 1 }, { + "global": 0, "name": "PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", @@ -38,6 +41,7 @@ "round": 1 }, { + "global": 0, "name": "PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", @@ -50,6 +54,7 @@ "round": 1 }, { + "global": 0, "name": "PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", @@ -62,6 +67,7 @@ "round": 1 }, { + "global": 0, "name": "PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", diff --git a/src/modules/sensors/RCswitch/modinfo.json b/src/modules/sensors/RCswitch/modinfo.json index 916aa31d..f5a6dc97 100644 --- a/src/modules/sensors/RCswitch/modinfo.json +++ b/src/modules/sensors/RCswitch/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [{ + "global": 0, "name": "Сканер кнопок 433 MHz", "num": 31, "type": "Reading", diff --git a/src/modules/sensors/Sds011/modinfo.json b/src/modules/sensors/Sds011/modinfo.json index 81a7eca6..8fd8eb80 100644 --- a/src/modules/sensors/Sds011/modinfo.json +++ b/src/modules/sensors/Sds011/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "SDS011 PM25 Пыль", "type": "Reading", "subtype": "Sds011_25", @@ -21,6 +22,7 @@ "retryDelayMs": 5 }, { + "global": 0, "name": "SDS011 PM10 Пыль", "type": "Reading", "subtype": "Sds011_10", diff --git a/src/modules/sensors/Sht20/modinfo.json b/src/modules/sensors/Sht20/modinfo.json index 6d8d745d..797390dc 100644 --- a/src/modules/sensors/Sht20/modinfo.json +++ b/src/modules/sensors/Sht20/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "Sht20 Температура", "type": "Reading", "subtype": "Sht20t", @@ -13,6 +14,7 @@ "round": 1 }, { + "global": 0, "name": "Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", diff --git a/src/modules/sensors/Sht30/modinfo.json b/src/modules/sensors/Sht30/modinfo.json index ca722253..df3c8ec7 100644 --- a/src/modules/sensors/Sht30/modinfo.json +++ b/src/modules/sensors/Sht30/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "Sht30 Температура", "type": "Reading", "subtype": "Sht30t", @@ -13,6 +14,7 @@ "round": 1 }, { + "global": 0, "name": "Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", diff --git a/src/modules/sensors/Sonar/modinfo.json b/src/modules/sensors/Sonar/modinfo.json index ce43afcb..be4911de 100644 --- a/src/modules/sensors/Sonar/modinfo.json +++ b/src/modules/sensors/Sonar/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "HC-SR04 Ультразвуковой дальномер", "num": 1, "type": "Reading", diff --git a/src/modules/sensors/UART/modinfo.json b/src/modules/sensors/UART/modinfo.json index 7f0dc67d..ff4f9710 100644 --- a/src/modules/sensors/UART/modinfo.json +++ b/src/modules/sensors/UART/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Сенсоры", "configItem": [ { + "global": 0, "name": "UART", "type": "Reading", "subtype": "UART", diff --git a/src/modules/virtual/Loging/modinfo.json b/src/modules/virtual/Loging/modinfo.json index 826a7b1c..1fb3da7f 100644 --- a/src/modules/virtual/Loging/modinfo.json +++ b/src/modules/virtual/Loging/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Виртуальные элементы", "configItem": [ { + "global": 0, "name": "График", "type": "Writing", "subtype": "Loging", diff --git a/src/modules/virtual/LogingDaily/modinfo.json b/src/modules/virtual/LogingDaily/modinfo.json index 6e5827bd..9688920c 100644 --- a/src/modules/virtual/LogingDaily/modinfo.json +++ b/src/modules/virtual/LogingDaily/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Виртуальные элементы", "configItem": [ { + "global": 0, "name": "График дневного расхода", "type": "Writing", "subtype": "LogingDaily", diff --git a/src/modules/virtual/Timer/modinfo.json b/src/modules/virtual/Timer/modinfo.json index 0485564b..e9379c50 100644 --- a/src/modules/virtual/Timer/modinfo.json +++ b/src/modules/virtual/Timer/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Виртуальные элементы", "configItem": [ { + "global": 0, "name": "Таймер", "type": "Writing", "subtype": "Timer", diff --git a/src/modules/virtual/VButton/modinfo.json b/src/modules/virtual/VButton/modinfo.json index 80ed2d53..05c5dcd9 100644 --- a/src/modules/virtual/VButton/modinfo.json +++ b/src/modules/virtual/VButton/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Виртуальные элементы", "configItem": [ { + "global": 0, "name": "Виртуальная кнопка", "type": "Reading", "subtype": "VButton", diff --git a/src/modules/virtual/Variable/modinfo.json b/src/modules/virtual/Variable/modinfo.json index 940ca4c0..5bbc918a 100644 --- a/src/modules/virtual/Variable/modinfo.json +++ b/src/modules/virtual/Variable/modinfo.json @@ -2,6 +2,7 @@ "menuSection": "Виртуальные элементы", "configItem": [ { + "global": 0, "name": "Окно ввода числа (переменная)", "type": "Reading", "subtype": "Variable", @@ -15,6 +16,7 @@ "num": 2 }, { + "global": 0, "name": "Окно ввода времени", "type": "Reading", "subtype": "Variable", @@ -28,6 +30,7 @@ "num": 3 }, { + "global": 0, "name": "Окно ввода даты", "type": "Reading", "subtype": "Variable", @@ -41,6 +44,7 @@ "num": 4 }, { + "global": 0, "name": "Окно ввода текста", "type": "Reading", "subtype": "Variable", From a8642ac915154bfc57dfc2e2fa803d37271673be Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 23 Oct 2022 11:20:23 +0300 Subject: [PATCH 052/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D1=80=20global=20=D0=B2=D0=BE=20=D0=B2=D1=81=D0=B5=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/data_svelte/items.json b/data_svelte/items.json index 4275b34b..435182da 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -7,6 +7,7 @@ "header": "Виртуальные элементы" }, { + "global": 0, "name": "1. График", "type": "Writing", "subtype": "Loging", @@ -20,6 +21,7 @@ "points": 300 }, { + "global": 0, "name": "2. График дневного расхода", "type": "Writing", "subtype": "LogingDaily", @@ -34,6 +36,7 @@ "test": 0 }, { + "global": 0, "name": "3. Таймер", "type": "Writing", "subtype": "Timer", @@ -49,6 +52,7 @@ "num": 3 }, { + "global": 0, "name": "4. Окно ввода числа (переменная)", "type": "Reading", "subtype": "Variable", @@ -62,6 +66,7 @@ "num": 4 }, { + "global": 0, "name": "5. Окно ввода времени", "type": "Reading", "subtype": "Variable", @@ -75,6 +80,7 @@ "num": 5 }, { + "global": 0, "name": "6. Окно ввода даты", "type": "Reading", "subtype": "Variable", @@ -88,6 +94,7 @@ "num": 6 }, { + "global": 0, "name": "7. Окно ввода текста", "type": "Reading", "subtype": "Variable", @@ -101,6 +108,7 @@ "num": 7 }, { + "global": 0, "name": "8. Виртуальная кнопка", "type": "Reading", "subtype": "VButton", @@ -130,6 +138,7 @@ "num": 9 }, { + "global": 0, "name": "10. AHTXX Температура", "type": "Reading", "subtype": "AhtXXt", @@ -144,6 +153,7 @@ "num": 10 }, { + "global": 0, "name": "11. AHTXX Влажность", "type": "Reading", "subtype": "AhtXXh", @@ -158,6 +168,7 @@ "num": 11 }, { + "global": 0, "name": "12. Аналоговый сенсор", "type": "Reading", "subtype": "AnalogAdc", @@ -175,6 +186,7 @@ "num": 12 }, { + "global": 0, "name": "13. BME280 Температура", "type": "Reading", "subtype": "Bme280t", @@ -188,6 +200,7 @@ "num": 13 }, { + "global": 0, "name": "14. BME280 Давление", "type": "Reading", "subtype": "Bme280p", @@ -201,6 +214,7 @@ "num": 14 }, { + "global": 0, "name": "15. BME280 Влажность", "type": "Reading", "subtype": "Bme280h", @@ -214,6 +228,7 @@ "num": 15 }, { + "global": 0, "name": "16. BMP280 Температура", "type": "Reading", "subtype": "Bmp280t", @@ -227,6 +242,7 @@ "num": 16 }, { + "global": 0, "name": "17. BMP280 Давление", "type": "Reading", "subtype": "Bmp280p", @@ -240,6 +256,7 @@ "num": 17 }, { + "global": 0, "name": "18. DHT11 Температура", "type": "Reading", "subtype": "Dht1122t", @@ -253,6 +270,7 @@ "num": 18 }, { + "global": 0, "name": "19. DHT11 Влажность", "type": "Reading", "subtype": "Dht1122h", @@ -266,6 +284,7 @@ "num": 19 }, { + "global": 0, "name": "20. DS18B20 Температура", "type": "Reading", "subtype": "Ds18b20", @@ -281,6 +300,7 @@ "num": 20 }, { + "global": 0, "name": "21. GY21 Температура", "type": "Reading", "subtype": "GY21t", @@ -293,6 +313,7 @@ "num": 21 }, { + "global": 0, "name": "22. GY21 Влажность", "type": "Reading", "subtype": "GY21h", @@ -305,6 +326,7 @@ "num": 22 }, { + "global": 0, "name": "23. HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", @@ -318,6 +340,7 @@ "num": 23 }, { + "global": 0, "name": "24. HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", @@ -331,6 +354,7 @@ "num": 24 }, { + "global": 0, "name": "25. MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", @@ -345,6 +369,7 @@ "num": 25 }, { + "global": 0, "name": "26. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", @@ -358,6 +383,7 @@ "num": 26 }, { + "global": 0, "name": "27. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", @@ -371,6 +397,7 @@ "num": 27 }, { + "global": 0, "name": "28. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", @@ -384,6 +411,7 @@ "num": 28 }, { + "global": 0, "name": "29. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", @@ -397,6 +425,7 @@ "num": 29 }, { + "global": 0, "name": "30. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", @@ -410,6 +439,7 @@ "num": 30 }, { + "global": 0, "name": "31. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", @@ -423,6 +453,7 @@ "num": 31 }, { + "global": 0, "name": "32. Сканер кнопок 433 MHz", "num": 32, "type": "Reading", @@ -433,6 +464,7 @@ "pinTx": 12 }, { + "global": 0, "name": "33. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", @@ -445,6 +477,7 @@ "num": 33 }, { + "global": 0, "name": "34. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", @@ -457,6 +490,7 @@ "num": 34 }, { + "global": 0, "name": "35. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", @@ -469,6 +503,7 @@ "num": 35 }, { + "global": 0, "name": "36. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", @@ -481,6 +516,7 @@ "num": 36 }, { + "global": 0, "name": "37. HC-SR04 Ультразвуковой дальномер", "num": 37, "type": "Reading", @@ -494,6 +530,7 @@ "int": 5 }, { + "global": 0, "name": "38. UART", "type": "Reading", "subtype": "UART", @@ -510,6 +547,7 @@ "header": "Исполнительные устройства" }, { + "global": 0, "name": "39. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", @@ -527,6 +565,7 @@ "num": 39 }, { + "global": 0, "name": "40. Управление пином", "type": "Writing", "subtype": "ButtonOut", @@ -541,6 +580,7 @@ "num": 40 }, { + "global": 0, "name": "41. Сервопривод", "type": "Writing", "subtype": "IoTServo", @@ -555,6 +595,7 @@ "num": 41 }, { + "global": 0, "name": "42. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", @@ -568,6 +609,7 @@ "num": 42 }, { + "global": 0, "name": "43. MP3 плеер", "type": "Reading", "subtype": "Mp3", @@ -581,6 +623,7 @@ "num": 43 }, { + "global": 0, "name": "44. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", @@ -594,6 +637,7 @@ "num": 44 }, { + "global": 0, "name": "45. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", @@ -609,6 +653,7 @@ "num": 45 }, { + "global": 0, "name": "46. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", @@ -624,6 +669,7 @@ "header": "Экраны" }, { + "global": 0, "name": "47. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", @@ -654,6 +700,7 @@ "num": 48 }, { + "global": 0, "name": "49. Strip ws2812b", "type": "Reading", "subtype": "Ws2812b", From b347c5c0fb2879b2c24af5697a8c947ea1149326 Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 27 Oct 2022 10:50:09 +0300 Subject: [PATCH 053/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=B1=D0=B5=D1=81=D1=88=D1=83=D0=BC=D0=BD?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=BF=D1=80=D0=B8=D1=81=D0=B2=D0=BE=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20:=3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTScenario.h | 2 +- src/classes/IoTScenario.cpp | 27 ++++++++++++++++++++++----- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/include/classes/IoTScenario.h b/include/classes/IoTScenario.h index 83a47531..9e14a92c 100644 --- a/include/classes/IoTScenario.h +++ b/include/classes/IoTScenario.h @@ -7,7 +7,7 @@ class ExprAST { public: virtual ~ExprAST(); virtual IoTValue *exec(); - virtual int setValue(IoTValue *val); // ret 0 - установка значения не поддерживается наследником + virtual int setValue(IoTValue *val, bool generateEvent); // ret 0 - установка значения не поддерживается наследником virtual bool hasEventIdName(String eventIdName); }; diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index 7964852a..bb0a53b2 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -21,6 +21,7 @@ enum Token { tok_notequal = -9, tok_lesseq = -10, tok_greateq = -11, + tok_silentset = -12, // управление tok_if = -6, @@ -35,7 +36,7 @@ enum Token { /// ExprAST - Базовый класс для всех узлов выражений. ExprAST::~ExprAST() {} IoTValue *ExprAST::exec() { return nullptr; } -int ExprAST::setValue(IoTValue *val) { return 0; } // 0 - установка значения не поддерживается наследником +int ExprAST::setValue(IoTValue *val, bool generateEvent) { return 0; } // 0 - установка значения не поддерживается наследником bool ExprAST::hasEventIdName(String eventIdName) { return false; } // по умолчанию все узлы не связаны с ИД события, для которого выполняется сценарий // struct IoTValue zeroIotVal; @@ -84,10 +85,10 @@ class VariableExprAST : public ExprAST { if (item) ItemIsLocal = item->iAmLocal; } - int setValue(IoTValue *val) { + int setValue(IoTValue *val, bool generateEvent) { if (!ItemIsLocal) Item = findIoTItem(Name); if (Item) - Item->setValue(*val); + Item->setValue(*val, generateEvent); else return 0; @@ -148,7 +149,11 @@ class BinaryExprAST : public ExprAST { IoTValue *rhs = RHS->exec(); // получаем значение правого операнда для возможного использования в операции присваивания if (rhs == nullptr) return nullptr; - if (Op == '=' && LHS->setValue(rhs)) { // если установка значения не поддерживается, т.е. слева не переменная, то работаем по другим комбинациям далее + if (Op == '=' && LHS->setValue(rhs, true)) { // если установка значения не поддерживается, т.е. слева не переменная, то работаем по другим комбинациям далее + return rhs; // иначе возвращаем присвоенное значение справа + } + + if (Op == tok_silentset && LHS->setValue(rhs, false)) { // если установка значения не поддерживается, т.е. слева не переменная, то работаем по другим комбинациям далее return rhs; // иначе возвращаем присвоенное значение справа } @@ -211,10 +216,12 @@ class BinaryExprAST : public ExprAST { lhsStr = (String)lhs->valD; else lhsStr = lhs->valS; + if (rhs->isDecimal) rhsStr = (String)rhs->valD; else rhsStr = rhs->valS; + switch (Op) { case tok_equal: val.valD = compStr(lhsStr, rhsStr); @@ -699,6 +706,15 @@ int IoTScenario::gettok() { return '='; } + if (LastChar == ':') { + LastChar = getLastChar(); + if (LastChar == '=') { + LastChar = getLastChar(); + return tok_silentset; + } else + return ':'; + } + if (LastChar == '!') { LastChar = getLastChar(); if (LastChar == '=') { @@ -1040,7 +1056,8 @@ void IoTScenario::exec(String eventIdName) { // посимвольно счит IoTScenario::IoTScenario() { // Задаём стандартные бинарные операторы. // 1 - наименьший приоритет. - BinopPrecedence['='] = 1; + BinopPrecedence[tok_silentset] = 1; + BinopPrecedence['='] = 2; BinopPrecedence['|'] = 5; BinopPrecedence['&'] = 6; BinopPrecedence[tok_equal] = 10; // == From 8a4ca7a6ae1004876058f6bb1b1c29459346e86c Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 28 Oct 2022 16:21:14 +0300 Subject: [PATCH 054/107] =?UTF-8?q?=D0=A3=D0=B1=D0=B8=D1=80=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D1=83?= =?UTF-8?q?=D1=8E=20=D0=B8=D0=B7=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=20?= =?UTF-8?q?Ads1115?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/Ads1115/Ads1115.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/sensors/Ads1115/Ads1115.cpp b/src/modules/sensors/Ads1115/Ads1115.cpp index 9ac3b98b..b043d8fe 100644 --- a/src/modules/sensors/Ads1115/Ads1115.cpp +++ b/src/modules/sensors/Ads1115/Ads1115.cpp @@ -12,13 +12,12 @@ #include "Wire.h" #include // Библиотека для работы с модулями ADS1115 и ADS1015 -// to do убрать глобальный экземпляр -Adafruit_ADS1115 ads; class Ads1115 : public IoTItem { int _pin; bool _isRaw; bool _isInited = false; + Adafruit_ADS1115 ads; public: Ads1115(String parameters) : IoTItem(parameters) { From 593bebd0deceb2ba0e915dfbb7f9724e8afe3621 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 28 Oct 2022 16:21:49 +0300 Subject: [PATCH 055/107] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=BA=D0=B8=20=D0=B2=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=81=D0=BE=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/classes/IoTScenario.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index bb0a53b2..93534041 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -566,7 +566,7 @@ class IfExprAST : public ExprAST { // if (!res_ret) Serial.printf("Call from IfExprAST: Cond result = %f, no body result\n", cond_ret->valD); // else if (res_ret->isDecimal) Serial.printf("Call from IfExprAST: Cond result = %f, result = %f\n", cond_ret->valD, res_ret->valD); // else Serial.printf("Call from IfExprAST: Cond result = %f, result = %s\n", cond_ret->valD, res_ret->valS.c_str()); - Serial.printf("\n"); + //Serial.printf("\n"); return cond_ret; } From 305a5f5ccbc1ba0706c956e0f12708d81f6c44f9 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 28 Oct 2022 16:22:18 +0300 Subject: [PATCH 056/107] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20Buffers=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB?= =?UTF-8?q?=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/Buffers.h | 12 +++++------ src/Buffers.cpp | 54 +++++++++++++++++++++++------------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/include/Buffers.h b/include/Buffers.h index 78026740..4e08a379 100644 --- a/include/Buffers.h +++ b/include/Buffers.h @@ -1,7 +1,7 @@ -#pragma once -#include "Global.h" -#include "MqttClient.h" +//#pragma once +//#include "Global.h" +//#include "MqttClient.h" -void eventGen2(String eventName, String eventValue); -extern void spaceCmdExecute(String &cmdStr); -extern String getValueJson(String &key); \ No newline at end of file +//void eventGen2(String eventName, String eventValue); +//extern void spaceCmdExecute(String &cmdStr); +//extern String getValueJson(String &key); \ No newline at end of file diff --git a/src/Buffers.cpp b/src/Buffers.cpp index d8338cb2..79d31500 100644 --- a/src/Buffers.cpp +++ b/src/Buffers.cpp @@ -1,35 +1,35 @@ -#include "Buffers.h" +//#include "Buffers.h" //генеирует событие -void eventGen2(String eventName, String eventValue) { - if (!jsonReadBool(settingsFlashJson, "scen")) { - return; - } - String event = eventName + " " + eventValue + ","; - eventBuf += event; +// void eventGen2(String eventName, String eventValue) { +// if (!jsonReadBool(settingsFlashJson, "scen")) { +// return; +// } +// String event = eventName + " " + eventValue + ","; +// eventBuf += event; - SerialPrint("i", "Event add", eventName + " " + eventValue); +// SerialPrint("i", "Event add", eventName + " " + eventValue); - if (jsonReadBool(settingsFlashJson, "MqttOut")) { - if (eventName != "timenow") { - publishEvent(eventName, eventValue); - } - } -} +// if (jsonReadBool(settingsFlashJson, "MqttOut")) { +// if (eventName != "timenow") { +// publishEvent(eventName, eventValue); +// } +// } +// } -void spaceCmdExecute(String& cmdStr) { - cmdStr += "\r\n"; - cmdStr.replace("\r\n", "\n"); - cmdStr.replace("\r", "\n"); - while (cmdStr.length()) { - String buf = selectToMarker(cmdStr, "\n"); - if (buf != "") { - sCmd.readStr(buf); - SerialPrint("i", F("Order done W"), buf); - } - cmdStr = deleteBeforeDelimiter(cmdStr, "\n"); - } -} +// void spaceCmdExecute(String& cmdStr) { +// cmdStr += "\r\n"; +// cmdStr.replace("\r\n", "\n"); +// cmdStr.replace("\r", "\n"); +// while (cmdStr.length()) { +// String buf = selectToMarker(cmdStr, "\n"); +// if (buf != "") { +// sCmd.readStr(buf); +// SerialPrint("i", F("Order done W"), buf); +// } +// cmdStr = deleteBeforeDelimiter(cmdStr, "\n"); +// } +// } // String getValueJson(String& key) { // String live = jsonReadStr(paramsHeapJson, key); From dfd24dc1f66622dad2d38c9a90c4115c29511cdc Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 28 Oct 2022 16:26:13 +0300 Subject: [PATCH 057/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D1=81=D0=BE=D0=BE=D0=B1?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=20=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=B1=D0=BA=D0=B5=20=D0=B2=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D1=8F=D1=85=20=D0=B4=D0=B0=D1=82=D1=87=D0=B8=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20=D0=B3=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BE=D0=B1=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/AhtXX/AhtXX.cpp | 18 +++++++++--------- src/modules/sensors/Bme280/Bme280.cpp | 6 +++--- src/modules/sensors/Bmp280/Bmp280.cpp | 4 ++-- src/modules/sensors/Dht1122/Dht1122.cpp | 4 ++-- src/modules/sensors/Ds18b20/Ds18b20.cpp | 2 +- src/modules/sensors/GY21/GY21.cpp | 4 ++-- src/modules/sensors/Hdc1080/Hdc1080.cpp | 4 ++-- src/modules/sensors/Max6675/Max6675.cpp | 2 +- src/modules/sensors/Sht20/Sht20.cpp | 4 ++-- src/modules/sensors/Sht30/Sht30.cpp | 4 ++-- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/modules/sensors/AhtXX/AhtXX.cpp b/src/modules/sensors/AhtXX/AhtXX.cpp index f32487bf..a49e8fe1 100644 --- a/src/modules/sensors/AhtXX/AhtXX.cpp +++ b/src/modules/sensors/AhtXX/AhtXX.cpp @@ -7,30 +7,30 @@ std::map ahts; -void printStatus(AHTxx *aht) { +String getStatus(AHTxx *aht) { switch (aht->getStatus()) { case AHTXX_NO_ERROR: - Serial.println(F("no error")); + return F("no error"); break; case AHTXX_BUSY_ERROR: - Serial.println(F("sensor AHT busy, increase polling time")); + return F("sensor AHT busy, increase polling time"); break; case AHTXX_ACK_ERROR: - Serial.println(F("sensor AHT didn't return ACK, not connected, broken, long wires (reduce speed), bus locked by slave (increase stretch limit)")); + return F("sensor AHT didn't return ACK, not connected, broken, long wires (reduce speed), bus locked by slave (increase stretch limit)"); break; case AHTXX_DATA_ERROR: - Serial.println(F(" AHT: received data smaller than expected, not connected, broken, long wires (reduce speed), bus locked by slave (increase stretch limit)")); + return F(" AHT: received data smaller than expected, not connected, broken, long wires (reduce speed), bus locked by slave (increase stretch limit)"); break; case AHTXX_CRC8_ERROR: - Serial.println(F("AHT: computed CRC8 not match received CRC8, this feature supported only by AHT2x sensors")); + return F("AHT: computed CRC8 not match received CRC8, this feature supported only by AHT2x sensors"); break; default: - Serial.println(F("AHT: unknown status")); + return F("AHT: unknown status"); break; } } @@ -49,7 +49,7 @@ class AhtXXt : public IoTItem { if (value.valD != AHTXX_ERROR) { regEvent(value.valD, "AhtXXt"); } else { - printStatus(_aht); //print temperature command status + SerialPrint("E", "Sensor AHTXX", getStatus(_aht), _id); } } @@ -70,7 +70,7 @@ class AhtXXh : public IoTItem { if (value.valD != AHTXX_ERROR) { regEvent(value.valD, "AhtXXh"); } else { - printStatus(_aht); //print temperature command status + SerialPrint("E", "Sensor AHTXX", getStatus(_aht), _id); } } diff --git a/src/modules/sensors/Bme280/Bme280.cpp b/src/modules/sensors/Bme280/Bme280.cpp index aa9050c8..071525c4 100644 --- a/src/modules/sensors/Bme280/Bme280.cpp +++ b/src/modules/sensors/Bme280/Bme280.cpp @@ -26,7 +26,7 @@ class Bme280t : public IoTItem { if (value.valD != NAN && value.valD < 145) regEvent(value.valD, "Bme280t"); else - SerialPrint("E", "Sensor Bme280t", "Error"); + SerialPrint("E", "Sensor Bme280t", "Error", _id); } ~Bme280t(){}; @@ -46,7 +46,7 @@ class Bme280h : public IoTItem { if (value.valD != NAN && value.valD < 100) regEvent(value.valD, "Bme280h"); else - SerialPrint("E", "Sensor Bme280h", "Error"); + SerialPrint("E", "Sensor Bme280h", "Error", _id); } ~Bme280h(){}; @@ -67,7 +67,7 @@ class Bme280p : public IoTItem { value.valD = value.valD / 1.333224 / 100; regEvent(value.valD, "Bme280p"); } else - SerialPrint("E", "Sensor Bme280p", "Error"); + SerialPrint("E", "Sensor Bme280p", "Error", _id); } ~Bme280p(){}; diff --git a/src/modules/sensors/Bmp280/Bmp280.cpp b/src/modules/sensors/Bmp280/Bmp280.cpp index fae4ccfa..d0db6beb 100644 --- a/src/modules/sensors/Bmp280/Bmp280.cpp +++ b/src/modules/sensors/Bmp280/Bmp280.cpp @@ -26,7 +26,7 @@ class Bmp280t : public IoTItem { if (value.valD != NAN && value.valD < 150) regEvent(value.valD, "Bmp280t"); else - SerialPrint("E", "Sensor Bmp280t", "Error"); + SerialPrint("E", "Sensor Bmp280t", "Error", _id); } ~Bmp280t(){}; @@ -47,7 +47,7 @@ class Bmp280p : public IoTItem { value.valD = value.valD / 1.333224 / 100; regEvent(value.valD, "Bmp280p"); } else - SerialPrint("E", "Sensor Bmp280p", "Error"); + SerialPrint("E", "Sensor Bmp280p", "Error", _id); } ~Bmp280p(){}; diff --git a/src/modules/sensors/Dht1122/Dht1122.cpp b/src/modules/sensors/Dht1122/Dht1122.cpp index a9c00ef4..00641724 100644 --- a/src/modules/sensors/Dht1122/Dht1122.cpp +++ b/src/modules/sensors/Dht1122/Dht1122.cpp @@ -26,7 +26,7 @@ class Dht1122t : public IoTItem { if (String(value.valD) != "nan") regEvent(value.valD, "Dht1122t"); else - SerialPrint("E", "Sensor DHTt", "Error"); + SerialPrint("E", "Sensor DHTt", "Error", _id); } ~Dht1122t(){}; @@ -46,7 +46,7 @@ class Dht1122h : public IoTItem { if (String(value.valD) != "nan") regEvent(value.valD, "Dht1122h"); else - SerialPrint("E", "Sensor DHTh", "Error"); + SerialPrint("E", "Sensor DHTh", "Error", _id); } ~Dht1122h(){}; diff --git a/src/modules/sensors/Ds18b20/Ds18b20.cpp b/src/modules/sensors/Ds18b20/Ds18b20.cpp index e6518e03..495c3406 100644 --- a/src/modules/sensors/Ds18b20/Ds18b20.cpp +++ b/src/modules/sensors/Ds18b20/Ds18b20.cpp @@ -71,7 +71,7 @@ class Ds18b20 : public IoTItem { if (value.valD != DEVICE_DISCONNECTED_C) regEvent(value.valD, ""); //обязательный вызов для отправки результата работы else - SerialPrint("E", "Sensor Ds18b20", "Error"); + SerialPrint("E", "Sensor Ds18b20", "Error", _id); } //======================================================================================================= diff --git a/src/modules/sensors/GY21/GY21.cpp b/src/modules/sensors/GY21/GY21.cpp index 4c89722e..5af1923b 100644 --- a/src/modules/sensors/GY21/GY21.cpp +++ b/src/modules/sensors/GY21/GY21.cpp @@ -23,7 +23,7 @@ class GY21t : public IoTItem { if (value.valD < 300) regEvent(value.valD, "GY21"); // TODO: найти способ понимания ошибки получения данных else - SerialPrint("E", "Sensor GY21t", "Error"); + SerialPrint("E", "Sensor GY21t", "Error", _id); } ~GY21t(){}; @@ -39,7 +39,7 @@ class GY21h : public IoTItem { if (value.valD != 0) regEvent(value.valD, "GY21h"); // TODO: найти способ понимания ошибки получения данных else - SerialPrint("E", "Sensor GY21h", "Error"); + SerialPrint("E", "Sensor GY21h", "Error", _id); } ~GY21h(){}; diff --git a/src/modules/sensors/Hdc1080/Hdc1080.cpp b/src/modules/sensors/Hdc1080/Hdc1080.cpp index 6af1eff3..fe595787 100644 --- a/src/modules/sensors/Hdc1080/Hdc1080.cpp +++ b/src/modules/sensors/Hdc1080/Hdc1080.cpp @@ -24,7 +24,7 @@ class Hdc1080t : public IoTItem { if (value.valD < 124) regEvent(value.valD, "Hdc1080t"); else - SerialPrint("E", "Sensor Hdc1080t", "Error"); + SerialPrint("E", "Sensor Hdc1080t", "Error", _id); } ~Hdc1080t(){}; @@ -39,7 +39,7 @@ class Hdc1080h : public IoTItem { if (value.valD < 99) regEvent(value.valD, "Hdc1080h"); else - SerialPrint("E", "Sensor Hdc1080h", "Error"); + SerialPrint("E", "Sensor Hdc1080h", "Error", _id); } ~Hdc1080h(){}; diff --git a/src/modules/sensors/Max6675/Max6675.cpp b/src/modules/sensors/Max6675/Max6675.cpp index 67ece7fd..e351602c 100644 --- a/src/modules/sensors/Max6675/Max6675.cpp +++ b/src/modules/sensors/Max6675/Max6675.cpp @@ -29,7 +29,7 @@ class MAX6675t : public IoTItem { if (String(value.valD) != "nan") { regEvent(value.valD, "Max6675t"); } else { - SerialPrint("E", "Sensor Max6675t", "Error"); + SerialPrint("E", "Sensor Max6675t", "Error", _id); } } diff --git a/src/modules/sensors/Sht20/Sht20.cpp b/src/modules/sensors/Sht20/Sht20.cpp index a9100974..85b0e508 100644 --- a/src/modules/sensors/Sht20/Sht20.cpp +++ b/src/modules/sensors/Sht20/Sht20.cpp @@ -16,7 +16,7 @@ class Sht20t : public IoTItem { if (value.valD > -46.85F) regEvent(value.valD, "Sht20t"); else - SerialPrint("E", "Sensor Sht20t", "Error"); + SerialPrint("E", "Sensor Sht20t", "Error", _id); } ~Sht20t(){}; @@ -32,7 +32,7 @@ class Sht20h : public IoTItem { if (value.valD != -6) regEvent(value.valD, "Sht20h"); else - SerialPrint("E", "Sensor Sht20h", "Error"); + SerialPrint("E", "Sensor Sht20h", "Error", _id); } ~Sht20h(){}; diff --git a/src/modules/sensors/Sht30/Sht30.cpp b/src/modules/sensors/Sht30/Sht30.cpp index d0a927b6..d78947df 100644 --- a/src/modules/sensors/Sht30/Sht30.cpp +++ b/src/modules/sensors/Sht30/Sht30.cpp @@ -26,7 +26,7 @@ class Sht30t : public IoTItem { SerialPrint("E", "Sensor Sht30t", "OK"); if (value.valD < -46.85F) regEvent(value.valD, "Sht30t"); // TODO: найти способ понимания ошибки получения данных - else SerialPrint("E", "Sensor Sht30t", "Error"); + else SerialPrint("E", "Sensor Sht30t", "Error", _id); } } ~Sht30t() {}; @@ -42,7 +42,7 @@ class Sht30h : public IoTItem { SerialPrint("E", "Sensor Sht30h", "OK"); if (value.valD != -6) regEvent(value.valD, "Sht30h"); // TODO: найти способ понимания ошибки получения данных - else SerialPrint("E", "Sensor Sht30h", "Error"); + else SerialPrint("E", "Sensor Sht30h", "Error", _id); } } ~Sht30h() {}; From a292f172851117a97ec2ad9f5f907051fd7d17ff Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 28 Oct 2022 17:16:53 +0300 Subject: [PATCH 058/107] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=83=20=D1=81=D0=BE=20=D1=81=D1=82=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D0=B2=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= =?UTF-8?q?=D1=85=20SerialPrint=20=D0=B8=20StringUtils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/utils/SerialPrint.h | 2 +- include/utils/StringUtils.h | 22 +++++++++++----------- src/utils/SerialPrint.cpp | 35 +++++++++++++++++++++-------------- src/utils/StringUtils.cpp | 30 ++++++++++++++---------------- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/include/utils/SerialPrint.h b/include/utils/SerialPrint.h index ee3a2d82..73e6c787 100644 --- a/include/utils/SerialPrint.h +++ b/include/utils/SerialPrint.h @@ -3,4 +3,4 @@ #include "utils/TimeUtils.h" #include "classes/IoTItem.h" -void SerialPrint(String errorLevel, String module, String msg, String itemId = ""); \ No newline at end of file +void SerialPrint(const String& errorLevel, const String& module, const String& msg, const String& itemId = ""); \ No newline at end of file diff --git a/include/utils/StringUtils.h b/include/utils/StringUtils.h index 9f2f9d75..7f5b7f90 100644 --- a/include/utils/StringUtils.h +++ b/include/utils/StringUtils.h @@ -8,29 +8,29 @@ void hex2string(byte array[], unsigned int len, char buffer[]); int string2hex(const char* str, unsigned char* bytes); -uint8_t hexStringToUint8(String hex); +uint8_t hexStringToUint8(const String& hex); -uint16_t hexStringToUint16(String hex); +uint16_t hexStringToUint16(const String& hex); -String selectToMarkerLast(String str, String found); +String selectToMarkerLast(String str, const String& found); -String selectToMarker(String str, String found); +String selectToMarker(String str, const String& found); String extractInner(String str); -String deleteAfterDelimiter(String str, String found); +String deleteAfterDelimiter(String str, const String& found); -String deleteBeforeDelimiter(String str, String found); +String deleteBeforeDelimiter(String str, const String& found); -String deleteBeforeDelimiterTo(String str, String found); +String deleteBeforeDelimiterTo(String str, const String& found); -String deleteToMarkerLast(String str, String found); +String deleteToMarkerLast(String str, const String& found); -String selectFromMarkerToMarker(String str, String found, int number); +String selectFromMarkerToMarker(String str, const String& found, int number); size_t itemsCount2(String str, const String& separator); -char* stringToChar(String& str); +char* stringToChar(const String& str); //size_t itemsCount(String& str, const char* delim); @@ -42,4 +42,4 @@ String prettyBytes(size_t size); String uint64ToString(uint64_t input, uint8_t base = 10); -String cleanString(String str); +void cleanString(String& str); diff --git a/src/utils/SerialPrint.cpp b/src/utils/SerialPrint.cpp index 22e8b96c..3e32547a 100644 --- a/src/utils/SerialPrint.cpp +++ b/src/utils/SerialPrint.cpp @@ -1,23 +1,16 @@ #include "utils/SerialPrint.h" -void SerialPrint(String errorLevel, String module, String msg, String itemId) { +void SerialPrint(const String& errorLevel, const String& module, const String& msg, const String& itemId) { String tosend = prettyMillis(millis()); - tosend = tosend + " [" + errorLevel + "] [" + module + "] " + msg; + tosend += " ["; + tosend += errorLevel; + tosend += "] ["; + tosend += module; + tosend += "] "; + tosend += msg; Serial.println(tosend); - if (errorLevel == "E") { - msg = cleanString(msg); - // создаем событие об ошибке для возможной реакции в сценарии - if (itemId != "") { - IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"" + itemId + "_onError\",\"val\":\"" + msg + "\",\"int\":1}")); - generateEvent(itemId + "_onError", "1"); - } else { - IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"onError\",\"val\":\"" + module + " " + msg + "\",\"int\":1}")); - generateEvent("onError", "1"); - } - } - if (isNetworkActive()) { if (jsonReadInt(settingsFlashJson, F("log")) != 0) { // String pl = "/log|" + tosend; @@ -25,4 +18,18 @@ void SerialPrint(String errorLevel, String module, String msg, String itemId) { sendStringToWs("corelg", tosend, -1); } } + + if (errorLevel == "E") { + cleanString(tosend); + // создаем событие об ошибке для возможной реакции в сценарии + if (itemId != "") { + IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"" + itemId + "_onError\",\"val\":\"" + tosend + "\",\"int\":1}")); + generateEvent(itemId + "_onError", "1"); + } else { + IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"onError\",\"val\":\"" + module + " " + tosend + "\",\"int\":1}")); + generateEvent("onError", "1"); + } + } + + } \ No newline at end of file diff --git a/src/utils/StringUtils.cpp b/src/utils/StringUtils.cpp index eb869812..a2a425d4 100644 --- a/src/utils/StringUtils.cpp +++ b/src/utils/StringUtils.cpp @@ -8,12 +8,12 @@ void writeUint8tToString(uint8_t* payload, size_t length, size_t headerLenth, St } } -String selectToMarkerLast(String str, String found) { +String selectToMarkerLast(String str, const String& found) { int p = str.lastIndexOf(found); return str.substring(p + found.length()); } -String selectToMarker(String str, String found) { +String selectToMarker(String str, const String& found) { int p = str.indexOf(found); return str.substring(0, p); } @@ -24,32 +24,32 @@ String extractInner(String str) { return str.substring(p1 + 1, p2); } -String deleteAfterDelimiter(String str, String found) { +String deleteAfterDelimiter(String str, const String& found) { int p = str.indexOf(found); return str.substring(0, p); } -String deleteBeforeDelimiter(String str, String found) { +String deleteBeforeDelimiter(String str, const String& found) { int p = str.indexOf(found) + found.length(); return str.substring(p); } -String deleteBeforeDelimiterTo(String str, String found) { +String deleteBeforeDelimiterTo(String str, const String& found) { int p = str.indexOf(found); return str.substring(p); } -String deleteToMarkerLast(String str, String found) { +String deleteToMarkerLast(String str, const String& found) { int p = str.lastIndexOf(found); return str.substring(0, p); } -String selectToMarkerPlus(String str, String found, int plus) { +String selectToMarkerPlus(String str, const String& found, int plus) { int p = str.indexOf(found); return str.substring(0, p + plus); } -String selectFromMarkerToMarker(String str, String tofind, int number) { +String selectFromMarkerToMarker(String str, const String& tofind, int number) { if (str.indexOf(tofind) == -1) { return "not found"; } @@ -98,14 +98,14 @@ int string2hex(const char* str, unsigned char* bytes) { return i; } -uint8_t hexStringToUint8(String hex) { +uint8_t hexStringToUint8(const String& hex) { uint8_t tmp = strtol(hex.c_str(), NULL, 0); if (tmp >= 0x00 && tmp <= 0xFF) { return tmp; } } -uint16_t hexStringToUint16(String hex) { +uint16_t hexStringToUint16(const String& hex) { uint16_t tmp = strtol(hex.c_str(), NULL, 0); if (tmp >= 0x0000 && tmp <= 0xFFFF) { return tmp; @@ -141,7 +141,7 @@ size_t itemsCount2(String str, const String& separator) { // return cnt; // } -char* stringToChar(String& str) { +char* stringToChar(const String& str) { char* mychar = new char[str.length() + 1]; strcpy(mychar, str.c_str()); return mychar; @@ -199,11 +199,9 @@ String uint64ToString(uint64_t input, uint8_t base) { return result; } -String cleanString(String str) { - String clearStr = ""; - const String allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя.!-+ "; +void cleanString(String& str) { + const String allowedChars = F("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя.!-+ "); for (size_t i = 0; i < str.length(); i++) { - if (allowedChars.indexOf(str.charAt(i)) != -1) clearStr += str.charAt(i); + if (allowedChars.indexOf(str.charAt(i)) == -1) str.setCharAt(i, ' '); } - return clearStr; } \ No newline at end of file From 7dc21ee91489ae3db90210260f57532b6785d118 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 28 Oct 2022 22:19:18 +0300 Subject: [PATCH 059/107] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=83=20=D1=81=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B0?= =?UTF-8?q?=D1=87=D0=B5=D0=B9=20=D1=81=D1=82=D1=80=D0=BE=D0=BA=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 18 +++++++-------- include/utils/JsonUtils.h | 24 ++++++++++---------- src/classes/IoTItem.cpp | 27 +++++++++-------------- src/modules/exec/ButtonIn/ButtonIn.cpp | 2 +- src/modules/exec/ButtonOut/ButtonOut.cpp | 2 +- src/modules/exec/IoTServo/IoTServo.cpp | 2 +- src/modules/exec/Pwm32/Pwm32.cpp | 2 +- src/modules/exec/Pwm8266/Pwm8266.cpp | 2 +- src/modules/virtual/Loging/Loging.cpp | 6 ++--- src/modules/virtual/VButton/VButton.cpp | 2 +- src/modules/virtual/Variable/Variable.cpp | 2 +- src/utils/JsonUtils.cpp | 24 ++++++++++---------- 12 files changed, 54 insertions(+), 59 deletions(-) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index 00d4a68c..dc42c57a 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -10,14 +10,14 @@ struct IoTValue { class IoTItem { public: - IoTItem(String parameters); + IoTItem(const String ¶meters); virtual ~IoTItem() {} virtual void loop(); virtual void doByInterval(); virtual IoTValue execute(String command, std::vector& param); - virtual void regEvent(String value, String consoleInfo, bool error = false); - virtual void regEvent(float value, String consoleInfo, bool error = false); + virtual void regEvent(const String& value, const String& consoleInfo, bool error = false); + virtual void regEvent(float value, const String& consoleInfo, bool error = false); String getSubtype(); @@ -38,8 +38,8 @@ class IoTItem { bool enableDoByInt = true; virtual IoTGpio* getGpioDriver(); - virtual void setValue(IoTValue Value, bool generateEvent = true); - virtual void setValue(String valStr, bool generateEvent = true); + virtual void setValue(const IoTValue& Value, bool generateEvent = true); + virtual void setValue(const String& valStr, bool generateEvent = true); String getRoundValue(); //методы для графиков @@ -66,15 +66,15 @@ class IoTItem { bool _global = false; // характеристика айтема, что ему нужно слать и принимать события из внешнего мира }; -IoTItem* findIoTItem(String name); // поиск экземпляра элемента модуля по имени -String getItemValue(String name); // поиск плюс получение значения -bool isItemExist(String name); // существует ли айтем +IoTItem* findIoTItem(const String& name); // поиск экземпляра элемента модуля по имени +String getItemValue(const String& name); // поиск плюс получение значения +bool isItemExist(const String& name); // существует ли айтем StaticJsonDocument* getLocalItemsAsJSON(); // сбор всех локальных значений Items class externalVariable : IoTItem { // объект, создаваемый при получении информации о событии на другом контроллере для хранения информации о событии указанное время public: - externalVariable(String parameters); + externalVariable(const String& parameters); ~externalVariable(); void doByInterval(); // для данного класса doByInterval+int выполняет роль счетчика обратного отсчета до уничтожения }; \ No newline at end of file diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h index 3a6912e9..8418163f 100644 --- a/include/utils/JsonUtils.h +++ b/include/utils/JsonUtils.h @@ -10,20 +10,20 @@ extern String jsonWriteInt(String& json, String name, int value, bool e = true); extern String jsonWriteFloat(String& json, String name, float value, bool e = true); extern String jsonWriteBool(String& json, String name, boolean value, bool e = true); -extern bool jsonRead(String& json, String key, unsigned long& value, bool e = true); -extern bool jsonRead(String& json, String key, float& value, bool e = true); -extern bool jsonRead(String& json, String key, String& value, bool e = true); -extern bool jsonRead(String& json, String key, bool& value, bool e = true); -extern bool jsonRead(String& json, String key, int& value, bool e = true); +extern bool jsonRead(const String& json, String key, unsigned long& value, bool e = true); +extern bool jsonRead(const String& json, String key, float& value, bool e = true); +extern bool jsonRead(const String& json, String key, String& value, bool e = true); +extern bool jsonRead(const String& json, String key, bool& value, bool e = true); +extern bool jsonRead(const String& json, String key, int& value, bool e = true); -extern String jsonReadStr(String& json, String name, bool e = true); -extern int jsonReadInt(String& json, String name, bool e = true); -extern boolean jsonReadBool(String& json, String name, bool e = true); +extern String jsonReadStr(const String& json, String name, bool e = true); +extern int jsonReadInt(const String& json, String name, bool e = true); +extern boolean jsonReadBool(const String& json, String name, bool e = true); -extern bool jsonWriteStr_(String& json, String name, String value, bool e = true); -extern bool jsonWriteBool_(String& json, String name, bool value, bool e = true); -extern bool jsonWriteInt_(String& json, String name, int value, bool e = true); -extern bool jsonWriteFloat_(String& json, String name, float value, bool e = true); +extern bool jsonWriteStr_(String& json, const String& name, const String& value, bool e = true); +extern bool jsonWriteBool_(String& json, const String& name, bool value, bool e = true); +extern bool jsonWriteInt_(String& json, const String& name, int value, bool e = true); +extern bool jsonWriteFloat_(String& json, const String& name, float value, bool e = true); void writeUint8tValueToJsonString(uint8_t* payload, size_t length, size_t headerLenth, String& json); extern bool jsonMergeObjects(String& json1, String& json2, bool e = true); extern void jsonMergeDocs(JsonObject dest, JsonObjectConst src); diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 37a59850..c63d12a7 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -6,7 +6,7 @@ #include "EventsAndOrders.h" //получение параметров в экземпляр класса -IoTItem::IoTItem(String parameters) { +IoTItem::IoTItem(const String& parameters) { jsonRead(parameters, F("int"), _interval); if (_interval == 0) enableDoByInt = false; _interval = _interval * 1000; @@ -58,7 +58,7 @@ String IoTItem::getValue() { } //определяем тип прилетевшей величины -void IoTItem::setValue(String valStr, bool generateEvent) { +void IoTItem::setValue(const String& valStr, bool generateEvent) { if (value.isDecimal = isDigitDotCommaStr(valStr)) { value.valD = valStr.toFloat(); } else { @@ -68,7 +68,7 @@ void IoTItem::setValue(String valStr, bool generateEvent) { } // -void IoTItem::setValue(IoTValue Value, bool generateEvent) { +void IoTItem::setValue(const IoTValue& Value, bool generateEvent) { value = Value; if (generateEvent) if (value.isDecimal) { @@ -79,22 +79,17 @@ void IoTItem::setValue(IoTValue Value, bool generateEvent) { } //когда событие случилось -void IoTItem::regEvent(String value, String consoleInfo, bool error) { +void IoTItem::regEvent(const String& value, const String& consoleInfo, bool error) { if (_needSave) { jsonWriteStr_(valuesFlashJson, _id, value); needSaveValues = true; } - - generateEvent(_id, value); publishStatusMqtt(_id, value); - publishStatusWs(_id, value); SerialPrint("i", "Sensor", consoleInfo + " '" + _id + "' data: " + value + "'"); - // проверка если global установлен то шлем всем о событии - // if (_global) { - // SerialPrint("i", F("=>ALLMQTT"), "Broadcast event: "); - // } + generateEvent(_id, value); + //отправка события другим устройствам в сети если не было ошибки============================== if (jsonReadBool(settingsFlashJson, "mqttin") && _global && !error) { String json = "{}"; @@ -120,7 +115,7 @@ String IoTItem::getRoundValue() { } } -void IoTItem::regEvent(float regvalue, String consoleInfo, bool error) { +void IoTItem::regEvent(float regvalue, const String& consoleInfo, bool error) { value.valD = regvalue; if (_multiply) value.valD = value.valD * _multiply; @@ -162,7 +157,7 @@ IoTGpio* IoTItem::getGpioDriver() { //сетевое общение==================================================================================================================================== -externalVariable::externalVariable(String parameters) : IoTItem(parameters) { +externalVariable::externalVariable(const String& parameters) : IoTItem(parameters) { prevMillis = millis(); // запоминаем текущее значение таймера для выполения doByInterval после int сек iAmLocal = false; // указываем, что это сущность прилетела из сети //Serial.printf("Call from externalVariable: parameters %s %d\n", parameters.c_str(), _interval); @@ -181,7 +176,7 @@ void externalVariable::doByInterval() { // для данного класса d IoTItem* myIoTItem; // поиск элемента модуля в существующей конфигурации -IoTItem* findIoTItem(String name) { +IoTItem* findIoTItem(const String& name) { for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getID() == name) return *it; } @@ -189,7 +184,7 @@ IoTItem* findIoTItem(String name) { return nullptr; } // поиск плюс получение значения -String getItemValue(String name) { +String getItemValue(const String& name) { IoTItem* tmp = findIoTItem(name); if (tmp) return tmp->getValue(); @@ -198,7 +193,7 @@ String getItemValue(String name) { } // существует ли айтем -bool isItemExist(String name) { +bool isItemExist(const String& name) { IoTItem* tmp = findIoTItem(name); if (tmp) return true; diff --git a/src/modules/exec/ButtonIn/ButtonIn.cpp b/src/modules/exec/ButtonIn/ButtonIn.cpp index e968eb18..700edce7 100644 --- a/src/modules/exec/ButtonIn/ButtonIn.cpp +++ b/src/modules/exec/ButtonIn/ButtonIn.cpp @@ -68,7 +68,7 @@ class ButtonIn : public IoTItem { _lastButtonState = _reading; } - void setValue(IoTValue Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool generateEvent = true) { value = Value; if (generateEvent) regEvent((String)(int)value.valD, "ButtonIn"); } diff --git a/src/modules/exec/ButtonOut/ButtonOut.cpp b/src/modules/exec/ButtonOut/ButtonOut.cpp index d041f748..2f3005d1 100644 --- a/src/modules/exec/ButtonOut/ButtonOut.cpp +++ b/src/modules/exec/ButtonOut/ButtonOut.cpp @@ -37,7 +37,7 @@ class ButtonOut : public IoTItem { return {}; // команда поддерживает возвращаемое значения. Т.е. по итогу выполнения команды или общения с внешней системой, можно вернуть значение в сценарий для дальнейшей обработки } - void setValue(IoTValue Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool generateEvent = true) { value = Value; IoTgpio.digitalWrite(_pin, _inv?!value.valD:value.valD); if (generateEvent) regEvent((String)(int)value.valD, "ButtonOut"); diff --git a/src/modules/exec/IoTServo/IoTServo.cpp b/src/modules/exec/IoTServo/IoTServo.cpp index d995918b..b2340558 100644 --- a/src/modules/exec/IoTServo/IoTServo.cpp +++ b/src/modules/exec/IoTServo/IoTServo.cpp @@ -52,7 +52,7 @@ class IoTServo : public IoTItem { return {}; } - void setValue(IoTValue Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool generateEvent = true) { value = Value; if (value.isDecimal & (_oldValue != value.valD)) { _oldValue = value.valD; diff --git a/src/modules/exec/Pwm32/Pwm32.cpp b/src/modules/exec/Pwm32/Pwm32.cpp index 6ad209e8..a1d3cb99 100644 --- a/src/modules/exec/Pwm32/Pwm32.cpp +++ b/src/modules/exec/Pwm32/Pwm32.cpp @@ -54,7 +54,7 @@ class Pwm32 : public IoTItem { } } - void setValue(IoTValue Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool generateEvent = true) { value = Value; ledcWrite(_ledChannel, value.valD); if (generateEvent) regEvent(value.valD, "Pwm32"); diff --git a/src/modules/exec/Pwm8266/Pwm8266.cpp b/src/modules/exec/Pwm8266/Pwm8266.cpp index 73c4493d..10cc1b01 100644 --- a/src/modules/exec/Pwm8266/Pwm8266.cpp +++ b/src/modules/exec/Pwm8266/Pwm8266.cpp @@ -44,7 +44,7 @@ class Pwm8266 : public IoTItem { } } - void setValue(IoTValue Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool generateEvent = true) { value = Value; IoTgpio.analogWrite(_pin, value.valD); if (generateEvent) regEvent(value.valD, "Pwm8266"); diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index d9f5fdde..117afead 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -263,7 +263,7 @@ class Loging : public IoTItem { } } - void regEvent(String value, String consoleInfo, bool error = false) { + void regEvent(const String& value, const String& consoleInfo, bool error = false) { String userDate = getItemValue(id + "-date"); String currentDate = getTodayDateDotFormated(); //отправляем в график данные только когда выбран сегодняшний день @@ -306,12 +306,12 @@ class Date : public IoTItem { value.isDecimal = false; } - void setValue(String valStr) { + void setValue(const String& valStr) { value.valS = valStr; setValue(value); } - void setValue(IoTValue Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool generateEvent = true) { value = Value; if (generateEvent) regEvent(value.valS, ""); //отправка данных при изменении даты diff --git a/src/modules/virtual/VButton/VButton.cpp b/src/modules/virtual/VButton/VButton.cpp index 44f846ad..b5411af0 100644 --- a/src/modules/virtual/VButton/VButton.cpp +++ b/src/modules/virtual/VButton/VButton.cpp @@ -6,7 +6,7 @@ class VButton : public IoTItem { public: VButton(String parameters): IoTItem(parameters) { } - void setValue(IoTValue Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool generateEvent = true) { value = Value; if (generateEvent) regEvent((String)(int)value.valD, "VButton"); } diff --git a/src/modules/virtual/Variable/Variable.cpp b/src/modules/virtual/Variable/Variable.cpp index 0ecd533e..28d495ed 100644 --- a/src/modules/virtual/Variable/Variable.cpp +++ b/src/modules/virtual/Variable/Variable.cpp @@ -9,7 +9,7 @@ class Variable : public IoTItem { } // особенность данного модуля - просто хранение значения для сценария, нет событий - // void setValue(IoTValue Value, bool generateEvent = true) { + // void setValue(const IoTValue& Value, bool generateEvent = true) { // value = Value; // } diff --git a/src/utils/JsonUtils.cpp b/src/utils/JsonUtils.cpp index 141bd8e6..d35eb7d2 100644 --- a/src/utils/JsonUtils.cpp +++ b/src/utils/JsonUtils.cpp @@ -11,7 +11,7 @@ void jsonWriteStrDoc(DynamicJsonDocument& doc, String name, String value) { } // new============================================================================== -bool jsonRead(String& json, String key, unsigned long& value, bool e) { +bool jsonRead(const String& json, String key, unsigned long& value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -32,7 +32,7 @@ bool jsonRead(String& json, String key, unsigned long& value, bool e) { return ret; } -bool jsonRead(String& json, String key, float& value, bool e) { +bool jsonRead(const String& json, String key, float& value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -53,7 +53,7 @@ bool jsonRead(String& json, String key, float& value, bool e) { return ret; } -bool jsonRead(String& json, String key, String& value, bool e) { +bool jsonRead(const String& json, String key, String& value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -74,7 +74,7 @@ bool jsonRead(String& json, String key, String& value, bool e) { return ret; } -bool jsonRead(String& json, String key, bool& value, bool e) { +bool jsonRead(const String& json, String key, bool& value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -95,7 +95,7 @@ bool jsonRead(String& json, String key, bool& value, bool e) { return ret; } -bool jsonRead(String& json, String key, int& value, bool e) { +bool jsonRead(const String& json, String key, int& value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -117,7 +117,7 @@ bool jsonRead(String& json, String key, int& value, bool e) { } // new============================================================================== -bool jsonWriteStr_(String& json, String key, String value, bool e) { +bool jsonWriteStr_(String& json, const String& key, const String& value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -134,7 +134,7 @@ bool jsonWriteStr_(String& json, String key, String value, bool e) { return ret; } -bool jsonWriteBool_(String& json, String key, bool value, bool e) { +bool jsonWriteBool_(String& json, const String& key, bool value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -151,7 +151,7 @@ bool jsonWriteBool_(String& json, String key, bool value, bool e) { return ret; } -bool jsonWriteInt_(String& json, String key, int value, bool e) { +bool jsonWriteInt_(String& json, const String& key, int value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -168,7 +168,7 @@ bool jsonWriteInt_(String& json, String key, int value, bool e) { return ret; } -bool jsonWriteFloat_(String& json, String key, float value, bool e) { +bool jsonWriteFloat_(String& json, const String &key, float value, bool e) { bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); @@ -220,7 +220,7 @@ void jsonMergeDocs(JsonObject dest, JsonObjectConst src) { } // depricated====================================================================== -String jsonReadStr(String& json, String name, bool e) { +String jsonReadStr(const String& json, String name, bool e) { DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { @@ -232,7 +232,7 @@ String jsonReadStr(String& json, String name, bool e) { return doc[name].as(); } -boolean jsonReadBool(String& json, String name, bool e) { +boolean jsonReadBool(const String& json, String name, bool e) { DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { @@ -244,7 +244,7 @@ boolean jsonReadBool(String& json, String name, bool e) { return doc[name].as(); } -int jsonReadInt(String& json, String name, bool e) { +int jsonReadInt(const String& json, String name, bool e) { DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { From 4ddbd97999113090a99b511c1c4d4cf441e1d291 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 29 Oct 2022 19:57:09 +0300 Subject: [PATCH 060/107] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=83=20=D1=81=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=D0=BC=D0=B8=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D1=8F=20JSO?= =?UTF-8?q?N?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/JsonUtils.cpp | 74 +++++++++++++---------------------------- 1 file changed, 24 insertions(+), 50 deletions(-) diff --git a/src/utils/JsonUtils.cpp b/src/utils/JsonUtils.cpp index d35eb7d2..ab362ac3 100644 --- a/src/utils/JsonUtils.cpp +++ b/src/utils/JsonUtils.cpp @@ -12,108 +12,82 @@ void jsonWriteStrDoc(DynamicJsonDocument& doc, String name, String value) { // new============================================================================== bool jsonRead(const String& json, String key, unsigned long& value, bool e) { - bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); - } - ret = false; + SerialPrint("EE", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + return false; } else if (!doc.containsKey(key)) { if (e) { - SerialPrint("EE", F("jsonRead"), "json key '" + key + "' missing"); + SerialPrint("EE", F("jsonRead"), key + " missing"); jsonErrorDetected(); } - ret = false; + return false; } value = doc[key].as(); - return ret; + return true; } bool jsonRead(const String& json, String key, float& value, bool e) { - bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); - } - ret = false; + SerialPrint("EE", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + return false; } else if (!doc.containsKey(key)) { if (e) { SerialPrint("EE", F("jsonRead"), key + " missing"); jsonErrorDetected(); } - ret = false; + return false; } value = doc[key].as(); - return ret; + return true; } bool jsonRead(const String& json, String key, String& value, bool e) { - bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); - } - ret = false; + SerialPrint("EE", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + return false; } else if (!doc.containsKey(key)) { if (e) { SerialPrint("EE", F("jsonRead"), key + " missing"); jsonErrorDetected(); } - ret = false; + return false; } value = doc[key].as(); - return ret; + return true; } bool jsonRead(const String& json, String key, bool& value, bool e) { - bool ret = true; - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - DeserializationError error = deserializeJson(doc, json); - if (error) { - if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); - } - ret = false; - } else if (!doc.containsKey(key)) { - if (e) { - SerialPrint("EE", F("jsonRead"), key + " missing"); - jsonErrorDetected(); - } - ret = false; - } - value = doc[key].as(); + int lvalue = value; + bool ret = jsonRead(json, key, lvalue, e); + value = lvalue; return ret; } bool jsonRead(const String& json, String key, int& value, bool e) { - bool ret = true; DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); - } - ret = false; + SerialPrint("EE", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + return false; } else if (!doc.containsKey(key)) { if (e) { SerialPrint("EE", F("jsonRead"), key + " missing"); jsonErrorDetected(); } - ret = false; + return false; } value = doc[key].as(); - return ret; + return true; } // new============================================================================== From d9788f96bd3f7ab6c82f5393466ab71fa7b9755b Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 29 Oct 2022 20:04:02 +0300 Subject: [PATCH 061/107] =?UTF-8?q?=D0=A0=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BF=D0=BE=D0=BD=D1=8F=D1=82=D0=B8=D1=8F?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=B8=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=B4=D1=80=D0=B0=D0=B7=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=B2=D0=B0=D0=B5=D1=82=20=D1=84=D0=B8=D0=BA=D1=81=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B8=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20WS=20=D0=B8=20MQTT=20=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=D0=B0=20=D0=B3=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D1=83=D0=BC=D0=B5=D0=B2=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=84=D0=B0=D0=BA=D1=82=D0=B0=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B2=20=D0=BE=D1=87=D0=B5=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D1=8C=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B4=D1=80=D1=83=D0=B3=D0=B8=D0=B5=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=B9=D1=81=D1=82=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 24 ++++---- src/classes/IoTItem.cpp | 70 ++++++++++++----------- src/modules/display/Ws2812b/Ws2181b.cpp | 4 +- src/modules/exec/ButtonIn/ButtonIn.cpp | 4 +- src/modules/exec/ButtonOut/ButtonOut.cpp | 4 +- src/modules/exec/IoTServo/IoTServo.cpp | 4 +- src/modules/exec/Pwm32/Pwm32.cpp | 4 +- src/modules/exec/Pwm8266/Pwm8266.cpp | 4 +- src/modules/virtual/Loging/Loging.cpp | 10 ++-- src/modules/virtual/VButton/VButton.cpp | 4 +- src/modules/virtual/Variable/Variable.cpp | 5 -- 11 files changed, 68 insertions(+), 69 deletions(-) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index dc42c57a..ba2b1053 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -16,8 +16,8 @@ class IoTItem { virtual void doByInterval(); virtual IoTValue execute(String command, std::vector& param); - virtual void regEvent(const String& value, const String& consoleInfo, bool error = false); - virtual void regEvent(float value, const String& consoleInfo, bool error = false); + virtual void regEvent(const String& value, const String& consoleInfo, bool error = false, bool genEvent = true); + virtual void regEvent(float value, const String& consoleInfo, bool error = false, bool genEvent = true); String getSubtype(); @@ -38,8 +38,8 @@ class IoTItem { bool enableDoByInt = true; virtual IoTGpio* getGpioDriver(); - virtual void setValue(const IoTValue& Value, bool generateEvent = true); - virtual void setValue(const String& valStr, bool generateEvent = true); + virtual void setValue(const IoTValue& Value, bool genEvent = true); + virtual void setValue(const String& valStr, bool genEvent = true); String getRoundValue(); //методы для графиков @@ -51,17 +51,17 @@ class IoTItem { protected: bool _needSave = false; // признак необходимости сохранять и загружать значение элемента на flash - String _subtype; - String _id; - unsigned long _interval; + String _subtype = ""; + String _id = ""; + unsigned long _interval = 1000; float _multiply; // умножаем на значение float _plus; // увеличиваем на значение - int _map1; - int _map2; - int _map3; - int _map4; - int _round; // 1, 10, 100, 1000, 10000 + int _map1 = 0; + int _map2 = 0; + int _map3 = 0; + int _map4 = 0; + int _round = 1; // 1, 10, 100, 1000, 10000 bool _global = false; // характеристика айтема, что ему нужно слать и принимать события из внешнего мира }; diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index c63d12a7..e78e6b42 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -18,14 +18,6 @@ IoTItem::IoTItem(const String& parameters) { if (!jsonRead(parameters, F("global"), _global, false)) _global = false; - String valAsStr; - if (jsonRead(parameters, F("val"), valAsStr, false)) // значение переменной или датчика при инициализации если есть в конфигурации - setValue(valAsStr, false); - - jsonRead(parameters, F("needSave"), _needSave, false); - if (_needSave && jsonRead(valuesFlashJson, _id, valAsStr, false)) // пробуем достать из сохранения значение элемента, если указано, что нужно сохранять - setValue(valAsStr, false); - String map; jsonRead(parameters, F("map"), map, false); if (map != "") { @@ -35,6 +27,14 @@ IoTItem::IoTItem(const String& parameters) { _map4 = selectFromMarkerToMarker(map, ",", 3).toInt(); } else _map1 = _map2 = _map3 = _map4 = 0; + + String valAsStr = ""; + if (jsonRead(parameters, F("val"), valAsStr, false)) // значение переменной или датчика при инициализации если есть в конфигурации + setValue(valAsStr, false); + + jsonRead(parameters, F("needSave"), _needSave, false); + if (_needSave && jsonRead(valuesFlashJson, _id, valAsStr, false)) // пробуем достать из сохранения значение элемента, если указано, что нужно сохранять + setValue(valAsStr, false); } //луп выполняющий переодическое дерганье @@ -58,48 +58,52 @@ String IoTItem::getValue() { } //определяем тип прилетевшей величины -void IoTItem::setValue(const String& valStr, bool generateEvent) { - if (value.isDecimal = isDigitDotCommaStr(valStr)) { +void IoTItem::setValue(const String& valStr, bool genEvent) { + value.isDecimal = isDigitDotCommaStr(valStr); + + if (value.isDecimal) { value.valD = valStr.toFloat(); } else { value.valS = valStr; } - setValue(value, generateEvent); + setValue(value, genEvent); } // -void IoTItem::setValue(const IoTValue& Value, bool generateEvent) { +void IoTItem::setValue(const IoTValue& Value, bool genEvent) { value = Value; - if (generateEvent) - if (value.isDecimal) { - regEvent(value.valD, ""); - } else { - regEvent(value.valS, ""); - } + + if (value.isDecimal) { + regEvent(value.valD, "", false, genEvent); + } else { + regEvent(value.valS, "", false, genEvent); + } } //когда событие случилось -void IoTItem::regEvent(const String& value, const String& consoleInfo, bool error) { +void IoTItem::regEvent(const String& value, const String& consoleInfo, bool error, bool genEvent) { if (_needSave) { jsonWriteStr_(valuesFlashJson, _id, value); needSaveValues = true; } publishStatusMqtt(_id, value); publishStatusWs(_id, value); - SerialPrint("i", "Sensor", consoleInfo + " '" + _id + "' data: " + value + "'"); + //SerialPrint("i", "Sensor", consoleInfo + " '" + _id + "' data: " + value + "'"); - generateEvent(_id, value); + if (genEvent) { + generateEvent(_id, value); - //отправка события другим устройствам в сети если не было ошибки============================== - if (jsonReadBool(settingsFlashJson, "mqttin") && _global && !error) { - String json = "{}"; - jsonWriteStr_(json, "id", _id); - jsonWriteStr_(json, "val", value); - jsonWriteInt_(json, "int", _interval/1000 + 5); // 5 секунд про запас - publishEvent(_id, json); - SerialPrint("i", F("<=MQTT"), "Broadcast event: " + json); + //отправка события другим устройствам в сети если не было ошибки============================== + if (jsonReadBool(settingsFlashJson, "mqttin") && _global && !error) { + String json = "{}"; + jsonWriteStr_(json, "id", _id); + jsonWriteStr_(json, "val", value); + jsonWriteInt_(json, "int", _interval/1000 + 5); // 5 секунд про запас + publishEvent(_id, json); + SerialPrint("i", F("<=MQTT"), "Broadcast event: " + json); + } + //======================================================================== } - //======================================================================== } String IoTItem::getRoundValue() { @@ -115,14 +119,14 @@ String IoTItem::getRoundValue() { } } -void IoTItem::regEvent(float regvalue, const String& consoleInfo, bool error) { +void IoTItem::regEvent(float regvalue, const String& consoleInfo, bool error, bool genEvent) { value.valD = regvalue; if (_multiply) value.valD = value.valD * _multiply; if (_plus) value.valD = value.valD + _plus; if (_map1 != _map2) value.valD = map(value.valD, _map1, _map2, _map3, _map4); - - regEvent(getRoundValue(), consoleInfo, error); + + regEvent(getRoundValue(), consoleInfo, error, genEvent); } void IoTItem::doByInterval() {} diff --git a/src/modules/display/Ws2812b/Ws2181b.cpp b/src/modules/display/Ws2812b/Ws2181b.cpp index 9ddac234..0dbf5362 100644 --- a/src/modules/display/Ws2812b/Ws2181b.cpp +++ b/src/modules/display/Ws2812b/Ws2181b.cpp @@ -171,14 +171,14 @@ public: return {}; } - void setValue(IoTValue Value, bool generateEvent = true){ + void setValue(const IoTValue& Value, bool genEvent = true){ if (!_strip) return; value = Value; int b = map(value.valD, 1,1024,1,255); _strip->setBrightness(b); _strip->show(); - if (generateEvent) regEvent(value.valD, "Ws2812b"); + regEvent(value.valD, "Ws2812b", false, genEvent); } ~Ws2812b(){}; diff --git a/src/modules/exec/ButtonIn/ButtonIn.cpp b/src/modules/exec/ButtonIn/ButtonIn.cpp index 700edce7..bd532aab 100644 --- a/src/modules/exec/ButtonIn/ButtonIn.cpp +++ b/src/modules/exec/ButtonIn/ButtonIn.cpp @@ -68,9 +68,9 @@ class ButtonIn : public IoTItem { _lastButtonState = _reading; } - void setValue(const IoTValue& Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; - if (generateEvent) regEvent((String)(int)value.valD, "ButtonIn"); + regEvent((String)(int)value.valD, "ButtonIn", false, genEvent); } String getValue() { diff --git a/src/modules/exec/ButtonOut/ButtonOut.cpp b/src/modules/exec/ButtonOut/ButtonOut.cpp index 2f3005d1..017d1fc9 100644 --- a/src/modules/exec/ButtonOut/ButtonOut.cpp +++ b/src/modules/exec/ButtonOut/ButtonOut.cpp @@ -37,10 +37,10 @@ class ButtonOut : public IoTItem { return {}; // команда поддерживает возвращаемое значения. Т.е. по итогу выполнения команды или общения с внешней системой, можно вернуть значение в сценарий для дальнейшей обработки } - void setValue(const IoTValue& Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; IoTgpio.digitalWrite(_pin, _inv?!value.valD:value.valD); - if (generateEvent) regEvent((String)(int)value.valD, "ButtonOut"); + regEvent((String)(int)value.valD, "ButtonOut", false, genEvent); } String getValue() { diff --git a/src/modules/exec/IoTServo/IoTServo.cpp b/src/modules/exec/IoTServo/IoTServo.cpp index b2340558..3cd672cf 100644 --- a/src/modules/exec/IoTServo/IoTServo.cpp +++ b/src/modules/exec/IoTServo/IoTServo.cpp @@ -52,12 +52,12 @@ class IoTServo : public IoTItem { return {}; } - void setValue(const IoTValue& Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; if (value.isDecimal & (_oldValue != value.valD)) { _oldValue = value.valD; servObj.write(_oldValue); - if (generateEvent) regEvent(value.valD, "IoTServo"); + regEvent(value.valD, "IoTServo", false, genEvent); } } diff --git a/src/modules/exec/Pwm32/Pwm32.cpp b/src/modules/exec/Pwm32/Pwm32.cpp index a1d3cb99..79f6eedf 100644 --- a/src/modules/exec/Pwm32/Pwm32.cpp +++ b/src/modules/exec/Pwm32/Pwm32.cpp @@ -54,10 +54,10 @@ class Pwm32 : public IoTItem { } } - void setValue(const IoTValue& Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; ledcWrite(_ledChannel, value.valD); - if (generateEvent) regEvent(value.valD, "Pwm32"); + regEvent(value.valD, "Pwm32", false, genEvent); } //======================================================================================================= diff --git a/src/modules/exec/Pwm8266/Pwm8266.cpp b/src/modules/exec/Pwm8266/Pwm8266.cpp index 10cc1b01..2f8883f4 100644 --- a/src/modules/exec/Pwm8266/Pwm8266.cpp +++ b/src/modules/exec/Pwm8266/Pwm8266.cpp @@ -44,10 +44,10 @@ class Pwm8266 : public IoTItem { } } - void setValue(const IoTValue& Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; IoTgpio.analogWrite(_pin, value.valD); - if (generateEvent) regEvent(value.valD, "Pwm8266"); + regEvent(value.valD, "Pwm8266", false, genEvent); } //======================================================================================================= diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index 117afead..a189d046 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -263,7 +263,7 @@ class Loging : public IoTItem { } } - void regEvent(const String& value, const String& consoleInfo, bool error = false) { + void regEvent(const String& value, const String& consoleInfo, bool error = false, bool genEvent = true) { String userDate = getItemValue(id + "-date"); String currentDate = getTodayDateDotFormated(); //отправляем в график данные только когда выбран сегодняшний день @@ -306,14 +306,14 @@ class Date : public IoTItem { value.isDecimal = false; } - void setValue(const String& valStr) { + void setValue(const String& valStr, bool genEvent = true) { value.valS = valStr; - setValue(value); + setValue(value, genEvent); } - void setValue(const IoTValue& Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; - if (generateEvent) regEvent(value.valS, ""); + regEvent(value.valS, "", false, genEvent); //отправка данных при изменении даты for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getSubtype() == "Loging") { diff --git a/src/modules/virtual/VButton/VButton.cpp b/src/modules/virtual/VButton/VButton.cpp index b5411af0..747ded1c 100644 --- a/src/modules/virtual/VButton/VButton.cpp +++ b/src/modules/virtual/VButton/VButton.cpp @@ -6,9 +6,9 @@ class VButton : public IoTItem { public: VButton(String parameters): IoTItem(parameters) { } - void setValue(const IoTValue& Value, bool generateEvent = true) { + void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; - if (generateEvent) regEvent((String)(int)value.valD, "VButton"); + regEvent((String)(int)value.valD, "VButton", false, genEvent); } String getValue() { diff --git a/src/modules/virtual/Variable/Variable.cpp b/src/modules/virtual/Variable/Variable.cpp index 28d495ed..755506a4 100644 --- a/src/modules/virtual/Variable/Variable.cpp +++ b/src/modules/virtual/Variable/Variable.cpp @@ -8,11 +8,6 @@ class Variable : public IoTItem { Variable(String parameters) : IoTItem(parameters) { } - // особенность данного модуля - просто хранение значения для сценария, нет событий - // void setValue(const IoTValue& Value, bool generateEvent = true) { - // value = Value; - // } - void doByInterval() { } }; From 94c156f1231de6cf363fc22510f2a6842d12b433 Mon Sep 17 00:00:00 2001 From: biver Date: Sat, 29 Oct 2022 20:23:35 +0300 Subject: [PATCH 062/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9?= =?UTF-8?q?=D1=81=D0=B0=20=D0=B2=20=D1=81=D0=BB=D1=83=D1=87=D0=B0=D0=B5=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=82=D0=B8=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/Timer/Timer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/virtual/Timer/Timer.cpp b/src/modules/virtual/Timer/Timer.cpp index f843206f..d1ab432e 100644 --- a/src/modules/virtual/Timer/Timer.cpp +++ b/src/modules/virtual/Timer/Timer.cpp @@ -33,6 +33,7 @@ class Timer : public IoTItem { if (value.valD == 0) { regEvent(value.valD, "Time's up"); } + if (!_ticker) regEvent(getValue(), "Timer tick", false, false); // только регистрируем изменения без генерации тиков } if (_ticker && (value.valD > 0 || _unfin) && !_pause) regEvent(value.valD, "Timer tick"); From 96c99ef5aca7ae3d079835bec160d95636b9f0c8 Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 30 Oct 2022 12:09:23 +0300 Subject: [PATCH 063/107] =?UTF-8?q?=D0=9F=D1=80=D1=8F=D1=87=D0=B5=D0=BC=20?= =?UTF-8?q?=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20=D0=B2=20=D0=BE=D0=B4?= =?UTF-8?q?=D0=BD=D1=83=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/ButtonIn/ButtonIn.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/modules/exec/ButtonIn/ButtonIn.cpp b/src/modules/exec/ButtonIn/ButtonIn.cpp index bd532aab..18e8a87f 100644 --- a/src/modules/exec/ButtonIn/ButtonIn.cpp +++ b/src/modules/exec/ButtonIn/ButtonIn.cpp @@ -29,12 +29,9 @@ class ButtonIn : public IoTItem { if (_pinMode == "INPUT_PULLUP") IoTgpio.digitalWrite(_pin, HIGH); else if (_pinMode == "INPUT_PULLDOWN") IoTgpio.digitalWrite(_pin, LOW); - - // TODO: загрузить значение из памяти иначе пока просто считываем значение текущего состояния PIN value.valD = _buttonState = IoTgpio.digitalRead(_pin); // сообщаем всем о стартовом статусе без генерации события - publishStatusMqtt(_id, (String)_buttonState); - publishStatusWs(_id, (String)_buttonState); + regEvent(_buttonState, "", false, false); } void loop() { From d91ffc4e25335a150d7cb3eeeba72f9c894d760e Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 30 Oct 2022 12:12:15 +0300 Subject: [PATCH 064/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=83=20=D1=81=D0=B5=D1=82=D0=B5=D0=B2=D1=8B=D1=85=20?= =?UTF-8?q?=D1=81=D1=86=D0=B5=D0=BD=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D0=B5=D0=BC=20=D1=81=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D1=82=D0=BE=D0=B5=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B8=20=D0=BF=D1=80=D0=B8=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=92=D1=85=D0=BE=D0=B4=D1=8F=D1=89=D0=B8=D0=B5=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20=D0=B1=D1=83=D0=B4=D1=83=D1=82?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D1=8C=D1=81=D1=8F=20=D1=82=D0=BE=D0=BB=D1=8C?= =?UTF-8?q?=D0=BA=D0=BE=20=D0=BF=D1=80=D0=B8=20=D0=BD=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D0=B8=20=D0=BE=D0=B4=D0=BD=D0=BE=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BB=D0=B5=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=20=D0=B2=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 9 ++++++++- src/Main.cpp | 15 +++++++++++---- src/MqttClient.cpp | 27 +++++++++++++-------------- src/classes/IoTItem.cpp | 19 ++++++++++++++++++- src/classes/IoTScenario.cpp | 6 ++++++ src/modules/sceninfo.json | 5 +++++ 6 files changed, 61 insertions(+), 20 deletions(-) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index ba2b1053..88162b28 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -16,15 +16,19 @@ class IoTItem { virtual void doByInterval(); virtual IoTValue execute(String command, std::vector& param); + void checkIntFromNet(); + virtual void regEvent(const String& value, const String& consoleInfo, bool error = false, bool genEvent = true); virtual void regEvent(float value, const String& consoleInfo, bool error = false, bool genEvent = true); String getSubtype(); String getID(); + int getIntFromNet(); virtual String getValue(); - + void setInterval(unsigned long interval); + void setIntFromNet(int interval); unsigned long currentMillis; unsigned long prevMillis; @@ -54,6 +58,9 @@ class IoTItem { String _subtype = ""; String _id = ""; unsigned long _interval = 1000; + int _intFromNet = -2; // количество секунд доверия, пришедших из сети вместе с данными для текущего ИД + // -2 - данные не приходили, скорее всего, элемент локальный, доверие есть + // -1 - данные приходили и обратный отсчет дошел до нуля, значит доверия нет float _multiply; // умножаем на значение float _plus; // увеличиваем на значение diff --git a/src/Main.cpp b/src/Main.cpp index bc5c2142..31cda3b5 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -150,10 +150,17 @@ void loop() { if (loopPeriod > 2) Serial.println(loopPeriod); #endif - // сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) - if (needSaveValues && millis()%1000 == 0) { - syncValuesFlashJson(); - needSaveValues = false; + if (millis()%1000 == 0) { + // сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) + if (needSaveValues) { + syncValuesFlashJson(); + needSaveValues = false; + } + + // проверяем все элементы на тухлость + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { + (*it)->checkIntFromNet(); + } } } diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index 72884352..3be1f3ef 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -160,7 +160,6 @@ void mqttCallback(char* topic, uint8_t* payload, size_t length) { //здесь мы получаем события с других устройств, которые потом проверяются в сценариях этого устройства else if (topicStr.indexOf("event") != -1) { //пока не работает сетевой обмен этот код будет закомментирован - if (!jsonReadBool(settingsFlashJson, "mqttin")) { return; } @@ -174,24 +173,24 @@ void mqttCallback(char* topic, uint8_t* payload, size_t length) { if (itemExist) { unsigned long interval; jsonRead(payloadStr, F("int"), interval); - itemExist->setInterval(interval); - itemExist->setValue(valAsStr, false); + itemExist->setInterval(interval); // устанавливаем такой же интервал как на источнике события + itemExist->setValue(valAsStr, false); // только регистрируем изменения в интерфейсе без создания цикла сетевых событий + if (interval) itemExist->setIntFromNet(interval+5); // если пришедший интервал =0, значит не нужно контролировать доверие, иначе даем фору в 5 сек + + // запустим проверку его в сценариях + generateEvent(id, valAsStr); + SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + valAsStr); } else { - //добавим событие в базу - itemExist = (IoTItem*)new externalVariable(payloadStr); - IoTItems.push_back(itemExist); + // зафиксируем данные в базе, если локально элемент отсутствует + //itemExist = (IoTItem*)new externalVariable(payloadStr); + //IoTItems.push_back(itemExist); } - //запустим проверку его в сценариях - generateEvent(id, valAsStr); - publishStatusMqtt(id, valAsStr); - publishStatusWs(id, valAsStr); - //itemExist->regEvent(valAsStr, ""); - SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + valAsStr); + } } - //здесь мы получаем прямые команды которые сразу выполнятся на этом устройстве - //необходимо для тех кто хочет управлять своим устройством из mqtt + // здесь мы получаем прямые команды которые сразу выполнятся на этом устройстве + // необходимо для тех кто хочет управлять своим устройством из mqtt else if (topicStr.indexOf("order") != -1) { if (!jsonReadBool(settingsFlashJson, "mqttin")) { return; diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index e78e6b42..65a03392 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -98,7 +98,7 @@ void IoTItem::regEvent(const String& value, const String& consoleInfo, bool erro String json = "{}"; jsonWriteStr_(json, "id", _id); jsonWriteStr_(json, "val", value); - jsonWriteInt_(json, "int", _interval/1000 + 5); // 5 секунд про запас + jsonWriteInt_(json, "int", _interval/1000); publishEvent(_id, json); SerialPrint("i", F("<=MQTT"), "Broadcast event: " + json); } @@ -137,6 +137,23 @@ String IoTItem::getSubtype() { return _subtype; } +int IoTItem::getIntFromNet() { + return _intFromNet; +} + +void IoTItem::setIntFromNet(int interval) { + _intFromNet = interval; +} + +void IoTItem::checkIntFromNet() { + if (_intFromNet >= 0) { + if (_intFromNet == 0) { + SerialPrint("E", "SYS", "The new data did not come from the network. The level of trust is low.", _id); + } + _intFromNet--; + } +} + void IoTItem::publishValue() {} void IoTItem::clearValue() {} diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index 93534041..e4c59e25 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -286,6 +286,12 @@ class CallExprAST : public ExprAST { if (!ItemIsLocal) Item = findIoTItem(Callee); // пробуем найти переменную если она не локальная (могла придти по сети в процессе) if (!Item) return nullptr; // ret = zeroIotVal; // если все же не пришла, то либо опечатка, либо уже стерлась - выходим + if (Cmd == "getIntFromNet") { + ret.valD = Item->getIntFromNet(); + ret.isDecimal = true; + return &ret; + } + // если все же все ок, то готовим параметры для передачи в модуль std::vector ArgsAsIoTValue; for (unsigned int i = 0; i < Args.size(); i++) { diff --git a/src/modules/sceninfo.json b/src/modules/sceninfo.json index 6f26cc47..fb472e38 100644 --- a/src/modules/sceninfo.json +++ b/src/modules/sceninfo.json @@ -9,6 +9,11 @@ "title": "Сценарии", "moduleDesc": "Сценарии позволяют реализовать индивидуальный алгоритм работы контроллера с учетом происходящих событий. Они представляют из себя описательный язык того, что нужно сделать при наступлении того или иного события, учитывая конкретные условия. \nВ базе языка - выражение вида: “Если условие истина, то выполнить одно действие, а если нет, то иное”. При этом проверка такого выражения будет осуществляться только при наступлении события связанного с элементом конфигурации, который упоминается в этом выражении. \nУсловием или действием может быть любое разрешенное выражение. Они все при выполнении возвращают значение. Выражение может состоять из: идентификаторов элементов конфигурации, чисел (целые, дробные и отрицательные), строк в кавычках, операций сравнения < > <= >= == !=, операций присваивания значений =, математических операций +-*/, логических операций &|, комментариев после символа #, функций (в параметрах которых так же могут быть любые разрешенные выражения), конструкции ветвления IfThenElse, группирующие блоки выражений {}", "funcInfo": [ + { + "name": "getIntFromNet", + "descr": "Получаем количество секунд доверия к значениям элемента. При -2 доверие полное, при -1 время доверия истекло. При >0 время обратного отсчета. Используется только совместно с ИД элемента: ID.getIntFromNet()", + "params": [] + }, { "name": "exit", "descr": "Прерываем работу сценария и выводим в консоль причину. Причина не обязательна.", From 898af539708e6335c7c7d421b6ca0f51d6be08f3 Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 30 Oct 2022 20:20:57 +0300 Subject: [PATCH 065/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83?= =?UTF-8?q?=20=D1=81=20=D0=B2=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=D0=BC=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D1=87=D0=BD=D1=8B=D1=85=20=D1=81=D0=B5=D0=BA=D1=83=D0=BD=D0=B4?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/Const.h | 4 ++-- src/Main.cpp | 29 ++++++++++++++++------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/include/Const.h b/include/Const.h index a4e065b4..4581349c 100644 --- a/include/Const.h +++ b/include/Const.h @@ -54,10 +54,10 @@ enum TimerTask_t { WIFI_SCAN, TIME_SYNC, UPTIME, UDP, // UDPP - TIMES, + TIMES, // периодические секундные проверки PTASK, ST, - END }; + END}; //задачи которые надо протащить через loop enum NotAsyncActions { diff --git a/src/Main.cpp b/src/Main.cpp index 31cda3b5..fa14d76e 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -94,6 +94,22 @@ void setup() { stInit(); + // настраиваем секундные обслуживания системы + ts.add( + TIMES, 1000, [&](void*) { + // сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) + if (needSaveValues) { + syncValuesFlashJson(); + needSaveValues = false; + } + + // проверяем все элементы на тухлость + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { + (*it)->checkIntFromNet(); + } + }, + nullptr, true); + // test Serial.println("-------test start--------"); Serial.println("--------test end---------"); @@ -149,19 +165,6 @@ void loop() { loopPeriod = millis() - st; if (loopPeriod > 2) Serial.println(loopPeriod); #endif - - if (millis()%1000 == 0) { - // сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) - if (needSaveValues) { - syncValuesFlashJson(); - needSaveValues = false; - } - - // проверяем все элементы на тухлость - for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { - (*it)->checkIntFromNet(); - } - } } //отправка json From 32f09cd8e434af3981458b573eaefaccbb02a07c Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 30 Oct 2022 20:50:05 +0300 Subject: [PATCH 066/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BF=D1=80=D0=B8=D0=B5=D0=BC=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D1=81=20=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D1=82=D0=B5=D0=B9=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MqttClient.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index 3be1f3ef..d527151d 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -167,11 +167,11 @@ void mqttCallback(char* topic, uint8_t* payload, size_t length) { String devId = selectFromMarkerToMarker(topicStr, "/", 2); String id = selectFromMarkerToMarker(topicStr, "/", 3); String valAsStr; - jsonRead(payloadStr, F("val"), valAsStr, false); + if (!jsonRead(payloadStr, F("val"), valAsStr, false)) valAsStr = payloadStr; IoTItem* itemExist = findIoTItem(id); if (itemExist) { - unsigned long interval; + unsigned long interval = 0; jsonRead(payloadStr, F("int"), interval); itemExist->setInterval(interval); // устанавливаем такой же интервал как на источнике события itemExist->setValue(valAsStr, false); // только регистрируем изменения в интерфейсе без создания цикла сетевых событий From ce23ab180fe0baacdcb84cb34c10c3d5817f421d Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 30 Oct 2022 21:40:11 +0300 Subject: [PATCH 067/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20edit=20=D0=BD=D0=B0=20esp32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/StandWebServer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StandWebServer.cpp b/src/StandWebServer.cpp index 3a465a7e..6d1e5d58 100644 --- a/src/StandWebServer.cpp +++ b/src/StandWebServer.cpp @@ -98,6 +98,7 @@ void standWebServerInit() { } bool handleFileRead(String path) { + path = "/" + path; if (path.endsWith("/")) path += "index.html"; String contentType = getContentType(path); String pathWithGz = path + ".gz"; @@ -211,7 +212,7 @@ void handleFileList() { entry.close(); } output += "]"; - Serial.println(output); + //Serial.println(output); HTTP.send(200, "text/json", output); } From bd48be37860c4bb02290a1470933aa5b53656986 Mon Sep 17 00:00:00 2001 From: avaksru Date: Mon, 31 Oct 2022 10:45:00 +0300 Subject: [PATCH 068/107] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20?= =?UTF-8?q?BLE=20(=20bluetooth)=20=D0=A1=D0=BA=D0=B0=D0=BD=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myProfile.json | 4 + src/modules/sensors/Ble/Ble.cpp | 248 +++++++++++++++++++++++++++ src/modules/sensors/Ble/modinfo.json | 64 +++++++ 3 files changed, 316 insertions(+) create mode 100644 src/modules/sensors/Ble/Ble.cpp create mode 100644 src/modules/sensors/Ble/modinfo.json diff --git a/myProfile.json b/myProfile.json index 3c0ddde3..1889e081 100644 --- a/myProfile.json +++ b/myProfile.json @@ -67,6 +67,10 @@ "path": "src/modules/sensors/AnalogAdc", "active": true }, + { + "path": "src/modules/sensors/Ble", + "active": false + }, { "path": "src/modules/sensors/Bme280", "active": true diff --git a/src/modules/sensors/Ble/Ble.cpp b/src/modules/sensors/Ble/Ble.cpp new file mode 100644 index 00000000..8632fd97 --- /dev/null +++ b/src/modules/sensors/Ble/Ble.cpp @@ -0,0 +1,248 @@ +#include "Global.h" +#include "classes/IoTItem.h" +#include +#ifdef ESP32 +#include +#include + +// Создаем переменную для хранения данных с датчиков bluetooth +StaticJsonDocument BLEbuffer; +JsonObject extBLEdata = BLEbuffer.to(); + +BLEScan *pBLEScan; +TheengsDecoder decoder; +StaticJsonDocument<512> doc; + +class BleScan : public IoTItem, BLEAdvertisedDeviceCallbacks +{ +private: + //описание параметров передаваемых из настроек датчика из веба + int _scanDuration; + String _filter; + +public: + //======================================================================================================= + std::string convertServiceData(std::string deviceServiceData) + { + int serviceDataLength = (int)deviceServiceData.length(); + char spr[2 * serviceDataLength + 1]; + for (int i = 0; i < serviceDataLength; i++) + sprintf(spr + 2 * i, "%.2x", (unsigned char)deviceServiceData[i]); + spr[2 * serviceDataLength] = 0; + return spr; + } + + void onResult(BLEAdvertisedDevice *advertisedDevice) + { + JsonObject BLEdata = doc.to(); + String mac_adress_ = advertisedDevice->getAddress().toString().c_str(); + mac_adress_.toUpperCase(); + BLEdata["id"] = (char *)mac_adress_.c_str(); + + if (advertisedDevice->haveName()) + { + BLEdata["name"] = (char *)advertisedDevice->getName().c_str(); + } + if (advertisedDevice->haveManufacturerData()) + { + char *manufacturerdata = BLEUtils::buildHexData(NULL, (uint8_t *)advertisedDevice->getManufacturerData().data(), advertisedDevice->getManufacturerData().length()); + BLEdata["manufacturerdata"] = manufacturerdata; + free(manufacturerdata); + } + if (advertisedDevice->haveRSSI()) + BLEdata["rssi"] = (int)advertisedDevice->getRSSI(); + if (advertisedDevice->haveTXPower()) + BLEdata["txpower"] = (int8_t)advertisedDevice->getTXPower(); + if (advertisedDevice->haveServiceData()) + { + int serviceDataCount = advertisedDevice->getServiceDataCount(); + for (int j = 0; j < serviceDataCount; j++) + { + std::string service_data = convertServiceData(advertisedDevice->getServiceData(j)); + BLEdata["servicedata"] = (char *)service_data.c_str(); + std::string serviceDatauuid = advertisedDevice->getServiceDataUUID(j).toString(); + BLEdata["servicedatauuid"] = (char *)serviceDatauuid.c_str(); + } + } + + if (decoder.decodeBLEJson(BLEdata)) + { + + BLEdata.remove("manufacturerdata"); + BLEdata.remove("servicedata"); + + String mac_address = BLEdata["id"].as(); + mac_address.replace(":", ""); + + if (_filter != "") + { + if (BLEdata[_filter]) + { + for (JsonPair kv : BLEdata) + { + extBLEdata[mac_address][kv.key()] = BLEdata[kv.key()]; + } + + // дописываем время прихода пакета данных + extBLEdata[mac_address]["last"] = millis(); + } + } + else + { + for (JsonPair kv : BLEdata) + { + extBLEdata[mac_address][kv.key()] = BLEdata[kv.key()]; + } + // дописываем время прихода пакета данных + extBLEdata[mac_address]["last"] = millis(); + } + }; + } + + BleScan(String parameters) : IoTItem(parameters) + { + _scanDuration = jsonReadInt(parameters, "scanDuration"); + _filter = jsonReadStr(parameters, "filter"); + + if (pBLEScan->isScanning() == false) + { + SerialPrint("i", F("BLE"), "Start Scanning..."); + BLEDevice::init(""); + pBLEScan = BLEDevice::getScan(); // create new scan + pBLEScan->setAdvertisedDeviceCallbacks(this); + pBLEScan->setActiveScan(false); // active scan uses more power, but get results faster + pBLEScan->setInterval(100); + pBLEScan->setWindow(99); // less or equal setInterval value + } + } + + //======================================================================================================= + + // doByInterval() + void doByInterval() + { + + if (_scanDuration > 0) + { + BLEScanResults foundDevices = pBLEScan->start(_scanDuration, true); + int count = foundDevices.getCount(); + SerialPrint("i", F("BLE"), "Devices found: " + String(count)); + SerialPrint("i", F("BLE"), "Scan done!"); + pBLEScan->clearResults(); + } + for (JsonPair kv : extBLEdata) + { + String val = extBLEdata[kv.key()].as(); + SerialPrint("i", F("BLE"), _id + " " + kv.key().c_str() + " " + val); + } + } + + //======================================================================================================= + + ~BleScan(){}; +}; + +class BleSens : public IoTItem +{ +private: + //описание параметров передаваемых из настроек датчика из веба + String _MAC; + String _sensor; + +public: + //======================================================================================================= + char *TimeToString(unsigned long t) + { + static char str[12]; + long h = t / 3600; + t = t % 3600; + int m = t / 60; + int s = t % 60; + sprintf(str, "%02ld:%02d:%02d", h, m, s); + return str; + } + + BleSens(String parameters) : IoTItem(parameters) + { + _MAC = jsonReadStr(parameters, "MAC"); + _sensor = jsonReadStr(parameters, "sensor"); + } + + //======================================================================================================= + + // doByInterval() + void doByInterval() + { + if (_sensor == "last") + { + int valInt = extBLEdata[_MAC][_sensor].as(); + char *s; + s = TimeToString(millis() / 1000 - valInt / 1000); + value.isDecimal = 0; + if (valInt > 0) + { + value.valS = s; + } + else + { + value.valS = ""; + } + regEvent(value.valS, _id); + } + else + { + String valStr = extBLEdata[_MAC][_sensor].as(); + if (valStr != "null") + { + if (value.isDecimal = isDigitDotCommaStr(valStr)) + { + value.isDecimal = 1; + value.valD = valStr.toFloat(); + regEvent(value.valD, _id); + } + else + { + value.isDecimal = 0; + value.valS = valStr; + regEvent(value.valS, _id); + } + } + else + { + value.isDecimal = 0; + value.valS = ""; + regEvent(value.valS, _id); + } + } + } + //======================================================================================================= + + ~BleSens(){}; +}; +#endif + +// Заглушка для ESP8266 +#ifdef ESP8266 +class Ble : public IoTItem +{ +private: +public: + Ble(String parameters) : IoTItem(parameters) {} +}; +#endif + +void *getAPI_Ble(String subtype, String param) +{ + if (subtype == F("BleScan")) + { + return new BleScan(param); + } + else if (subtype == F("BleSens")) + { + return new BleSens(param); + } + else + { + return nullptr; + } +} diff --git a/src/modules/sensors/Ble/modinfo.json b/src/modules/sensors/Ble/modinfo.json new file mode 100644 index 00000000..fda2ea7a --- /dev/null +++ b/src/modules/sensors/Ble/modinfo.json @@ -0,0 +1,64 @@ +{ + "menuSection": "Сенсоры", + "configItem": [ + { + "name": "bluetooth сканер", + "num": 1, + "type": "Reading", + "subtype": "BleScan", + "id": "BleScan", + "widget": "na", + "page": "", + "descr": "", + "int": 135, + "scanDuration": 10, + "filter": "servicedatauuid" + }, + { + "name": "bluetooth датчик", + "num": 1, + "type": "Reading", + "subtype": "BleSens", + "id": "BleSens", + "widget": "anydataDef", + "page": "Сенсоры", + "descr": "", + "needSave": 0, + "global": 0, + "round": 1, + "int": 60, + "MAC": "", + "sensor": "" + } + ], + "about": { + "authorName": "AVAKS", + "authorContact": "https://t.me/@avaks_dev", + "authorGit": "https://github.com/avaksru", + "specialThanks": "@Serghei63", + "moduleName": "Ble", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 1261449, + "esp8266_4mb": 0 + }, + "subTypes": ["BleSens", "BleScan"], + "title": "Сканер Bluetooth", + "moduleDesc": "Позволяет получить данные с Bluetooth часов и термометров Mijia, Xiaomi, Cleargrass, ...", + "propInfo": { + "round": "Округление после запятой.", + "int": "Интервал сканирования BLE окружения (BleScan) / Интервал отправки собранной телеметрии в MQTT (BleSens)", + "scanDuration": "Длительность сканирования ", + "filter": "Позволяет установить фильтр по параметру передаваемому датчиком. Данные будут считываться только с датчиков у которых есть передаваемый параметр указанный в фильтре", + "MAC": "MAC адрес беспроводного датчика", + "sensor": "Тип сенсора: температура / влажность / время / ... " + } + }, + "defActive": false, + "usedLibs": { + "esp32_4mb": [ + "https://github.com/h2zero/NimBLE-Arduino.git", + "https://github.com/avaksru/decoder.git" + ] + } +} From 868c7bf4f292f7f7cf093475810564f6228f5162 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 31 Oct 2022 19:33:01 +0300 Subject: [PATCH 069/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BF=D0=BB=D0=B0=D0=BD=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D1=89=D0=B8=D0=BA=20=D0=BF=D0=BE=20=D1=82=D0=B8=D0=BF?= =?UTF-8?q?=D1=83=20Cron?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 211 +-- platformio.ini | 3 + src/modules/API.cpp | 2 + src/modules/virtual/Cron/Cron.cpp | 111 ++ .../virtual/Cron/ccronexpr/ccronexpr.c | 1272 +++++++++++++++++ .../virtual/Cron/ccronexpr/ccronexpr.h | 95 ++ src/modules/virtual/Cron/modinfo.json | 57 + 7 files changed, 1653 insertions(+), 98 deletions(-) create mode 100644 src/modules/virtual/Cron/Cron.cpp create mode 100644 src/modules/virtual/Cron/ccronexpr/ccronexpr.c create mode 100644 src/modules/virtual/Cron/ccronexpr/ccronexpr.h create mode 100644 src/modules/virtual/Cron/modinfo.json diff --git a/data_svelte/items.json b/data_svelte/items.json index 435182da..8f52d3ce 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -8,28 +8,43 @@ }, { "global": 0, - "name": "1. График", + "name": "1. Будильник (Cron)", + "type": "Writing", + "subtype": "Cron", + "id": "cron", + "widget": "anydataDef", + "page": "Таймеры", + "descr": "Будильник", + "int": 1, + "val": "*/15 * * * * *", + "formatNextAlarm": "dd.mm.yy hh:mm:ss", + "needSave": 0, + "num": 1 + }, + { + "global": 0, + "name": "2. График", "type": "Writing", "subtype": "Loging", "id": "log", "widget": "chart2", "page": "Графики", "descr": "Температура", - "num": 1, + "num": 2, "int": 5, "logid": "t", "points": 300 }, { "global": 0, - "name": "2. График дневного расхода", + "name": "3. График дневного расхода", "type": "Writing", "subtype": "LogingDaily", "id": "log", "widget": "chart3", "page": "Графики", "descr": "Температура", - "num": 2, + "num": 3, "int": 1, "logid": "t", "points": 365, @@ -37,7 +52,7 @@ }, { "global": 0, - "name": "3. Таймер", + "name": "4. Таймер", "type": "Writing", "subtype": "Timer", "id": "timer", @@ -49,11 +64,11 @@ "ticker": 1, "repeat": 1, "needSave": 0, - "num": 3 + "num": 4 }, { "global": 0, - "name": "4. Окно ввода числа (переменная)", + "name": "5. Окно ввода числа (переменная)", "type": "Reading", "subtype": "Variable", "id": "value", @@ -63,11 +78,11 @@ "descr": "Введите число", "int": "0", "val": "0.0", - "num": 4 + "num": 5 }, { "global": 0, - "name": "5. Окно ввода времени", + "name": "6. Окно ввода времени", "type": "Reading", "subtype": "Variable", "id": "time", @@ -77,11 +92,11 @@ "descr": "Введите время", "int": "0", "val": "02:00", - "num": 5 + "num": 6 }, { "global": 0, - "name": "6. Окно ввода даты", + "name": "7. Окно ввода даты", "type": "Reading", "subtype": "Variable", "id": "time", @@ -91,11 +106,11 @@ "descr": "Введите дату", "int": "0", "val": "24.05.2022", - "num": 6 + "num": 7 }, { "global": 0, - "name": "7. Окно ввода текста", + "name": "8. Окно ввода текста", "type": "Reading", "subtype": "Variable", "id": "txt", @@ -105,11 +120,11 @@ "descr": "Введите текст", "int": "0", "val": "текст", - "num": 7 + "num": 8 }, { "global": 0, - "name": "8. Виртуальная кнопка", + "name": "9. Виртуальная кнопка", "type": "Reading", "subtype": "VButton", "id": "vbtn", @@ -119,13 +134,13 @@ "descr": "Кнопка", "int": "0", "val": "0", - "num": 8 + "num": 9 }, { "header": "Сенсоры" }, { - "name": "9. Acs712 Ток", + "name": "10. Acs712 Ток", "type": "Reading", "subtype": "Acs712", "id": "amp", @@ -135,11 +150,11 @@ "round": 3, "pin": 39, "int": 5, - "num": 9 + "num": 10 }, { "global": 0, - "name": "10. AHTXX Температура", + "name": "11. AHTXX Температура", "type": "Reading", "subtype": "AhtXXt", "id": "Temp20", @@ -150,11 +165,11 @@ "addr": "0x38", "shtType": 1, "round": 1, - "num": 10 + "num": 11 }, { "global": 0, - "name": "11. AHTXX Влажность", + "name": "12. AHTXX Влажность", "type": "Reading", "subtype": "AhtXXh", "id": "Hum20", @@ -165,11 +180,11 @@ "addr": "0x38", "shtType": 1, "round": 1, - "num": 11 + "num": 12 }, { "global": 0, - "name": "12. Аналоговый сенсор", + "name": "13. Аналоговый сенсор", "type": "Reading", "subtype": "AnalogAdc", "id": "t", @@ -183,11 +198,11 @@ "pin": 0, "int": 15, "avgSteps": 1, - "num": 12 + "num": 13 }, { "global": 0, - "name": "13. BME280 Температура", + "name": "14. BME280 Температура", "type": "Reading", "subtype": "Bme280t", "id": "tmp3", @@ -197,11 +212,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 13 + "num": 14 }, { "global": 0, - "name": "14. BME280 Давление", + "name": "15. BME280 Давление", "type": "Reading", "subtype": "Bme280p", "id": "Press3", @@ -211,11 +226,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 14 + "num": 15 }, { "global": 0, - "name": "15. BME280 Влажность", + "name": "16. BME280 Влажность", "type": "Reading", "subtype": "Bme280h", "id": "Hum3", @@ -225,11 +240,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 15 + "num": 16 }, { "global": 0, - "name": "16. BMP280 Температура", + "name": "17. BMP280 Температура", "type": "Reading", "subtype": "Bmp280t", "id": "tmp3", @@ -239,11 +254,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 16 + "num": 17 }, { "global": 0, - "name": "17. BMP280 Давление", + "name": "18. BMP280 Давление", "type": "Reading", "subtype": "Bmp280p", "id": "Press3", @@ -253,11 +268,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 17 + "num": 18 }, { "global": 0, - "name": "18. DHT11 Температура", + "name": "19. DHT11 Температура", "type": "Reading", "subtype": "Dht1122t", "id": "tmp3", @@ -267,11 +282,11 @@ "int": 15, "pin": 0, "senstype": "dht11", - "num": 18 + "num": 19 }, { "global": 0, - "name": "19. DHT11 Влажность", + "name": "20. DHT11 Влажность", "type": "Reading", "subtype": "Dht1122h", "id": "Hum3", @@ -281,11 +296,11 @@ "int": 15, "pin": 0, "senstype": "dht11", - "num": 19 + "num": 20 }, { "global": 0, - "name": "20. DS18B20 Температура", + "name": "21. DS18B20 Температура", "type": "Reading", "subtype": "Ds18b20", "id": "dstmp", @@ -297,11 +312,11 @@ "index": 0, "addr": "", "round": 1, - "num": 20 + "num": 21 }, { "global": 0, - "name": "21. GY21 Температура", + "name": "22. GY21 Температура", "type": "Reading", "subtype": "GY21t", "id": "tmp4", @@ -310,11 +325,11 @@ "descr": "Температура", "round": 1, "int": 15, - "num": 21 + "num": 22 }, { "global": 0, - "name": "22. GY21 Влажность", + "name": "23. GY21 Влажность", "type": "Reading", "subtype": "GY21h", "id": "Hum4", @@ -323,11 +338,11 @@ "descr": "Влажность", "round": 1, "int": 15, - "num": 22 + "num": 23 }, { "global": 0, - "name": "23. HDC1080 Температура", + "name": "24. HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", "id": "Temp1080", @@ -337,11 +352,11 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 23 + "num": 24 }, { "global": 0, - "name": "24. HDC1080 Влажность", + "name": "25. HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", "id": "Hum1080", @@ -351,11 +366,11 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 24 + "num": 25 }, { "global": 0, - "name": "25. MAX6675 Температура", + "name": "26. MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", "id": "maxtmp", @@ -366,11 +381,11 @@ "DO": 12, "CS": 13, "CLK": 14, - "num": 25 + "num": 26 }, { "global": 0, - "name": "26. PZEM 004t Напряжение", + "name": "27. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", "id": "v", @@ -380,11 +395,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 26 + "num": 27 }, { "global": 0, - "name": "27. PZEM 004t Сила тока", + "name": "28. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", "id": "a", @@ -394,11 +409,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 27 + "num": 28 }, { "global": 0, - "name": "28. PZEM 004t Мощность", + "name": "29. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", "id": "w", @@ -408,11 +423,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 28 + "num": 29 }, { "global": 0, - "name": "29. PZEM 004t Энергия", + "name": "30. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", "id": "wh", @@ -422,11 +437,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 29 + "num": 30 }, { "global": 0, - "name": "30. PZEM 004t Частота", + "name": "31. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", "id": "hz", @@ -436,11 +451,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 30 + "num": 31 }, { "global": 0, - "name": "31. PZEM 004t Косинус", + "name": "32. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", "id": "pf", @@ -450,12 +465,12 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 31 + "num": 32 }, { "global": 0, - "name": "32. Сканер кнопок 433 MHz", - "num": 32, + "name": "33. Сканер кнопок 433 MHz", + "num": 33, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -465,7 +480,7 @@ }, { "global": 0, - "name": "33. Sht20 Температура", + "name": "34. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", "id": "tmp2", @@ -474,11 +489,11 @@ "descr": "Температура", "int": 15, "round": 1, - "num": 33 + "num": 34 }, { "global": 0, - "name": "34. Sht20 Влажность", + "name": "35. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", "id": "Hum2", @@ -487,11 +502,11 @@ "descr": "Влажность", "int": 15, "round": 1, - "num": 34 + "num": 35 }, { "global": 0, - "name": "35. Sht30 Температура", + "name": "36. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", "id": "tmp30", @@ -500,11 +515,11 @@ "descr": "SHT30 Температура", "int": 15, "round": 1, - "num": 35 + "num": 36 }, { "global": 0, - "name": "36. Sht30 Влажность", + "name": "37. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", "id": "Hum30", @@ -513,12 +528,12 @@ "descr": "SHT30 Влажность", "int": 15, "round": 1, - "num": 36 + "num": 37 }, { "global": 0, - "name": "37. HC-SR04 Ультразвуковой дальномер", - "num": 37, + "name": "38. HC-SR04 Ультразвуковой дальномер", + "num": 38, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -531,7 +546,7 @@ }, { "global": 0, - "name": "38. UART", + "name": "39. UART", "type": "Reading", "subtype": "UART", "page": "", @@ -541,14 +556,14 @@ "tx": 12, "rx": 13, "speed": 9600, - "num": 38 + "num": 39 }, { "header": "Исполнительные устройства" }, { "global": 0, - "name": "39. Кнопка подключенная к пину", + "name": "40. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -562,11 +577,11 @@ "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 39 + "num": 40 }, { "global": 0, - "name": "40. Управление пином", + "name": "41. Управление пином", "type": "Writing", "subtype": "ButtonOut", "needSave": 0, @@ -577,11 +592,11 @@ "int": 0, "inv": 0, "pin": 2, - "num": 40 + "num": 41 }, { "global": 0, - "name": "41. Сервопривод", + "name": "42. Сервопривод", "type": "Writing", "subtype": "IoTServo", "id": "servo", @@ -592,11 +607,11 @@ "pin": 12, "apin": -1, "amap": "0, 4096, 0, 180", - "num": 41 + "num": 42 }, { "global": 0, - "name": "42. Расширитель портов Mcp23017", + "name": "43. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -606,11 +621,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 42 + "num": 43 }, { "global": 0, - "name": "43. MP3 плеер", + "name": "44. MP3 плеер", "type": "Reading", "subtype": "Mp3", "id": "mp3", @@ -620,11 +635,11 @@ "int": 1, "pins": "14,12", "volume": 20, - "num": 43 + "num": 44 }, { "global": 0, - "name": "44. Расширитель портов Pcf8574", + "name": "45. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -634,11 +649,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 44 + "num": 45 }, { "global": 0, - "name": "45. PWM ESP8266", + "name": "46. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -650,11 +665,11 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 45 + "num": 46 }, { "global": 0, - "name": "46. Телеграм-Лайт", + "name": "47. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -663,14 +678,14 @@ "descr": "", "token": "", "chatID": "", - "num": 46 + "num": 47 }, { "header": "Экраны" }, { "global": 0, - "name": "47. LCD экран 2004", + "name": "48. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -682,10 +697,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 47 + "num": 48 }, { - "name": "48. LCD экран 1602", + "name": "49. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -697,11 +712,11 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 48 + "num": 49 }, { "global": 0, - "name": "49. Strip ws2812b", + "name": "50. Strip ws2812b", "type": "Reading", "subtype": "Ws2812b", "id": "strip", @@ -717,6 +732,6 @@ "min": "15", "max": "30", "idshow": "t", - "num": 49 + "num": 50 } ] \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index fc750421..c68e8c20 100644 --- a/platformio.ini +++ b/platformio.ini @@ -158,6 +158,7 @@ lib_deps = marcoschwartz/LiquidCrystal_I2C@^1.1.4 adafruit/Adafruit NeoPixel@^1.10.6 build_src_filter = + + + + + @@ -210,6 +211,7 @@ lib_deps = dfrobot/DFRobotDFPlayerMini @ ^1.0.5 adafruit/Adafruit BusIO @ ^1.13.2 marcoschwartz/LiquidCrystal_I2C@^1.1.4 + adafruit/Adafruit NeoPixel@^1.10.6 build_src_filter = + + @@ -241,4 +243,5 @@ build_src_filter = + + + + + diff --git a/src/modules/API.cpp b/src/modules/API.cpp index 7523aff7..5756874b 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -1,5 +1,6 @@ #include "ESPConfiguration.h" +void* getAPI_Cron(String subtype, String params); void* getAPI_Loging(String subtype, String params); void* getAPI_LogingDaily(String subtype, String params); void* getAPI_Timer(String subtype, String params); @@ -34,6 +35,7 @@ void* getAPI_Ws2812b(String subtype, String params); void* getAPI(String subtype, String params) { void* tmpAPI; +if ((tmpAPI = getAPI_Cron(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Loging(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_LogingDaily(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Timer(subtype, params)) != nullptr) return tmpAPI; diff --git a/src/modules/virtual/Cron/Cron.cpp b/src/modules/virtual/Cron/Cron.cpp new file mode 100644 index 00000000..2c24eb59 --- /dev/null +++ b/src/modules/virtual/Cron/Cron.cpp @@ -0,0 +1,111 @@ +#include "NTP.h" +#include "Global.h" +#include "classes/IoTItem.h" + +extern "C" { +#include "ccronexpr/ccronexpr.h" +} + + +class Cron : public IoTItem { + private: + bool _pause = false; + String _format = ""; + cron_expr _expr; + time_t _nextAlarm = 0; + + public: + Cron(String parameters): IoTItem(parameters) { + jsonRead(parameters, F("formatNextAlarm"), _format); + initCron(); + } + + void initCron() { + const char* err = NULL; + memset(&_expr, 0, sizeof(_expr)); + + cron_parse_expr(value.valS.c_str(), &_expr, &err); + if (err) { + _pause = true; + _nextAlarm = 0; + memset(&_expr, 0, sizeof(_expr)); + SerialPrint("E", "Cron", F("The Cron string did not apply."), _id); + } else + updateNextAlarm(true); + } + + void updateNextAlarm(bool forced) { + if (!_pause && _time_isTrust) { + if (forced || (_nextAlarm <= gmtTimeToLocal(unixTime))) { + // update alarm if next trigger is not yet in the future + _nextAlarm = cron_next(&_expr, gmtTimeToLocal(unixTime)); + } + } + } + + String getNextAlarmF() { + if (_pause) return "Pause"; + if (!_time_isTrust) return "No time"; + struct tm* timeinfo; + char buffer [80]; + timeinfo = localtime(&_nextAlarm); + strftime(buffer, 80, _format.c_str(), timeinfo); + return buffer; + } + + String getValue() { + return getNextAlarmF(); + } + + void setValue(const IoTValue& Value, bool genEvent) { + value = Value; + _pause = false; + initCron(); + + if (_needSave) { + jsonWriteStr_(valuesFlashJson, _id, value.valS); + needSaveValues = true; + } + + bool _needSaveBak = _needSave; + _needSave = false; + regEvent(getNextAlarmF(), F("Cron alarm"), false, false); + _needSave = _needSaveBak; + } + + void doByInterval() { + if (!_pause && _time_isTrust && (gmtTimeToLocal(unixTime) >= _nextAlarm)) { + updateNextAlarm(true); + bool _needSaveBak = _needSave; + _needSave = false; + regEvent(getNextAlarmF(), F("Cron alarm")); + _needSave = _needSaveBak; + } + } + + IoTValue execute(String command, std::vector ¶m) { + if (command == "stop") { + _pause = true; + } else if (command == "continue") { + _pause = false; + updateNextAlarm(false); + } + + bool _needSaveBak = _needSave; + _needSave = false; + regEvent(getNextAlarmF(), F("Cron alarm"), false, false); + _needSave = _needSaveBak; + + return {}; + } + + ~Cron() {}; +}; + +void* getAPI_Cron(String subtype, String param) { + if (subtype == F("Cron")) { + return new Cron(param); + } else { + return nullptr; + } +} diff --git a/src/modules/virtual/Cron/ccronexpr/ccronexpr.c b/src/modules/virtual/Cron/ccronexpr/ccronexpr.c new file mode 100644 index 00000000..d9715ff7 --- /dev/null +++ b/src/modules/virtual/Cron/ccronexpr/ccronexpr.c @@ -0,0 +1,1272 @@ +/* + * Copyright 2015, alex at staticlibs.net + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * File: ccronexpr.c + * Author: alex + * + * Created on February 24, 2015, 9:35 AM + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "ccronexpr.h" + +#define CRON_MAX_SECONDS 60 +#define CRON_MAX_MINUTES 60 +#define CRON_MAX_HOURS 24 +#define CRON_MAX_DAYS_OF_WEEK 8 +#define CRON_MAX_DAYS_OF_MONTH 32 +#define CRON_MAX_MONTHS 12 +#define CRON_MAX_YEARS_DIFF 4 + +#define CRON_CF_SECOND 0 +#define CRON_CF_MINUTE 1 +#define CRON_CF_HOUR_OF_DAY 2 +#define CRON_CF_DAY_OF_WEEK 3 +#define CRON_CF_DAY_OF_MONTH 4 +#define CRON_CF_MONTH 5 +#define CRON_CF_YEAR 6 + +#define CRON_CF_ARR_LEN 7 + +#define CRON_INVALID_INSTANT ((time_t) -1) + +static const char* const DAYS_ARR[] = { "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" }; +#define CRON_DAYS_ARR_LEN 7 +static const char* const MONTHS_ARR[] = { "FOO", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" }; +#define CRON_MONTHS_ARR_LEN 13 + +#define CRON_MAX_STR_LEN_TO_SPLIT 256 +#define CRON_MAX_NUM_TO_SRING 1000000000 +/* computes number of digits in decimal number */ +#define CRON_NUM_OF_DIGITS(num) (abs(num) < 10 ? 1 : \ + (abs(num) < 100 ? 2 : \ + (abs(num) < 1000 ? 3 : \ + (abs(num) < 10000 ? 4 : \ + (abs(num) < 100000 ? 5 : \ + (abs(num) < 1000000 ? 6 : \ + (abs(num) < 10000000 ? 7 : \ + (abs(num) < 100000000 ? 8 : \ + (abs(num) < 1000000000 ? 9 : 10))))))))) + +#ifndef CRON_TEST_MALLOC +#define cron_malloc(x) malloc(x); +#define cron_free(x) free(x); +#else /* CRON_TEST_MALLOC */ +void* cron_malloc(size_t n); +void cron_free(void* p); +#endif /* CRON_TEST_MALLOC */ + +/** + * Time functions from standard library. + * This part defines: cron_mktime: create time_t from tm + * cron_time: create tm from time_t + */ + +/* forward declarations for platforms that may need them */ +/* can be hidden in time.h */ +#if !defined(_WIN32) && !defined(__AVR__) && !defined(ESP8266) && !defined(ANDROID) +struct tm *gmtime_r(const time_t *timep, struct tm *result); +time_t timegm(struct tm* __tp); +struct tm *localtime_r(const time_t *timep, struct tm *result); +#endif /* PLEASE CHECK _WIN32 AND ANDROID NEEDS FOR THESE DECLARATIONS */ +#ifdef __MINGW32__ +/* To avoid warning when building with mingw */ +time_t _mkgmtime(struct tm* tm); +#endif /* __MINGW32__ */ + +/* function definitions */ +time_t cron_mktime_gm(struct tm* tm) { +#if defined(_WIN32) +/* http://stackoverflow.com/a/22557778 */ + return _mkgmtime(tm); +#elif defined(__AVR__) +/* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ + return mk_gmtime(tm); +#elif defined(ESP8266) + /* https://linux.die.net/man/3/timegm */ + /* http://www.catb.org/esr/time-programming/ */ + /* portable version of timegm() */ + time_t ret; + char *tz; + tz = getenv("TZ"); + if (tz) + tz = strdup(tz); + setenv("TZ", "UTC+0", 1); + tzset(); + ret = mktime(tm); + if (tz) { + setenv("TZ", tz, 1); + free(tz); + } else + unsetenv("TZ"); + tzset(); + return ret; +#elif defined(ANDROID) + /* https://github.com/adobe/chromium/blob/cfe5bf0b51b1f6b9fe239c2a3c2f2364da9967d7/base/os_compat_android.cc#L20 */ + static const time_t kTimeMax = ~(1L << (sizeof (time_t) * CHAR_BIT - 1)); + static const time_t kTimeMin = (1L << (sizeof (time_t) * CHAR_BIT - 1)); + time64_t result = timegm64(tm); + if (result < kTimeMin || result > kTimeMax) return -1; + return result; +#else + return timegm(tm); +#endif +} + +struct tm* cron_time_gm(time_t* date, struct tm* out) { +#if defined(__MINGW32__) + (void)(out); /* To avoid unused warning */ + return gmtime(date); +#elif defined(_WIN32) + errno_t err = gmtime_s(out, date); + return 0 == err ? out : NULL; +#elif defined(__AVR__) + /* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ + gmtime_r(date, out); + return out; +#else + return gmtime_r(date, out); +#endif +} + +time_t cron_mktime_local(struct tm* tm) { + tm->tm_isdst = -1; + return mktime(tm); +} + +struct tm* cron_time_local(time_t* date, struct tm* out) { +#if defined(_WIN32) + errno_t err = localtime_s(out, date); + return 0 == err ? out : NULL; +#elif defined(__AVR__) + /* https://www.nongnu.org/avr-libc/user-manual/group__avr__time.html */ + localtime_r(date, out); + return out; +#else + return localtime_r(date, out); +#endif +} + +/* Defining 'cron_' time functions to use use UTC (default) or local time */ +#ifndef CRON_USE_LOCAL_TIME +time_t cron_mktime(struct tm* tm) { + return cron_mktime_gm(tm); +} + +struct tm* cron_time(time_t* date, struct tm* out) { + return cron_time_gm(date, out); +} + +#else /* CRON_USE_LOCAL_TIME */ +time_t cron_mktime(struct tm* tm) { + return cron_mktime_local(tm); +} + +struct tm* cron_time(time_t* date, struct tm* out) { + return cron_time_local(date, out); +} + +#endif /* CRON_USE_LOCAL_TIME */ + +/** + * Functions. + */ + +void cron_set_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + rbyte[j] |= (1 << k); +} + +void cron_del_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + rbyte[j] &= ~(1 << k); +} + +uint8_t cron_get_bit(uint8_t* rbyte, int idx) { + uint8_t j = (uint8_t) (idx / 8); + uint8_t k = (uint8_t) (idx % 8); + + if (rbyte[j] & (1 << k)) { + return 1; + } else { + return 0; + } +} + +static void free_splitted(char** splitted, size_t len) { + size_t i; + if (!splitted) return; + for (i = 0; i < len; i++) { + if (splitted[i]) { + cron_free(splitted[i]); + } + } + cron_free(splitted); +} + +static char* strdupl(const char* str, size_t len) { + if (!str) return NULL; + char* res = (char*) cron_malloc(len + 1); + if (!res) return NULL; + memset(res, 0, len + 1); + memcpy(res, str, len); + return res; +} + +static unsigned int next_set_bit(uint8_t* bits, unsigned int max, unsigned int from_index, int* notfound) { + unsigned int i; + if (!bits) { + *notfound = 1; + return 0; + } + for (i = from_index; i < max; i++) { + if (cron_get_bit(bits, i)) return i; + } + *notfound = 1; + return 0; +} + +static void push_to_fields_arr(int* arr, int fi) { + int i; + if (!arr || -1 == fi) { + return; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (arr[i] == fi) return; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 == arr[i]) { + arr[i] = fi; + return; + } + } +} + +static int add_to_field(struct tm* calendar, int field, int val) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = calendar->tm_sec + val; + break; + case CRON_CF_MINUTE: + calendar->tm_min = calendar->tm_min + val; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = calendar->tm_hour + val; + break; + case CRON_CF_DAY_OF_WEEK: /* mkgmtime ignores this field */ + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = calendar->tm_mday + val; + break; + case CRON_CF_MONTH: + calendar->tm_mon = calendar->tm_mon + val; + break; + case CRON_CF_YEAR: + calendar->tm_year = calendar->tm_year + val; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +/** + * Reset the calendar setting all the fields provided to zero. + */ +static int reset_min(struct tm* calendar, int field) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = 0; + break; + case CRON_CF_MINUTE: + calendar->tm_min = 0; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = 0; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = 0; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = 1; + break; + case CRON_CF_MONTH: + calendar->tm_mon = 0; + break; + case CRON_CF_YEAR: + calendar->tm_year = 0; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +static int reset_all_min(struct tm* calendar, int* fields) { + int i; + int res = 0; + if (!calendar || !fields) { + return 1; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 != fields[i]) { + res = reset_min(calendar, fields[i]); + if (0 != res) return res; + } + } + return 0; +} + +static int set_field(struct tm* calendar, int field, int val) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = val; + break; + case CRON_CF_MINUTE: + calendar->tm_min = val; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = val; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = val; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = val; + break; + case CRON_CF_MONTH: + calendar->tm_mon = val; + break; + case CRON_CF_YEAR: + calendar->tm_year = val; + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +/** + * Search the bits provided for the next set bit after the value provided, + * and reset the calendar. + */ +static unsigned int find_next(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { + int notfound = 0; + int err = 0; + unsigned int next_value = next_set_bit(bits, max, value, ¬found); + /* roll over if needed */ + if (notfound) { + err = add_to_field(calendar, nextField, 1); + if (err) goto return_error; + err = reset_min(calendar, field); + if (err) goto return_error; + notfound = 0; + next_value = next_set_bit(bits, max, 0, ¬found); + } + if (notfound || next_value != value) { + err = set_field(calendar, field, next_value); + if (err) goto return_error; + err = reset_all_min(calendar, lower_orders); + if (err) goto return_error; + } + return next_value; + + return_error: + *res_out = 1; + return 0; +} + +static unsigned int find_next_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { + int err; + unsigned int count = 0; + unsigned int max = 366; + while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { + err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, 1); + + if (err) goto return_error; + day_of_month = calendar->tm_mday; + day_of_week = calendar->tm_wday; + reset_all_min(calendar, resets); + } + return day_of_month; + + return_error: + *res_out = 1; + return 0; +} + +static int do_next(cron_expr* expr, struct tm* calendar, unsigned int dot) { + int i; + int res = 0; + int* resets = NULL; + int* empty_list = NULL; + unsigned int second = 0; + unsigned int update_second = 0; + unsigned int minute = 0; + unsigned int update_minute = 0; + unsigned int hour = 0; + unsigned int update_hour = 0; + unsigned int day_of_week = 0; + unsigned int day_of_month = 0; + unsigned int update_day_of_month = 0; + unsigned int month = 0; + unsigned int update_month = 0; + + resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!resets) goto return_result; + empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!empty_list) goto return_result; + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + resets[i] = -1; + empty_list[i] = -1; + } + + second = calendar->tm_sec; + update_second = find_next(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); + if (0 != res) goto return_result; + if (second == update_second) { + push_to_fields_arr(resets, CRON_CF_SECOND); + } + + minute = calendar->tm_min; + update_minute = find_next(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); + if (0 != res) goto return_result; + if (minute == update_minute) { + push_to_fields_arr(resets, CRON_CF_MINUTE); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + hour = calendar->tm_hour; + update_hour = find_next(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); + if (0 != res) goto return_result; + if (hour == update_hour) { + push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + day_of_week = calendar->tm_wday; + day_of_month = calendar->tm_mday; + update_day_of_month = find_next_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); + if (0 != res) goto return_result; + if (day_of_month == update_day_of_month) { + push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); + } else { + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + + month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ + update_month = find_next(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); + if (0 != res) goto return_result; + if (month != update_month) { + if (calendar->tm_year - dot > 4) { + res = -1; + goto return_result; + } + res = do_next(expr, calendar, dot); + if (0 != res) goto return_result; + } + goto return_result; + + return_result: + if (!resets || !empty_list) { + res = -1; + } + if (resets) { + cron_free(resets); + } + if (empty_list) { + cron_free(empty_list); + } + return res; +} + +static int to_upper(char* str) { + if (!str) return 1; + int i; + for (i = 0; '\0' != str[i]; i++) { + int c = (int)str[i]; + str[i] = (char) toupper(c); + } + return 0; +} + +static char* to_string(int num) { + if (abs(num) >= CRON_MAX_NUM_TO_SRING) return NULL; + char* str = (char*) cron_malloc(CRON_NUM_OF_DIGITS(num) + 1); + if (!str) return NULL; + int res = sprintf(str, "%d", num); + if (res < 0) { + cron_free(str); + return NULL; + } + return str; +} + +static char* str_replace(char *orig, const char *rep, const char *with) { + char *result; /* the return string */ + char *ins; /* the next insert point */ + char *tmp; /* varies */ + size_t len_rep; /* length of rep */ + size_t len_with; /* length of with */ + size_t len_front; /* distance between rep and end of last rep */ + int count; /* number of replacements */ + if (!orig) return NULL; + if (!rep) rep = ""; + if (!with) with = ""; + len_rep = strlen(rep); + len_with = strlen(with); + + ins = orig; + for (count = 0; NULL != (tmp = strstr(ins, rep)); ++count) { + ins = tmp + len_rep; + } + + /* first time through the loop, all the variable are set correctly + from here on, + tmp points to the end of the result string + ins points to the next occurrence of rep in orig + orig points to the remainder of orig after "end of rep" + */ + tmp = result = (char*) cron_malloc(strlen(orig) + (len_with - len_rep) * count + 1); + if (!result) return NULL; + + while (count--) { + ins = strstr(orig, rep); + len_front = ins - orig; + tmp = strncpy(tmp, orig, len_front) + len_front; + tmp = strcpy(tmp, with) + len_with; + orig += len_front + len_rep; /* move to next "end of rep" */ + } + strcpy(tmp, orig); + return result; +} + +static unsigned int parse_uint(const char* str, int* errcode) { + char* endptr; + errno = 0; + long int l = strtol(str, &endptr, 0); + if (errno == ERANGE || *endptr != '\0' || l < 0 || l > INT_MAX) { + *errcode = 1; + return 0; + } else { + *errcode = 0; + return (unsigned int) l; + } +} + +static char** split_str(const char* str, char del, size_t* len_out) { + size_t i; + size_t stlen = 0; + size_t len = 0; + int accum = 0; + char* buf = NULL; + char** res = NULL; + size_t bi = 0; + size_t ri = 0; + char* tmp; + + if (!str) goto return_error; + for (i = 0; '\0' != str[i]; i++) { + stlen += 1; + if (stlen >= CRON_MAX_STR_LEN_TO_SPLIT) goto return_error; + } + + for (i = 0; i < stlen; i++) { + int c = str[i]; + if (del == str[i]) { + if (accum > 0) { + len += 1; + accum = 0; + } + } else if (!isspace(c)) { + accum += 1; + } + } + /* tail */ + if (accum > 0) { + len += 1; + } + if (0 == len) return NULL; + + buf = (char*) cron_malloc(stlen + 1); + if (!buf) goto return_error; + memset(buf, 0, stlen + 1); + res = (char**) cron_malloc(len * sizeof(char*)); + if (!res) goto return_error; + memset(res, 0, len * sizeof(char*)); + + for (i = 0; i < stlen; i++) { + int c = str[i]; + if (del == str[i]) { + if (bi > 0) { + tmp = strdupl(buf, bi); + if (!tmp) goto return_error; + res[ri++] = tmp; + memset(buf, 0, stlen + 1); + bi = 0; + } + } else if (!isspace(c)) { + buf[bi++] = str[i]; + } + } + /* tail */ + if (bi > 0) { + tmp = strdupl(buf, bi); + if (!tmp) goto return_error; + res[ri++] = tmp; + } + cron_free(buf); + *len_out = len; + return res; + + return_error: + if (buf) { + cron_free(buf); + } + free_splitted(res, len); + *len_out = 0; + return NULL; +} + +static char* replace_ordinals(char* value, const char* const * arr, size_t arr_len) { + size_t i; + char* cur = value; + char* res = NULL; + int first = 1; + for (i = 0; i < arr_len; i++) { + char* strnum = to_string((int) i); + if (!strnum) { + if (!first) { + cron_free(cur); + } + return NULL; + } + res = str_replace(cur, arr[i], strnum); + cron_free(strnum); + if (!first) { + cron_free(cur); + } + if (!res) { + return NULL; + } + cur = res; + if (first) { + first = 0; + } + } + return res; +} + +static int has_char(char* str, char ch) { + size_t i; + size_t len = 0; + if (!str) return 0; + len = strlen(str); + for (i = 0; i < len; i++) { + if (str[i] == ch) return 1; + } + return 0; +} + +static unsigned int* get_range(char* field, unsigned int min, unsigned int max, const char** error) { + + char** parts = NULL; + size_t len = 0; + unsigned int* res = (unsigned int*) cron_malloc(2 * sizeof(unsigned int)); + if (!res) goto return_error; + + res[0] = 0; + res[1] = 0; + if (1 == strlen(field) && '*' == field[0]) { + res[0] = min; + res[1] = max - 1; + } else if (!has_char(field, '-')) { + int err = 0; + unsigned int val = parse_uint(field, &err); + if (err) { + *error = "Unsigned integer parse error 1"; + goto return_error; + } + + res[0] = val; + res[1] = val; + } else { + parts = split_str(field, '-', &len); + if (2 != len) { + *error = "Specified range requires two fields"; + goto return_error; + } + int err = 0; + res[0] = parse_uint(parts[0], &err); + if (err) { + *error = "Unsigned integer parse error 2"; + goto return_error; + } + res[1] = parse_uint(parts[1], &err); + if (err) { + *error = "Unsigned integer parse error 3"; + goto return_error; + } + } + if (res[0] >= max || res[1] >= max) { + *error = "Specified range exceeds maximum"; + goto return_error; + } + if (res[0] < min || res[1] < min) { + *error = "Specified range is less than minimum"; + goto return_error; + } + if (res[0] > res[1]) { + *error = "Specified range start exceeds range end"; + goto return_error; + } + + free_splitted(parts, len); + *error = NULL; + return res; + + return_error: + free_splitted(parts, len); + if (res) { + cron_free(res); + } + + return NULL; +} + +static void set_number_hits(const char* value, uint8_t* target, unsigned int min, unsigned int max, const char** error) { + size_t i; + unsigned int i1; + size_t len = 0; + + char** fields = split_str(value, ',', &len); + if (!fields) { + *error = "Comma split error"; + goto return_result; + } + + for (i = 0; i < len; i++) { + if (!has_char(fields[i], '/')) { + /* Not an incrementer so it must be a range (possibly empty) */ + + unsigned int* range = get_range(fields[i], min, max, error); + + if (*error) { + if (range) { + cron_free(range); + } + goto return_result; + + } + + for (i1 = range[0]; i1 <= range[1]; i1++) { + cron_set_bit(target, i1); + + } + cron_free(range); + + } else { + size_t len2 = 0; + char** split = split_str(fields[i], '/', &len2); + if (2 != len2) { + *error = "Incrementer must have two fields"; + free_splitted(split, len2); + goto return_result; + } + unsigned int* range = get_range(split[0], min, max, error); + if (*error) { + if (range) { + cron_free(range); + } + free_splitted(split, len2); + goto return_result; + } + if (!has_char(split[0], '-')) { + range[1] = max - 1; + } + int err = 0; + unsigned int delta = parse_uint(split[1], &err); + if (err) { + *error = "Unsigned integer parse error 4"; + cron_free(range); + free_splitted(split, len2); + goto return_result; + } + if (0 == delta) { + *error = "Incrementer may not be zero"; + cron_free(range); + free_splitted(split, len2); + goto return_result; + } + for (i1 = range[0]; i1 <= range[1]; i1 += delta) { + cron_set_bit(target, i1); + } + free_splitted(split, len2); + cron_free(range); + + } + } + goto return_result; + + return_result: + free_splitted(fields, len); + +} + +static void set_months(char* value, uint8_t* targ, const char** error) { + unsigned int i; + unsigned int max = 12; + + char* replaced = NULL; + + to_upper(value); + replaced = replace_ordinals(value, MONTHS_ARR, CRON_MONTHS_ARR_LEN); + if (!replaced) { + *error = "Invalid month format"; + return; + } + set_number_hits(replaced, targ, 1, max + 1, error); + cron_free(replaced); + + /* ... and then rotate it to the front of the months */ + for (i = 1; i <= max; i++) { + if (cron_get_bit(targ, i)) { + cron_set_bit(targ, i - 1); + cron_del_bit(targ, i); + } + } +} + +static void set_days_of_week(char* field, uint8_t* targ, const char** error) { + unsigned int max = 7; + char* replaced = NULL; + + if (1 == strlen(field) && '?' == field[0]) { + field[0] = '*'; + } + to_upper(field); + replaced = replace_ordinals(field, DAYS_ARR, CRON_DAYS_ARR_LEN); + if (!replaced) { + *error = "Invalid day format"; + return; + } + set_number_hits(replaced, targ, 0, max + 1, error); + cron_free(replaced); + if (cron_get_bit(targ, 7)) { + /* Sunday can be represented as 0 or 7*/ + cron_set_bit(targ, 0); + cron_del_bit(targ, 7); + } +} + +static void set_days_of_month(char* field, uint8_t* targ, const char** error) { + /* Days of month start with 1 (in Cron and Calendar) so add one */ + if (1 == strlen(field) && '?' == field[0]) { + field[0] = '*'; + } + set_number_hits(field, targ, 1, CRON_MAX_DAYS_OF_MONTH, error); +} + +void cron_parse_expr(const char* expression, cron_expr* target, const char** error) { + const char* err_local; + size_t len = 0; + char** fields = NULL; + if (!error) { + error = &err_local; + } + *error = NULL; + if (!expression) { + *error = "Invalid NULL expression"; + goto return_res; + } + if (!target) { + *error = "Invalid NULL target"; + goto return_res; + } + + fields = split_str(expression, ' ', &len); + if (len != 6) { + *error = "Invalid number of fields, expression must consist of 6 fields"; + goto return_res; + } + memset(target, 0, sizeof(*target)); + set_number_hits(fields[0], target->seconds, 0, 60, error); + if (*error) goto return_res; + set_number_hits(fields[1], target->minutes, 0, 60, error); + if (*error) goto return_res; + set_number_hits(fields[2], target->hours, 0, 24, error); + if (*error) goto return_res; + set_days_of_month(fields[3], target->days_of_month, error); + if (*error) goto return_res; + set_months(fields[4], target->months, error); + if (*error) goto return_res; + set_days_of_week(fields[5], target->days_of_week, error); + if (*error) goto return_res; + + goto return_res; + + return_res: + free_splitted(fields, len); +} + +time_t cron_next(cron_expr* expr, time_t date) { + /* + The plan: + + 1 Round up to the next whole second + + 2 If seconds match move on, otherwise find the next match: + 2.1 If next match is in the next minute then roll forwards + + 3 If minute matches move on, otherwise find the next match + 3.1 If next match is in the next hour then roll forwards + 3.2 Reset the seconds and go to 2 + + 4 If hour matches move on, otherwise find the next match + 4.1 If next match is in the next day then roll forwards, + 4.2 Reset the minutes and seconds and go to 2 + + ... + */ + if (!expr) return CRON_INVALID_INSTANT; + struct tm calval; + memset(&calval, 0, sizeof(struct tm)); + struct tm* calendar = cron_time(&date, &calval); + if (!calendar) return CRON_INVALID_INSTANT; + time_t original = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; + + int res = do_next(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + + time_t calculated = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; + if (calculated == original) { + /* We arrived at the original timestamp - round up to the next whole second and try again... */ + res = add_to_field(calendar, CRON_CF_SECOND, 1); + if (0 != res) return CRON_INVALID_INSTANT; + res = do_next(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + } + + return cron_mktime(calendar); +} + + +/* https://github.com/staticlibs/ccronexpr/pull/8 */ + +static unsigned int prev_set_bit(uint8_t* bits, int from_index, int to_index, int* notfound) { + int i; + if (!bits) { + *notfound = 1; + return 0; + } + for (i = from_index; i >= to_index; i--) { + if (cron_get_bit(bits, i)) return i; + } + *notfound = 1; + return 0; +} + +static int last_day_of_month(int month, int year) { + struct tm cal; + time_t t; + memset(&cal,0,sizeof(cal)); + cal.tm_sec=0; + cal.tm_min=0; + cal.tm_hour=0; + cal.tm_mon = month+1; + cal.tm_mday = 0; + cal.tm_year=year; + t=mktime(&cal); + return gmtime(&t)->tm_mday; +} + +/** + * Reset the calendar setting all the fields provided to zero. + */ +static int reset_max(struct tm* calendar, int field) { + if (!calendar || -1 == field) { + return 1; + } + switch (field) { + case CRON_CF_SECOND: + calendar->tm_sec = 59; + break; + case CRON_CF_MINUTE: + calendar->tm_min = 59; + break; + case CRON_CF_HOUR_OF_DAY: + calendar->tm_hour = 23; + break; + case CRON_CF_DAY_OF_WEEK: + calendar->tm_wday = 6; + break; + case CRON_CF_DAY_OF_MONTH: + calendar->tm_mday = last_day_of_month(calendar->tm_mon, calendar->tm_year); + break; + case CRON_CF_MONTH: + calendar->tm_mon = 11; + break; + case CRON_CF_YEAR: + /* I don't think this is supposed to happen ... */ + fprintf(stderr, "reset CRON_CF_YEAR\n"); + break; + default: + return 1; /* unknown field */ + } + time_t res = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == res) { + return 1; + } + return 0; +} + +static int reset_all_max(struct tm* calendar, int* fields) { + int i; + int res = 0; + if (!calendar || !fields) { + return 1; + } + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + if (-1 != fields[i]) { + res = reset_max(calendar, fields[i]); + if (0 != res) return res; + } + } + return 0; +} + +/** + * Search the bits provided for the next set bit after the value provided, + * and reset the calendar. + */ +static unsigned int find_prev(uint8_t* bits, unsigned int max, unsigned int value, struct tm* calendar, unsigned int field, unsigned int nextField, int* lower_orders, int* res_out) { + int notfound = 0; + int err = 0; + unsigned int next_value = prev_set_bit(bits, value, 0, ¬found); + /* roll under if needed */ + if (notfound) { + err = add_to_field(calendar, nextField, -1); + if (err) goto return_error; + err = reset_max(calendar, field); + if (err) goto return_error; + notfound = 0; + next_value = prev_set_bit(bits, max - 1, value, ¬found); + } + if (notfound || next_value != value) { + err = set_field(calendar, field, next_value); + if (err) goto return_error; + err = reset_all_max(calendar, lower_orders); + if (err) goto return_error; + } + return next_value; + + return_error: + *res_out = 1; + return 0; +} + +static unsigned int find_prev_day(struct tm* calendar, uint8_t* days_of_month, unsigned int day_of_month, uint8_t* days_of_week, unsigned int day_of_week, int* resets, int* res_out) { + int err; + unsigned int count = 0; + unsigned int max = 366; + while ((!cron_get_bit(days_of_month, day_of_month) || !cron_get_bit(days_of_week, day_of_week)) && count++ < max) { + err = add_to_field(calendar, CRON_CF_DAY_OF_MONTH, -1); + + if (err) goto return_error; + day_of_month = calendar->tm_mday; + day_of_week = calendar->tm_wday; + reset_all_max(calendar, resets); + } + return day_of_month; + + return_error: + *res_out = 1; + return 0; +} + +static int do_prev(cron_expr* expr, struct tm* calendar, unsigned int dot) { + int i; + int res = 0; + int* resets = NULL; + int* empty_list = NULL; + unsigned int second = 0; + unsigned int update_second = 0; + unsigned int minute = 0; + unsigned int update_minute = 0; + unsigned int hour = 0; + unsigned int update_hour = 0; + unsigned int day_of_week = 0; + unsigned int day_of_month = 0; + unsigned int update_day_of_month = 0; + unsigned int month = 0; + unsigned int update_month = 0; + + resets = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!resets) goto return_result; + empty_list = (int*) cron_malloc(CRON_CF_ARR_LEN * sizeof(int)); + if (!empty_list) goto return_result; + for (i = 0; i < CRON_CF_ARR_LEN; i++) { + resets[i] = -1; + empty_list[i] = -1; + } + + second = calendar->tm_sec; + update_second = find_prev(expr->seconds, CRON_MAX_SECONDS, second, calendar, CRON_CF_SECOND, CRON_CF_MINUTE, empty_list, &res); + if (0 != res) goto return_result; + if (second == update_second) { + push_to_fields_arr(resets, CRON_CF_SECOND); + } + + minute = calendar->tm_min; + update_minute = find_prev(expr->minutes, CRON_MAX_MINUTES, minute, calendar, CRON_CF_MINUTE, CRON_CF_HOUR_OF_DAY, resets, &res); + if (0 != res) goto return_result; + if (minute == update_minute) { + push_to_fields_arr(resets, CRON_CF_MINUTE); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + hour = calendar->tm_hour; + update_hour = find_prev(expr->hours, CRON_MAX_HOURS, hour, calendar, CRON_CF_HOUR_OF_DAY, CRON_CF_DAY_OF_WEEK, resets, &res); + if (0 != res) goto return_result; + if (hour == update_hour) { + push_to_fields_arr(resets, CRON_CF_HOUR_OF_DAY); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + day_of_week = calendar->tm_wday; + day_of_month = calendar->tm_mday; + update_day_of_month = find_prev_day(calendar, expr->days_of_month, day_of_month, expr->days_of_week, day_of_week, resets, &res); + if (0 != res) goto return_result; + if (day_of_month == update_day_of_month) { + push_to_fields_arr(resets, CRON_CF_DAY_OF_MONTH); + } else { + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + + month = calendar->tm_mon; /*day already adds one if no day in same month is found*/ + update_month = find_prev(expr->months, CRON_MAX_MONTHS, month, calendar, CRON_CF_MONTH, CRON_CF_YEAR, resets, &res); + if (0 != res) goto return_result; + if (month != update_month) { + if (dot - calendar->tm_year > CRON_MAX_YEARS_DIFF) { + res = -1; + goto return_result; + } + res = do_prev(expr, calendar, dot); + if (0 != res) goto return_result; + } + goto return_result; + + return_result: + if (!resets || !empty_list) { + res = -1; + } + if (resets) { + cron_free(resets); + } + if (empty_list) { + cron_free(empty_list); + } + return res; +} + +time_t cron_prev(cron_expr* expr, time_t date) { + /* + The plan: + + 1 Round down to a whole second + + 2 If seconds match move on, otherwise find the next match: + 2.1 If next match is in the next minute then roll forwards + + 3 If minute matches move on, otherwise find the next match + 3.1 If next match is in the next hour then roll forwards + 3.2 Reset the seconds and go to 2 + + 4 If hour matches move on, otherwise find the next match + 4.1 If next match is in the next day then roll forwards, + 4.2 Reset the minutes and seconds and go to 2 + + ... + */ + if (!expr) return CRON_INVALID_INSTANT; + struct tm calval; + memset(&calval, 0, sizeof(struct tm)); + struct tm* calendar = cron_time(&date, &calval); + if (!calendar) return CRON_INVALID_INSTANT; + time_t original = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == original) return CRON_INVALID_INSTANT; + + /* calculate the previous occurrence */ + int res = do_prev(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + + /* check for a match, try from the next second if one wasn't found */ + time_t calculated = cron_mktime(calendar); + if (CRON_INVALID_INSTANT == calculated) return CRON_INVALID_INSTANT; + if (calculated == original) { + /* We arrived at the original timestamp - round up to the next whole second and try again... */ + res = add_to_field(calendar, CRON_CF_SECOND, -1); + if (0 != res) return CRON_INVALID_INSTANT; + res = do_prev(expr, calendar, calendar->tm_year); + if (0 != res) return CRON_INVALID_INSTANT; + } + + return cron_mktime(calendar); +} diff --git a/src/modules/virtual/Cron/ccronexpr/ccronexpr.h b/src/modules/virtual/Cron/ccronexpr/ccronexpr.h new file mode 100644 index 00000000..8f7ad30d --- /dev/null +++ b/src/modules/virtual/Cron/ccronexpr/ccronexpr.h @@ -0,0 +1,95 @@ +/* + * Copyright 2015, alex at staticlibs.net + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * File: ccronexpr.h + * Author: alex + * + * Created on February 24, 2015, 9:35 AM + */ + +#ifndef CCRONEXPR_H +#define CCRONEXPR_H + +#define CRON_USE_LOCAL_TIME + +#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) +extern "C" { +#endif + +#ifndef ANDROID +#include +#else /* ANDROID */ +#include +#endif /* ANDROID */ + +#include /*added for use if uint*_t data types*/ + +/** + * Parsed cron expression + */ +typedef struct { + uint8_t seconds[8]; + uint8_t minutes[8]; + uint8_t hours[3]; + uint8_t days_of_week[1]; + uint8_t days_of_month[4]; + uint8_t months[2]; +} cron_expr; + +/** + * Parses specified cron expression. + * + * @param expression cron expression as nul-terminated string, + * should be no longer that 256 bytes + * @param pointer to cron expression structure, it's client code responsibility + * to free/destroy it afterwards + * @param error output error message, will be set to string literal + * error message in case of error. Will be set to NULL on success. + * The error message should NOT be freed by client. + */ +void cron_parse_expr(const char* expression, cron_expr* target, const char** error); + +/** + * Uses the specified expression to calculate the next 'fire' date after + * the specified date. All dates are processed as UTC (GMT) dates + * without timezones information. To use local dates (current system timezone) + * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' + * + * @param expr parsed cron expression to use in next date calculation + * @param date start date to start calculation from + * @return next 'fire' date in case of success, '((time_t) -1)' in case of error. + */ +time_t cron_next(cron_expr* expr, time_t date); + +/** + * Uses the specified expression to calculate the previous 'fire' date after + * the specified date. All dates are processed as UTC (GMT) dates + * without timezones information. To use local dates (current system timezone) + * instead of GMT compile with '-DCRON_USE_LOCAL_TIME' + * + * @param expr parsed cron expression to use in previous date calculation + * @param date start date to start calculation from + * @return previous 'fire' date in case of success, '((time_t) -1)' in case of error. + */ +time_t cron_prev(cron_expr* expr, time_t date); + + +#if defined(__cplusplus) && !defined(CRON_COMPILE_AS_CXX) +} /* extern "C"*/ +#endif + +#endif /* CCRONEXPR_H */ diff --git a/src/modules/virtual/Cron/modinfo.json b/src/modules/virtual/Cron/modinfo.json new file mode 100644 index 00000000..c305ab4e --- /dev/null +++ b/src/modules/virtual/Cron/modinfo.json @@ -0,0 +1,57 @@ +{ + "menuSection": "Виртуальные элементы", + "configItem": [ + { + "global": 0, + "name": "Будильник (Cron)", + "type": "Writing", + "subtype": "Cron", + "id": "cron", + "widget": "anydataDef", + "page": "Таймеры", + "descr": "Будильник", + "int": 1, + "val": "*/15 * * * * *", + "formatNextAlarm": "%H:%M:%S", + "needSave": 0 + } + ], + "about": { + "authorName": "Ilya Belyakov", + "authorContact": "https://t.me/Biveraxe", + "authorGit": "https://github.com/biveraxe", + "specialThanks": "", + "moduleName": "Cron", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Будильник типа Cron", + "moduleDesc": "Планировщик времени для периодического выполнения заданий в определённое время. Генерирует событие в указанное время по формату Cron https://ru.wikipedia.org/wiki/Cron ", + "propInfo": { + "formatNextAlarm": "Формат представления даты и времени срабатывания следующего уведомления. http://cppstudio.com/post/621/", + "needSave": "Требуется сохранять(1) или нет(0) состояние в энерго независимую память." + }, + "retInfo": "Содержит время следующего срабатывания.", + "funcInfo": [ + { + "name": "stop", + "descr": "Поставить процесс на паузу, при этом не будет событий.", + "params": [] + }, + { + "name": "continue", + "descr": "Продолжить выполнение с момента остановки.", + "params": [] + } + ] + }, + "defActive": true, + "usedLibs": { + "esp32_4mb": [], + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] + } +} \ No newline at end of file From fcf83c3f0d0a9e28454cbb8f5641dbce0afdc134 Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 1 Nov 2022 12:33:39 +0300 Subject: [PATCH 070/107] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D1=81=D0=BC?= =?UTF-8?q?=D0=B0=D1=82=D1=80=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC=20=D0=BC=D0=BE?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D1=8C=20=D1=84=D0=BE=D1=80=D0=BC=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D1=82=D0=B5?= =?UTF-8?q?=D0=B2=D1=8B=D1=85=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B8=20=D0=B8=D1=85=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 2 +- include/classes/IoTItem.h | 16 +++++++------ myProfile.json | 4 ++++ src/Main.cpp | 7 +++--- src/MqttClient.cpp | 16 ++++++++----- src/WsServer.cpp | 3 +-- src/classes/IoTItem.cpp | 46 ++++++++++++++++++++++++++----------- src/classes/IoTScenario.cpp | 16 ++++++------- src/utils/SerialPrint.cpp | 6 ++--- 9 files changed, 72 insertions(+), 44 deletions(-) diff --git a/data_svelte/items.json b/data_svelte/items.json index 8f52d3ce..37fbeb62 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -17,7 +17,7 @@ "descr": "Будильник", "int": 1, "val": "*/15 * * * * *", - "formatNextAlarm": "dd.mm.yy hh:mm:ss", + "formatNextAlarm": "%H:%M:%S", "needSave": 0, "num": 1 }, diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index 88162b28..39c5fbbd 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -36,7 +36,7 @@ class IoTItem { IoTValue value; // хранение основного значения, которое обновляется из сценария, execute(), loop() или doByInterval() - bool iAmDead = false; // признак необходимости удалить объект из базы + //bool iAmDead = false; // признак необходимости удалить объект из базы bool iAmLocal = true; // признак того, что айтем был создан локально bool enableDoByInt = true; @@ -78,10 +78,12 @@ String getItemValue(const String& name); // поис bool isItemExist(const String& name); // существует ли айтем StaticJsonDocument* getLocalItemsAsJSON(); // сбор всех локальных значений Items -class externalVariable : IoTItem { // объект, создаваемый при получении информации о событии на другом контроллере для хранения информации о событии указанное время +IoTItem* createItemFromNet(const String& itemId, const String& value, int interval); - public: - externalVariable(const String& parameters); - ~externalVariable(); - void doByInterval(); // для данного класса doByInterval+int выполняет роль счетчика обратного отсчета до уничтожения -}; \ No newline at end of file +// class externalVariable : IoTItem { // объект, создаваемый при получении информации о событии на другом контроллере для хранения информации о событии указанное время + +// public: +// externalVariable(const String& parameters); +// ~externalVariable(); +// void doByInterval(); // для данного класса doByInterval+int выполняет роль счетчика обратного отсчета до уничтожения +// }; \ No newline at end of file diff --git a/myProfile.json b/myProfile.json index bfa9edb7..9918d45c 100644 --- a/myProfile.json +++ b/myProfile.json @@ -29,6 +29,10 @@ }, "modules": { "Виртуальные элементы": [ + { + "path": "src/modules/virtual/Cron", + "active": true + }, { "path": "src/modules/virtual/Loging", "active": true diff --git a/src/Main.cpp b/src/Main.cpp index fa14d76e..3ffb623f 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -89,8 +89,7 @@ void setup() { iotScen.loadScenario("/scenario.txt"); // создаем событие завершения конфигурирования для возможности выполнения блока кода при загрузке - IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"onStart\",\"val\":1,\"int\":60}")); - generateEvent("onStart", ""); + createItemFromNet("onStart", "1", 1); stInit(); @@ -151,7 +150,9 @@ void loop() { // передаем управление каждому элементу конфигурации для выполнения своих функций for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { (*it)->loop(); - if ((*it)->iAmDead) { + + //if ((*it)->iAmDead) { + if (!((*it)->iAmLocal) && (*it)->getIntFromNet() == -1) { delete *it; IoTItems.erase(it); break; diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index d527151d..234753dc 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -169,22 +169,26 @@ void mqttCallback(char* topic, uint8_t* payload, size_t length) { String valAsStr; if (!jsonRead(payloadStr, F("val"), valAsStr, false)) valAsStr = payloadStr; + unsigned long interval = 0; + jsonRead(payloadStr, F("int"), interval); IoTItem* itemExist = findIoTItem(id); if (itemExist) { - unsigned long interval = 0; - jsonRead(payloadStr, F("int"), interval); itemExist->setInterval(interval); // устанавливаем такой же интервал как на источнике события itemExist->setValue(valAsStr, false); // только регистрируем изменения в интерфейсе без создания цикла сетевых событий if (interval) itemExist->setIntFromNet(interval+5); // если пришедший интервал =0, значит не нужно контролировать доверие, иначе даем фору в 5 сек - - // запустим проверку его в сценариях - generateEvent(id, valAsStr); - SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + valAsStr); } else { // зафиксируем данные в базе, если локально элемент отсутствует //itemExist = (IoTItem*)new externalVariable(payloadStr); //IoTItems.push_back(itemExist); + + itemExist = new IoTItem(payloadStr); + itemExist->setIntFromNet(interval+5); // устанавливаем время жизни 3 сек + itemExist->iAmLocal = false; + IoTItems.push_back(itemExist); } + // запустим проверку его в сценариях + generateEvent(id, valAsStr); + SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + valAsStr); } } diff --git a/src/WsServer.cpp b/src/WsServer.cpp index e2bde5f7..ca80bde8 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -113,8 +113,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) configure("/config.json"); iotScen.loadScenario("/scenario.txt"); // создаем событие завершения конфигурирования для возможности выполнения блока кода при загрузке - IoTItems.push_back((IoTItem*)new externalVariable("{\"id\":\"onStart\",\"val\":1,\"int\":60}")); - generateEvent("onStart", ""); + createItemFromNet("onStart", "1", 1); } //----------------------------------------------------------------------// diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 65a03392..bc6c98fd 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -146,9 +146,12 @@ void IoTItem::setIntFromNet(int interval) { } void IoTItem::checkIntFromNet() { + // проверяем элемент на доверие данным. if (_intFromNet >= 0) { - if (_intFromNet == 0) { - SerialPrint("E", "SYS", "The new data did not come from the network. The level of trust is low.", _id); + // если время жизни истекло, то удаляем элемент + // если это было уведомление не об ошибке или начале работы, то сообщаем, что сетевое событие давно не приходило + if (_intFromNet == 0 && _id.indexOf("onError") == -1 && _id.indexOf("onStart") == -1) { + SerialPrint("E", _id, "The new data did not come from the network. The level of trust is low.", _id); } _intFromNet--; } @@ -178,19 +181,19 @@ IoTGpio* IoTItem::getGpioDriver() { //сетевое общение==================================================================================================================================== -externalVariable::externalVariable(const String& parameters) : IoTItem(parameters) { - prevMillis = millis(); // запоминаем текущее значение таймера для выполения doByInterval после int сек - iAmLocal = false; // указываем, что это сущность прилетела из сети - //Serial.printf("Call from externalVariable: parameters %s %d\n", parameters.c_str(), _interval); -} +// externalVariable::externalVariable(const String& parameters) : IoTItem(parameters) { +// prevMillis = millis(); // запоминаем текущее значение таймера для выполения doByInterval после int сек +// iAmLocal = false; // указываем, что это сущность прилетела из сети +// //Serial.printf("Call from externalVariable: parameters %s %d\n", parameters.c_str(), _interval); +// } -externalVariable::~externalVariable() { - Serial.printf("Call from ~externalVariable: Im dead\n"); -} +// externalVariable::~externalVariable() { +// Serial.printf("Call from ~externalVariable: Im dead\n"); +// } -void externalVariable::doByInterval() { // для данного класса doByInterval+int выполняет роль счетчика обратного отсчета до уничтожения - iAmDead = true; -} +// void externalVariable::doByInterval() { // для данного класса doByInterval+int выполняет роль счетчика обратного отсчета до уничтожения +// iAmDead = true; +// } //========================================================================================================================================= @@ -222,6 +225,23 @@ bool isItemExist(const String& name) { return false; } +IoTItem* createItemFromNet(const String& itemId, const String& value, int interval) { + String jsonStr = "{\"id\":\""; + jsonStr += itemId; + jsonStr += "\",\"val\":\""; + jsonStr += value; + jsonStr += "\",\"int\":"; + jsonStr += interval; + jsonStr += "}"; + + IoTItem *tmpp = new IoTItem(jsonStr); + tmpp->setIntFromNet(interval); // устанавливаем время жизни 3 сек + tmpp->iAmLocal = false; + IoTItems.push_back(tmpp); + generateEvent(itemId, "1"); + return tmpp; +} + StaticJsonDocument docForExport; StaticJsonDocument* getLocalItemsAsJSON() { diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index e4c59e25..3175ef17 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -78,15 +78,15 @@ class StringExprAST : public ExprAST { class VariableExprAST : public ExprAST { String Name; IoTItem *Item; // ссылка на объект модуля (прямой доступ к идентификатору указанному в сценарии), если получилось найти модуль по ID - bool ItemIsLocal = false; + //bool ItemIsLocal = false; public: VariableExprAST(const String &name, IoTItem *item) : Name(name), Item(item) { - if (item) ItemIsLocal = item->iAmLocal; + //if (item) ItemIsLocal = item->iAmLocal; } int setValue(IoTValue *val, bool generateEvent) { - if (!ItemIsLocal) Item = findIoTItem(Name); + //if (!ItemIsLocal) Item = findIoTItem(Name); if (Item) Item->setValue(*val, generateEvent); else @@ -97,7 +97,7 @@ class VariableExprAST : public ExprAST { IoTValue *exec() { if (isIotScenException) return nullptr; - if (!ItemIsLocal) Item = findIoTItem(Name); + //if (!ItemIsLocal) Item = findIoTItem(Name); if (Item) { // if (Item->value.isDecimal) // Serial.printf("Call from VariableExprAST: %s = %f\n", Name.c_str(), Item->value.valD); @@ -105,7 +105,7 @@ class VariableExprAST : public ExprAST { return &(Item->value); } - SerialPrint("E", Name, "Элемент не найден или соединение потеряно", Name); + SerialPrint("E", Name, "The element is not found or the connection is lost", Name); return nullptr; // Item не найден. } }; @@ -260,12 +260,12 @@ class CallExprAST : public ExprAST { std::vector Args; IoTItem *Item; // ссылка на объект модуля (прямой доступ к идентификатору указанному в сценарии), если получилось найти модуль по ID IoTValue ret; // хранение возвращаемого значения, т.к. возврат по ссылке осуществляется - bool ItemIsLocal = false; + //bool ItemIsLocal = false; public: CallExprAST(const String &callee, String &cmd, std::vector &args, IoTItem *item) : Callee(callee), Cmd(cmd), Args(args), Item(item) { - if (item) ItemIsLocal = item->iAmLocal; + //if (item) ItemIsLocal = item->iAmLocal; } IoTValue *exec() { @@ -283,7 +283,7 @@ class CallExprAST : public ExprAST { return nullptr; } - if (!ItemIsLocal) Item = findIoTItem(Callee); // пробуем найти переменную если она не локальная (могла придти по сети в процессе) + //if (!ItemIsLocal) Item = findIoTItem(Callee); // пробуем найти переменную если она не локальная (могла придти по сети в процессе) if (!Item) return nullptr; // ret = zeroIotVal; // если все же не пришла, то либо опечатка, либо уже стерлась - выходим if (Cmd == "getIntFromNet") { diff --git a/src/utils/SerialPrint.cpp b/src/utils/SerialPrint.cpp index 3e32547a..60cc41e1 100644 --- a/src/utils/SerialPrint.cpp +++ b/src/utils/SerialPrint.cpp @@ -23,11 +23,9 @@ void SerialPrint(const String& errorLevel, const String& module, const String& m cleanString(tosend); // создаем событие об ошибке для возможной реакции в сценарии if (itemId != "") { - IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"" + itemId + "_onError\",\"val\":\"" + tosend + "\",\"int\":1}")); - generateEvent(itemId + "_onError", "1"); + createItemFromNet(itemId + "_onError", tosend, 2); } else { - IoTItems.push_back((IoTItem *)new externalVariable("{\"id\":\"onError\",\"val\":\"" + module + " " + tosend + "\",\"int\":1}")); - generateEvent("onError", "1"); + createItemFromNet("onError", tosend, 2); } } From 0a787df79f767de61df586018f03672082b261f3 Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 1 Nov 2022 13:27:03 +0300 Subject: [PATCH 071/107] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=BE=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B8=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B1=D0=B5=D0=B7=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/Timer/Timer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/virtual/Timer/Timer.cpp b/src/modules/virtual/Timer/Timer.cpp index d1ab432e..822e8110 100644 --- a/src/modules/virtual/Timer/Timer.cpp +++ b/src/modules/virtual/Timer/Timer.cpp @@ -33,7 +33,7 @@ class Timer : public IoTItem { if (value.valD == 0) { regEvent(value.valD, "Time's up"); } - if (!_ticker) regEvent(getValue(), "Timer tick", false, false); // только регистрируем изменения без генерации тиков + //if (!_ticker) regEvent(getValue(), "Timer tick", false, false); // только регистрируем изменения без генерации тиков } if (_ticker && (value.valD > 0 || _unfin) && !_pause) regEvent(value.valD, "Timer tick"); From 67c9340f0f7e909c82711856cab147667425b9e4 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 16:42:30 +0300 Subject: [PATCH 072/107] =?UTF-8?q?=D0=9C=D0=B5=D0=BD=D1=8F=D0=B5=D0=BC=20?= =?UTF-8?q?unsigned=20long=20=D0=BD=D0=B0=20long=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=5Finterval,=20=D0=B4=D0=BB=D1=8F=20=D0=B8=D1=81=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BE?= =?UTF-8?q?=D1=82=D1=80=D0=B8=D1=86=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/Global.h | 2 +- include/utils/JsonUtils.h | 2 +- src/Global.cpp | 2 +- src/modules/exec/ButtonIn/ButtonIn.cpp | 2 +- src/modules/virtual/Loging/Loging.cpp | 2 +- .../virtual/LogingDaily/LogingDaily.cpp | 2 +- src/utils/JsonUtils.cpp | 60 +++++++++++-------- src/utils/StringUtils.cpp | 2 +- 8 files changed, 41 insertions(+), 33 deletions(-) diff --git a/include/Global.h b/include/Global.h index a8ae38d9..fb0e535f 100644 --- a/include/Global.h +++ b/include/Global.h @@ -139,7 +139,7 @@ extern Time_t _time_local; extern Time_t _time_utc; extern bool _time_isTrust; -extern unsigned long loopPeriod; +//extern unsigned long loopPeriod; // extern DynamicJsonDocument settingsFlashJsonDoc; // extern DynamicJsonDocument paramsFlashJsonDoc; diff --git a/include/utils/JsonUtils.h b/include/utils/JsonUtils.h index 8418163f..a9810dc2 100644 --- a/include/utils/JsonUtils.h +++ b/include/utils/JsonUtils.h @@ -10,7 +10,7 @@ extern String jsonWriteInt(String& json, String name, int value, bool e = true); extern String jsonWriteFloat(String& json, String name, float value, bool e = true); extern String jsonWriteBool(String& json, String name, boolean value, bool e = true); -extern bool jsonRead(const String& json, String key, unsigned long& value, bool e = true); +extern bool jsonRead(const String& json, String key, long& value, bool e = true); extern bool jsonRead(const String& json, String key, float& value, bool e = true); extern bool jsonRead(const String& json, String key, String& value, bool e = true); extern bool jsonRead(const String& json, String key, bool& value, bool e = true); diff --git a/src/Global.cpp b/src/Global.cpp index 2be45040..309e379a 100644 --- a/src/Global.cpp +++ b/src/Global.cpp @@ -70,7 +70,7 @@ String mqttRootDevice = ""; unsigned long unixTime = 0; unsigned long unixTimeShort = 0; -unsigned long loopPeriod; +//unsigned long loopPeriod; bool isTimeSynch = false; Time_t _time_local; diff --git a/src/modules/exec/ButtonIn/ButtonIn.cpp b/src/modules/exec/ButtonIn/ButtonIn.cpp index 18e8a87f..204fcbd4 100644 --- a/src/modules/exec/ButtonIn/ButtonIn.cpp +++ b/src/modules/exec/ButtonIn/ButtonIn.cpp @@ -12,7 +12,7 @@ class ButtonIn : public IoTItem { String _pinMode; int _lastButtonState = LOW; unsigned long _lastDebounceTime = 0; - unsigned long _debounceDelay = 50; + long _debounceDelay = 50; int _buttonState; int _reading; diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index a189d046..9fb21aec 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -22,7 +22,7 @@ class Loging : public IoTItem { String prevDate = ""; bool firstTimeDate = true; - unsigned long interval; + long interval; public: Loging(String parameters) : IoTItem(parameters) { diff --git a/src/modules/virtual/LogingDaily/LogingDaily.cpp b/src/modules/virtual/LogingDaily/LogingDaily.cpp index 05981d39..50ed724f 100644 --- a/src/modules/virtual/LogingDaily/LogingDaily.cpp +++ b/src/modules/virtual/LogingDaily/LogingDaily.cpp @@ -21,7 +21,7 @@ class LogingDaily : public IoTItem { String prevDate = ""; bool firstTimeDate = true; - unsigned long interval; + long interval; public: LogingDaily(String parameters) : IoTItem(parameters) { diff --git a/src/utils/JsonUtils.cpp b/src/utils/JsonUtils.cpp index ab362ac3..f04f910f 100644 --- a/src/utils/JsonUtils.cpp +++ b/src/utils/JsonUtils.cpp @@ -11,21 +11,23 @@ void jsonWriteStrDoc(DynamicJsonDocument& doc, String name, String value) { } // new============================================================================== -bool jsonRead(const String& json, String key, unsigned long& value, bool e) { +bool jsonRead(const String& json, String key, long& value, bool e) { DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); + if (e) { + SerialPrint("E", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + } return false; } else if (!doc.containsKey(key)) { if (e) { - SerialPrint("EE", F("jsonRead"), key + " missing"); + SerialPrint("E", F("jsonRead"), key + " missing"); jsonErrorDetected(); } return false; } - value = doc[key].as(); + value = doc[key].as(); return true; } @@ -33,12 +35,14 @@ bool jsonRead(const String& json, String key, float& value, bool e) { DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); + if (e) { + SerialPrint("E", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + } return false; } else if (!doc.containsKey(key)) { if (e) { - SerialPrint("EE", F("jsonRead"), key + " missing"); + SerialPrint("E", F("jsonRead"), key + " missing"); jsonErrorDetected(); } return false; @@ -51,12 +55,14 @@ bool jsonRead(const String& json, String key, String& value, bool e) { DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); + if (e) { + SerialPrint("E", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + } return false; } else if (!doc.containsKey(key)) { if (e) { - SerialPrint("EE", F("jsonRead"), key + " missing"); + SerialPrint("E", F("jsonRead"), key + " missing"); jsonErrorDetected(); } return false; @@ -76,12 +82,14 @@ bool jsonRead(const String& json, String key, int& value, bool e) { DynamicJsonDocument doc(JSON_BUFFER_SIZE); DeserializationError error = deserializeJson(doc, json); if (error) { - SerialPrint("EE", F("jsonRead"), error.f_str()); - jsonErrorDetected(); + if (e) { + SerialPrint("E", F("jsonRead"), error.f_str()); + jsonErrorDetected(); + } return false; } else if (!doc.containsKey(key)) { if (e) { - SerialPrint("EE", F("jsonRead"), key + " missing"); + SerialPrint("E", F("jsonRead"), key + " missing"); jsonErrorDetected(); } return false; @@ -97,7 +105,7 @@ bool jsonWriteStr_(String& json, const String& key, const String& value, bool e) DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } ret = false; @@ -114,7 +122,7 @@ bool jsonWriteBool_(String& json, const String& key, bool value, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } ret = false; @@ -131,7 +139,7 @@ bool jsonWriteInt_(String& json, const String& key, int value, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } ret = false; @@ -148,7 +156,7 @@ bool jsonWriteFloat_(String& json, const String &key, float value, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } ret = false; @@ -177,7 +185,7 @@ bool jsonMergeObjects(String& json1, String& json2, bool e) { jsonMergeDocs(doc1.as(), doc2.as()); if (error1 || error2) { if (e) { - SerialPrint("EE", F("json"), "jsonMergeObjects error"); + SerialPrint("E", F("json"), "jsonMergeObjects error"); jsonErrorDetected(); } ret = false; @@ -199,7 +207,7 @@ String jsonReadStr(const String& json, String name, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); + SerialPrint("E", F("jsonRead"), error.f_str()); jsonErrorDetected(); } } @@ -211,7 +219,7 @@ boolean jsonReadBool(const String& json, String name, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); + SerialPrint("E", F("jsonRead"), error.f_str()); jsonErrorDetected(); } } @@ -223,7 +231,7 @@ int jsonReadInt(const String& json, String name, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonRead"), error.f_str()); + SerialPrint("E", F("jsonRead"), error.f_str()); jsonErrorDetected(); } } @@ -236,7 +244,7 @@ String jsonWriteStr(String& json, String name, String value, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } } @@ -251,7 +259,7 @@ String jsonWriteBool(String& json, String name, boolean value, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } } @@ -266,7 +274,7 @@ String jsonWriteInt(String& json, String name, int value, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } } @@ -281,7 +289,7 @@ String jsonWriteFloat(String& json, String name, float value, bool e) { DeserializationError error = deserializeJson(doc, json); if (error) { if (e) { - SerialPrint("EE", F("jsonWrite"), error.f_str()); + SerialPrint("E", F("jsonWrite"), error.f_str()); jsonErrorDetected(); } } diff --git a/src/utils/StringUtils.cpp b/src/utils/StringUtils.cpp index a2a425d4..4234a68f 100644 --- a/src/utils/StringUtils.cpp +++ b/src/utils/StringUtils.cpp @@ -79,7 +79,7 @@ void hex2string(byte array[], unsigned int len, char buffer[]) { buffer[len * 2] = '\0'; } -inline unsigned char ChartoHex(char ch) { +unsigned char ChartoHex(char ch) { return ((ch >= 'A') ? (ch - 'A' + 0xA) : (ch - '0')) & 0x0F; } From 5c5c36e25d795f7ce48d193e6c33f61f67d6e117 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 16:45:17 +0300 Subject: [PATCH 073/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=B2=D1=80=D0=B5=D0=BC?= =?UTF-8?q?=D1=8F=20=D0=B6=D0=B8=D0=B7=D0=BD=D0=B8=20=D0=B2=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BE=D1=87=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=8D=D0=BB=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Main.cpp | 12 +++++++----- src/WsServer.cpp | 2 +- src/utils/SerialPrint.cpp | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Main.cpp b/src/Main.cpp index 3ffb623f..6961e6a1 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -89,7 +89,7 @@ void setup() { iotScen.loadScenario("/scenario.txt"); // создаем событие завершения конфигурирования для возможности выполнения блока кода при загрузке - createItemFromNet("onStart", "1", 1); + createItemFromNet("onStart", "1", -4); stInit(); @@ -105,6 +105,8 @@ void setup() { // проверяем все элементы на тухлость for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { (*it)->checkIntFromNet(); + + Serial.printf("[ITEM] size: %d, id: %s, int: %d, intnet: %d\n", sizeof(**it), (*it)->getID(), (*it)->getInterval(), (*it)->getIntFromNet()); } }, nullptr, true); @@ -162,10 +164,10 @@ void loop() { handleOrder(); handleEvent(); -#ifdef LOOP_DEBUG - loopPeriod = millis() - st; - if (loopPeriod > 2) Serial.println(loopPeriod); -#endif +// #ifdef LOOP_DEBUG +// loopPeriod = millis() - st; +// if (loopPeriod > 2) Serial.println(loopPeriod); +// #endif } //отправка json diff --git a/src/WsServer.cpp b/src/WsServer.cpp index ca80bde8..a3901634 100644 --- a/src/WsServer.cpp +++ b/src/WsServer.cpp @@ -113,7 +113,7 @@ void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length) configure("/config.json"); iotScen.loadScenario("/scenario.txt"); // создаем событие завершения конфигурирования для возможности выполнения блока кода при загрузке - createItemFromNet("onStart", "1", 1); + createItemFromNet("onStart", "1", -4); } //----------------------------------------------------------------------// diff --git a/src/utils/SerialPrint.cpp b/src/utils/SerialPrint.cpp index 60cc41e1..e633df9c 100644 --- a/src/utils/SerialPrint.cpp +++ b/src/utils/SerialPrint.cpp @@ -23,9 +23,9 @@ void SerialPrint(const String& errorLevel, const String& module, const String& m cleanString(tosend); // создаем событие об ошибке для возможной реакции в сценарии if (itemId != "") { - createItemFromNet(itemId + "_onError", tosend, 2); + createItemFromNet(itemId + "_onError", tosend, -4); } else { - createItemFromNet("onError", tosend, 2); + createItemFromNet("onError", tosend, -4); } } From 72c832169550a714e9f676fbac7b7401fd2972b2 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 16:46:49 +0300 Subject: [PATCH 074/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=B0=D0=BB=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D0=BC=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B8=20=D0=BF=D1=80=D0=B8=D1=85=D0=BE=D0=B4=D1=8F?= =?UTF-8?q?=D1=89=D0=B8=D1=85=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB=D1=8F=D1=8F=20=D0=B5=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B2=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=83=D1=8E=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8E=20ana?= =?UTF-8?q?lyzeMsgFromNet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 13 ++++++--- src/MqttClient.cpp | 26 ++---------------- src/classes/IoTItem.cpp | 55 ++++++++++++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 33 deletions(-) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index 39c5fbbd..fa8533a8 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -26,8 +26,9 @@ class IoTItem { String getID(); int getIntFromNet(); virtual String getValue(); + long getInterval(); - void setInterval(unsigned long interval); + void setInterval(long interval); void setIntFromNet(int interval); unsigned long currentMillis; @@ -45,6 +46,10 @@ class IoTItem { virtual void setValue(const IoTValue& Value, bool genEvent = true); virtual void setValue(const String& valStr, bool genEvent = true); String getRoundValue(); + void getNetEvent(String& event); + + // хуки для системных событий + virtual void onRegEvent(IoTItem* item); //методы для графиков virtual void publishValue(); @@ -56,8 +61,8 @@ class IoTItem { protected: bool _needSave = false; // признак необходимости сохранять и загружать значение элемента на flash String _subtype = ""; - String _id = ""; - unsigned long _interval = 1000; + String _id = "errorId"; // если будет попытка создания Item без указания id, то элемент оставит это значение + long _interval = 0; int _intFromNet = -2; // количество секунд доверия, пришедших из сети вместе с данными для текущего ИД // -2 - данные не приходили, скорее всего, элемент локальный, доверие есть // -1 - данные приходили и обратный отсчет дошел до нуля, значит доверия нет @@ -79,6 +84,8 @@ bool isItemExist(const String& name); // суще StaticJsonDocument* getLocalItemsAsJSON(); // сбор всех локальных значений Items IoTItem* createItemFromNet(const String& itemId, const String& value, int interval); +IoTItem* createItemFromNet(const String& msgFromNet); +void analyzeMsgFromNet(const String& msg, String altId = ""); // class externalVariable : IoTItem { // объект, создаваемый при получении информации о событии на другом контроллере для хранения информации о событии указанное время diff --git a/src/MqttClient.cpp b/src/MqttClient.cpp index 234753dc..28d37249 100644 --- a/src/MqttClient.cpp +++ b/src/MqttClient.cpp @@ -166,30 +166,8 @@ void mqttCallback(char* topic, uint8_t* payload, size_t length) { if (topicStr.indexOf(chipId) == -1) { String devId = selectFromMarkerToMarker(topicStr, "/", 2); String id = selectFromMarkerToMarker(topicStr, "/", 3); - String valAsStr; - if (!jsonRead(payloadStr, F("val"), valAsStr, false)) valAsStr = payloadStr; - - unsigned long interval = 0; - jsonRead(payloadStr, F("int"), interval); - IoTItem* itemExist = findIoTItem(id); - if (itemExist) { - itemExist->setInterval(interval); // устанавливаем такой же интервал как на источнике события - itemExist->setValue(valAsStr, false); // только регистрируем изменения в интерфейсе без создания цикла сетевых событий - if (interval) itemExist->setIntFromNet(interval+5); // если пришедший интервал =0, значит не нужно контролировать доверие, иначе даем фору в 5 сек - } else { - // зафиксируем данные в базе, если локально элемент отсутствует - //itemExist = (IoTItem*)new externalVariable(payloadStr); - //IoTItems.push_back(itemExist); - - itemExist = new IoTItem(payloadStr); - itemExist->setIntFromNet(interval+5); // устанавливаем время жизни 3 сек - itemExist->iAmLocal = false; - IoTItems.push_back(itemExist); - } - // запустим проверку его в сценариях - generateEvent(id, valAsStr); - SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + valAsStr); - + analyzeMsgFromNet(payloadStr, id); + //SerialPrint("i", F("=>MQTT"), "Received event from other device: '" + devId + "' " + id + " " + valAsStr); } } diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index bc6c98fd..1662329d 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -8,7 +8,7 @@ //получение параметров в экземпляр класса IoTItem::IoTItem(const String& parameters) { jsonRead(parameters, F("int"), _interval); - if (_interval == 0) enableDoByInt = false; + if (_interval <= 0) enableDoByInt = false; _interval = _interval * 1000; jsonRead(parameters, F("subtype"), _subtype, false); jsonRead(parameters, F("id"), _id); @@ -57,6 +57,8 @@ String IoTItem::getValue() { return value.valS; } +long IoTItem::getInterval() { return _interval; } + //определяем тип прилетевшей величины void IoTItem::setValue(const String& valStr, bool genEvent) { value.isDecimal = isDigitDotCommaStr(valStr); @@ -89,10 +91,15 @@ void IoTItem::regEvent(const String& value, const String& consoleInfo, bool erro publishStatusMqtt(_id, value); publishStatusWs(_id, value); //SerialPrint("i", "Sensor", consoleInfo + " '" + _id + "' data: " + value + "'"); - + if (genEvent) { generateEvent(_id, value); + // распространяем событие через хуки + for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { + (*it)->onRegEvent(this); + } + //отправка события другим устройствам в сети если не было ошибки============================== if (jsonReadBool(settingsFlashJson, "mqttin") && _global && !error) { String json = "{}"; @@ -141,6 +148,13 @@ int IoTItem::getIntFromNet() { return _intFromNet; } +void IoTItem::getNetEvent(String& event) { + event = "{}"; + jsonWriteStr_(event, "id", _id); + jsonWriteStr_(event, "val", getValue()); + jsonWriteInt_(event, "int", _interval/1000); +} + void IoTItem::setIntFromNet(int interval) { _intFromNet = interval; } @@ -157,6 +171,8 @@ void IoTItem::checkIntFromNet() { } } +void IoTItem::onRegEvent(IoTItem* item) {} + void IoTItem::publishValue() {} void IoTItem::clearValue() {} @@ -171,7 +187,7 @@ String IoTItem::getID() { return _id; }; -void IoTItem::setInterval(unsigned long interval) { +void IoTItem::setInterval(long interval) { _interval = interval; } @@ -179,6 +195,7 @@ IoTGpio* IoTItem::getGpioDriver() { return nullptr; } + //сетевое общение==================================================================================================================================== // externalVariable::externalVariable(const String& parameters) : IoTItem(parameters) { @@ -201,6 +218,7 @@ IoTItem* myIoTItem; // поиск элемента модуля в существующей конфигурации IoTItem* findIoTItem(const String& name) { + if (name == "") return nullptr; for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { if ((*it)->getID() == name) return *it; } @@ -225,6 +243,7 @@ bool isItemExist(const String& name) { return false; } +// создаем временную копию элемента из сети на основе события IoTItem* createItemFromNet(const String& itemId, const String& value, int interval) { String jsonStr = "{\"id\":\""; jsonStr += itemId; @@ -234,14 +253,38 @@ IoTItem* createItemFromNet(const String& itemId, const String& value, int interv jsonStr += interval; jsonStr += "}"; - IoTItem *tmpp = new IoTItem(jsonStr); - tmpp->setIntFromNet(interval); // устанавливаем время жизни 3 сек + return createItemFromNet(jsonStr); +} + +// создаем временную копию элемента из сети на основе события +IoTItem* createItemFromNet(const String& msgFromNet) { + IoTItem *tmpp = new IoTItem(msgFromNet); + if (tmpp->getInterval()) tmpp->setIntFromNet(tmpp->getInterval()/1000 + 5); tmpp->iAmLocal = false; IoTItems.push_back(tmpp); - generateEvent(itemId, "1"); + generateEvent(tmpp->getID(), tmpp->getValue()); return tmpp; } +void analyzeMsgFromNet(const String& msg, String altId) { + if (!jsonRead(msg, F("id"), altId, altId == "") && altId == "") return; // ничего не предпринимаем, если ошибка и altId = "", вообще данная конструкция нужна для совместимости с форматом данных 3 версией + IoTItem* itemExist = findIoTItem(altId); + if (itemExist) { + String valAsStr = msg; + jsonRead(msg, F("val"), valAsStr, false); + long interval = 0; + jsonRead(msg, F("int"), interval, false); + + itemExist->setInterval(interval); // устанавливаем такой же интервал как на источнике события + itemExist->setValue(valAsStr, false); // только регистрируем изменения в интерфейсе без создания цикла сетевых событий + if (interval) itemExist->setIntFromNet(interval+5); // если пришедший интервал =0, значит не нужно контролировать доверие, иначе даем фору в 5 сек + generateEvent(altId, valAsStr); + } else { + // временно зафиксируем данные в базе, если локально элемент отсутствует + createItemFromNet(msg); + } +} + StaticJsonDocument docForExport; StaticJsonDocument* getLocalItemsAsJSON() { From a7f49cdd5b069728acd1818500c6941c1a681e5f Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 16:47:46 +0300 Subject: [PATCH 075/107] =?UTF-8?q?=D0=9E=D0=BF=D1=82=D0=B8=D0=BC=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D1=83=20=D1=81=D0=BE=20=D1=81=D1=82=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D1=8B=20=D1=84=D1=83?= =?UTF-8?q?=D0=BD=D0=BA=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTScenario.h | 4 ++-- src/classes/IoTScenario.cpp | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/classes/IoTScenario.h b/include/classes/IoTScenario.h index 9e14a92c..c1a6cf83 100644 --- a/include/classes/IoTScenario.h +++ b/include/classes/IoTScenario.h @@ -8,7 +8,7 @@ class ExprAST { virtual ~ExprAST(); virtual IoTValue *exec(); virtual int setValue(IoTValue *val, bool generateEvent); // ret 0 - установка значения не поддерживается наследником - virtual bool hasEventIdName(String eventIdName); + virtual bool hasEventIdName(const String& eventIdName); }; class IoTScenario { @@ -93,7 +93,7 @@ class IoTScenario { public: void loadScenario(String fileName); - void exec(String eventIdName); + void exec(const String& eventIdName); IoTScenario(); ~IoTScenario(); diff --git a/src/classes/IoTScenario.cpp b/src/classes/IoTScenario.cpp index 3175ef17..39d4954a 100644 --- a/src/classes/IoTScenario.cpp +++ b/src/classes/IoTScenario.cpp @@ -37,7 +37,7 @@ enum Token { ExprAST::~ExprAST() {} IoTValue *ExprAST::exec() { return nullptr; } int ExprAST::setValue(IoTValue *val, bool generateEvent) { return 0; } // 0 - установка значения не поддерживается наследником -bool ExprAST::hasEventIdName(String eventIdName) { return false; } // по умолчанию все узлы не связаны с ИД события, для которого выполняется сценарий +bool ExprAST::hasEventIdName(const String& eventIdName) { return false; } // по умолчанию все узлы не связаны с ИД события, для которого выполняется сценарий // struct IoTValue zeroIotVal; /// NumberExprAST - Класс узла выражения для числовых литералов (Например, "1.0"). @@ -543,7 +543,7 @@ class IfExprAST : public ExprAST { _IDNames = ""; } - bool hasEventIdName(String eventIdName) { + bool hasEventIdName(const String& eventIdName) { // Serial.printf("Call from BinaryExprAST _IDNames:%s\n", _IDNames.c_str()); return _IDNames.indexOf(" " + eventIdName + " ") >= 0; // определяем встречался ли ИД, для которого исполняем сценарий в выражении IF } @@ -1026,7 +1026,7 @@ void IoTScenario::loadScenario(String fileName) { // подготавливае } } -void IoTScenario::exec(String eventIdName) { // посимвольно считываем и сразу интерпретируем сценарий в дерево AST +void IoTScenario::exec(const String& eventIdName) { // посимвольно считываем и сразу интерпретируем сценарий в дерево AST if (mode == 0 && !file) return; LastChar = 0; From 67e9c160eaf88a33f009be10444648a2dfba4aad Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 16:48:52 +0300 Subject: [PATCH 076/107] =?UTF-8?q?=D0=94=D0=B5=D0=BB=D0=B0=D0=B5=D0=BC=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=BD=D0=BE=D0=B9=20Charto?= =?UTF-8?q?Hex=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/utils/StringUtils.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/utils/StringUtils.h b/include/utils/StringUtils.h index 7f5b7f90..816d8353 100644 --- a/include/utils/StringUtils.h +++ b/include/utils/StringUtils.h @@ -43,3 +43,5 @@ String prettyBytes(size_t size); String uint64ToString(uint64_t input, uint8_t base = 10); void cleanString(String& str); + +unsigned char ChartoHex(char ch); From 15da6dd973764ed3a604c7ee8699eb352b04223b Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 17:01:12 +0300 Subject: [PATCH 077/107] =?UTF-8?q?=D0=A0=D0=B0=D1=81=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20UAR?= =?UTF-8?q?T=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=D0=BC?= =?UTF-8?q?=D0=B8=20=D1=81=20=D0=B4=D1=80=D1=83=D0=B3=D0=B8=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=9C=D0=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 141 +++++++++++++++++++++----- src/modules/sensors/UART/modinfo.json | 9 +- 2 files changed, 118 insertions(+), 32 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index 052acb61..444dc31f 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -6,58 +6,143 @@ #include "modules/sensors/UART/Uart.h" #ifdef ESP8266 -SoftwareSerial* myUART = nullptr; + SoftwareSerial* myUART = nullptr; #else -HardwareSerial* myUART = nullptr; + HardwareSerial* myUART = nullptr; #endif class UART : public IoTItem { private: - int tx; - int rx; - int speed; + int _tx; + int _rx; + int _speed; + int _eventFormat = 0; // 0 - нет приема, 1 - json IoTM, 2 - Nextion + +#ifdef ESP8266 + SoftwareSerial* _myUART = nullptr; +#else + HardwareSerial* _myUART = nullptr; +#endif public: UART(String parameters) : IoTItem(parameters) { - tx = jsonReadInt(parameters, "tx"); - rx = jsonReadInt(parameters, "rx"); - speed = jsonReadInt(parameters, "speed"); + jsonRead(parameters, "tx", _tx); + jsonRead(parameters, "rx", _rx); + jsonRead(parameters, "speed", _speed); + jsonRead(parameters, "eventFormat", _eventFormat); - if (!myUART) { #ifdef ESP8266 - myUART = new SoftwareSerial(tx, rx); - myUART->begin(speed); + myUART = _myUART = new SoftwareSerial(_tx, _rx); + _myUART->begin(_speed); #endif #ifdef ESP32 - myUART = new HardwareSerial(2); - myUART->begin(speed, SERIAL_8N1, rx, tx); + _myUART = new HardwareSerial(2); + _myUART->begin(speed, SERIAL_8N1, rx, tx); #endif + } + + // проверяем формат и если событие, то регистрируем его + void analyzeString(const String& msg) { + switch (_eventFormat) { + case 0: // не указан формат, значит все полученные данные воспринимаем как общее значение из UART + setValue(msg); + break; + + case 1: // формат событий IoTM с использованием json, значит создаем временную копию + analyzeMsgFromNet(msg); + break; + + case 2: // формат команд от Nextion ID=Value + String id = selectToMarker(msg, "="); + IoTItem *item = findIoTItem(id); + if (!item) return; + String valStr = selectToMarkerLast(msg, "="); + valStr.replace("\"", ""); + item->setValue(valStr); + break; } - SerialPrint("i", F("UART"), F("UART Init")); } void uartHandle() { - if (myUART) { - static String incStr; - if (myUART->available()) { - char inc; - inc = myUART->read(); - incStr += inc; - if (inc == '\n') { - parse(incStr); - incStr = ""; - } + if (!_myUART) return; + + if (_myUART->available()) { + static String inStr = ""; + char inc; + inc = _myUART->read(); + inStr += inc; + if (inc == '\n') { + analyzeString(inStr); + inStr = ""; } } } - void parse(String& incStr) { - SerialPrint("I", "=>UART", incStr); + void onRegEvent(IoTItem* eventItem) { + if (!_myUART) return; + + String printStr = ""; + switch (_eventFormat) { + case 0: return; // не указан формат, значит не следим за событиями + case 1: // формат событий IoTM с использованием json + eventItem->getNetEvent(printStr); + _myUART->println(printStr); + break; + + case 2: // формат событий для Nextion ID=Value0xFF0xFF0xFF + printStr += eventItem->getID(); + printStr += "="; + if (eventItem->value.isDecimal) + printStr += eventItem->value.valD; + else { + printStr += "\""; + printStr += eventItem->value.valS; + printStr += "\""; + } + _myUART->print(printStr); + _myUART->write(0xff); + _myUART->write(0xff); + _myUART->write(0xff); + break; + } } - void uartPrint(String msg) { - myUART->print(msg); + virtual void loop() { + uartHandle(); } + + void uartPrint(const String& msg) { + if (_myUART) { + _myUART->println(msg); + } + } + + void uartPrintHex(const String& msg) { + if (!_myUART) return; + + unsigned char Hi, Lo; + uint8_t byteTx; + const char* strPtr = msg.c_str(); + while ((Hi = *strPtr++) && (Lo = *strPtr++)) { + byteTx = (ChartoHex(Hi) << 4) | ChartoHex(Lo); + _myUART->write(byteTx); + } + } + + IoTValue execute(String command, std::vector ¶m) { + if (command == "print") { + if (param.size() == 1) { + uartPrint(param[0].valS); + } + } else if (command == "printHex") { + if (param.size() == 1) { + uartPrintHex(param[0].valS); + } + } + + return {}; + } + }; void* getAPI_UART(String subtype, String param) { diff --git a/src/modules/sensors/UART/modinfo.json b/src/modules/sensors/UART/modinfo.json index ff4f9710..02a35f33 100644 --- a/src/modules/sensors/UART/modinfo.json +++ b/src/modules/sensors/UART/modinfo.json @@ -2,7 +2,6 @@ "menuSection": "Сенсоры", "configItem": [ { - "global": 0, "name": "UART", "type": "Reading", "subtype": "UART", @@ -12,7 +11,8 @@ "id": "u", "tx": 12, "rx": 13, - "speed": 9600 + "speed": 9600, + "eventFormat": 0 } ], "about": { @@ -30,11 +30,12 @@ "SoftUART" ], "title": "Software uart для esp8266 или hardware uart для esp32", - "moduleDesc": "Используется вместе с Pzem004t или с другими работающими по uart сенсорами, в последствии будет доработан для связи с arduino платами", + "moduleDesc": "Используется вместе с Pzem004t или с другими работающими по uart сенсорами. Пригоден для обмена данными с другими контроллерами в ручном режиме или с автоматической трансляцией событий как по сети.", "propInfo": { "tx": "TX пин", "rx": "RX пин", - "speed": "Скорость UART" + "speed": "Скорость UART", + "eventFormat": "Выбор формата обмена сообщениями с другими контроллерами. =0 - не указан формат, значит не следим за событиями, =1 - формат событий IoTM с использованием json, =2 - формат событий для Nextion отправка событий: ID=Value0xFF0xFF0xFF прием ордеров: ID=Value" } }, "defActive": true, From 04bbcc983ac0a010ead83a7b0ca8dcaf6a94b77b Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 17:01:52 +0300 Subject: [PATCH 078/107] =?UTF-8?q?=D0=94=D0=B5=D0=BB=D0=B0=D0=B5=D0=BC=20?= =?UTF-8?q?int=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=B1=D1=8F=D0=B7=D0=B0=D1=82=D0=B5=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=BC=20=D0=BF=D1=80=D0=B8=20=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?Item?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/classes/IoTItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 1662329d..4a4e3721 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -7,7 +7,7 @@ //получение параметров в экземпляр класса IoTItem::IoTItem(const String& parameters) { - jsonRead(parameters, F("int"), _interval); + jsonRead(parameters, F("int"), _interval, false); if (_interval <= 0) enableDoByInt = false; _interval = _interval * 1000; jsonRead(parameters, F("subtype"), _subtype, false); From cc69fde50def3d09cdc8cba89d67b82d899dc9a9 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 17:15:59 +0300 Subject: [PATCH 079/107] =?UTF-8?q?=D0=9F=D1=80=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=20=D0=B2=D1=8B=D0=B7=D1=8B=D0=B2=D0=B0=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=BE=D1=80=D0=B4=D0=B5=D1=80=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D1=85=D0=BE=D0=B4=D0=B5=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B8=D0=B7=20Nexion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index 444dc31f..c3f896a5 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -54,11 +54,9 @@ class UART : public IoTItem { case 2: // формат команд от Nextion ID=Value String id = selectToMarker(msg, "="); - IoTItem *item = findIoTItem(id); - if (!item) return; String valStr = selectToMarkerLast(msg, "="); valStr.replace("\"", ""); - item->setValue(valStr); + generateOrder(id, valStr); break; } } From 3564c43512f215c90700dab36ea785d533a64a61 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 17:17:46 +0300 Subject: [PATCH 080/107] =?UTF-8?q?=D0=9E=D1=82=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=20=D0=BE=D1=82=D0=BB=D0=B0=D0=B4=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D1=83=D1=8E=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E=20=D0=BE=20=D1=81=D0=BE=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B2=D0=B5=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20Items?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.cpp b/src/Main.cpp index 6961e6a1..34860c02 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -106,7 +106,7 @@ void setup() { for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { (*it)->checkIntFromNet(); - Serial.printf("[ITEM] size: %d, id: %s, int: %d, intnet: %d\n", sizeof(**it), (*it)->getID(), (*it)->getInterval(), (*it)->getIntFromNet()); + //Serial.printf("[ITEM] size: %d, id: %s, int: %d, intnet: %d\n", sizeof(**it), (*it)->getID(), (*it)->getInterval(), (*it)->getIntFromNet()); } }, nullptr, true); From 8523a278dbf40e57b37f435dc8c2542538fb7ce8 Mon Sep 17 00:00:00 2001 From: biver Date: Fri, 4 Nov 2022 18:58:13 +0300 Subject: [PATCH 081/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=B0=D0=BB=D0=B3=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D0=BC=20=D0=BF=D1=80=D0=B8=D0=B5=D0=BC=D0=B0?= =?UTF-8?q?=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20?= =?UTF-8?q?=D1=81=20Nextion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index c3f896a5..e08dd120 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -67,12 +67,21 @@ class UART : public IoTItem { if (_myUART->available()) { static String inStr = ""; char inc; + inc = _myUART->read(); - inStr += inc; + if (inc == 0xFF) { + inc = _myUART->read(); + inc = _myUART->read(); + inStr = ""; + return; + } + + if (inc == '\r') return; + if (inc == '\n') { analyzeString(inStr); inStr = ""; - } + } else inStr += inc; } } From b07436742b545f18dc361637a0d021cddf684fb7 Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 6 Nov 2022 13:07:52 +0300 Subject: [PATCH 082/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D0=BF=D0=B5=D1=87=D0=B0=D1=82?= =?UTF-8?q?=D0=BA=D1=83=20=D0=B4=D0=BB=D1=8F=20ESP32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index e08dd120..382807fc 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -37,7 +37,7 @@ class UART : public IoTItem { #endif #ifdef ESP32 _myUART = new HardwareSerial(2); - _myUART->begin(speed, SERIAL_8N1, rx, tx); + _myUART->begin(_speed, SERIAL_8N1, _rx, _tx); #endif } From c02e50452ab55d34af20b49c130b12d2395aab39 Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 6 Nov 2022 13:08:21 +0300 Subject: [PATCH 083/107] =?UTF-8?q?=D0=9C=D0=B5=D0=BD=D1=8F=D0=B5=D0=BC=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D1=80=D0=B5=D1=88=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=A8=D0=98=D0=9C=20=D0=BD=D0=B0=2010?= =?UTF-8?q?=D0=B1=D0=B8=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/Pwm8266/Pwm8266.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/exec/Pwm8266/Pwm8266.cpp b/src/modules/exec/Pwm8266/Pwm8266.cpp index 2f8883f4..7e61a80a 100644 --- a/src/modules/exec/Pwm8266/Pwm8266.cpp +++ b/src/modules/exec/Pwm8266/Pwm8266.cpp @@ -20,6 +20,7 @@ class Pwm8266 : public IoTItem { IoTgpio.pinMode(_pin, OUTPUT); analogWriteFreq(_freq); + analogWriteResolution(10); IoTgpio.analogWrite(_pin, value.valD); jsonRead(parameters, "apin", _apin); From c276f4e5e894ac989cb63b36a7519ee8075e1087 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 7 Nov 2022 11:45:57 +0300 Subject: [PATCH 084/107] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BE=D0=BA=D1=80=D1=83=D0=B3=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BF=D1=80=D0=B8=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BA=D0=B5=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index 382807fc..ce7e3073 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -100,7 +100,7 @@ class UART : public IoTItem { printStr += eventItem->getID(); printStr += "="; if (eventItem->value.isDecimal) - printStr += eventItem->value.valD; + printStr += eventItem->getRoundValue(); else { printStr += "\""; printStr += eventItem->value.valS; From 875cc933b3e91fac3276e6e0750b901ef4a5b1b1 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 7 Nov 2022 12:03:46 +0300 Subject: [PATCH 085/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D1=80=D0=B8=D0=B5=D0=BC=D0=B0?= =?UTF-8?q?=20=D0=BD=D0=B5=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D1=85=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B2=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5=202=20=D1=81?= =?UTF-8?q?=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20Nextion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index ce7e3073..e0a1ea31 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -53,6 +53,10 @@ class UART : public IoTItem { break; case 2: // формат команд от Nextion ID=Value + if (msg.indexOf("=") == -1) { // если входящее сообщение не по формату, то работаем как в режиме 0 + setValue(msg); + break; + } String id = selectToMarker(msg, "="); String valStr = selectToMarkerLast(msg, "="); valStr.replace("\"", ""); From 2899f53d4b0d5a9fb66027172d17f86d987ad092 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 7 Nov 2022 13:24:01 +0300 Subject: [PATCH 086/107] =?UTF-8?q?=D0=94=D0=B5=D0=BB=D0=B0=D0=B5=D0=BC=20?= =?UTF-8?q?=D0=BE=D0=BA=D1=80=D1=83=D0=B3=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=81=D0=B5=D1=85=20=D0=BA=D0=BD?= =?UTF-8?q?=D0=BE=D0=BF=D0=BE=D0=BA=20=D1=80=D0=B0=D0=B2=D0=BD=D0=BE=D0=B9?= =?UTF-8?q?=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/ButtonIn/ButtonIn.cpp | 1 + src/modules/exec/ButtonOut/ButtonOut.cpp | 1 + src/modules/virtual/VButton/VButton.cpp | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/exec/ButtonIn/ButtonIn.cpp b/src/modules/exec/ButtonIn/ButtonIn.cpp index 204fcbd4..aecaf87e 100644 --- a/src/modules/exec/ButtonIn/ButtonIn.cpp +++ b/src/modules/exec/ButtonIn/ButtonIn.cpp @@ -23,6 +23,7 @@ class ButtonIn : public IoTItem { jsonRead(parameters, "pinMode", _pinMode); jsonRead(parameters, "debounceDelay", _debounceDelay); jsonRead(parameters, "fixState", _fixState); + _round = 0; //Serial.printf("vvvvvvvvvvvvvvvv =%d \n", _fixState); IoTgpio.pinMode(_pin, INPUT); diff --git a/src/modules/exec/ButtonOut/ButtonOut.cpp b/src/modules/exec/ButtonOut/ButtonOut.cpp index 017d1fc9..46743487 100644 --- a/src/modules/exec/ButtonOut/ButtonOut.cpp +++ b/src/modules/exec/ButtonOut/ButtonOut.cpp @@ -12,6 +12,7 @@ class ButtonOut : public IoTItem { ButtonOut(String parameters): IoTItem(parameters) { jsonRead(parameters, "pin", _pin); jsonRead(parameters, "inv", _inv); + _round = 0; IoTgpio.pinMode(_pin, OUTPUT); IoTgpio.digitalWrite(_pin, value.valD?HIGH:LOW); diff --git a/src/modules/virtual/VButton/VButton.cpp b/src/modules/virtual/VButton/VButton.cpp index 747ded1c..59d0118a 100644 --- a/src/modules/virtual/VButton/VButton.cpp +++ b/src/modules/virtual/VButton/VButton.cpp @@ -4,7 +4,9 @@ class VButton : public IoTItem { public: - VButton(String parameters): IoTItem(parameters) { } + VButton(String parameters): IoTItem(parameters) { + _round = 0; + } void setValue(const IoTValue& Value, bool genEvent = true) { value = Value; From 2925b13bd1e9e5344eab4ccd097ee5f4a0bebad1 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 7 Nov 2022 17:26:24 +0300 Subject: [PATCH 087/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B3=D0=B5=D1=82=D1=82=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=5Fglobal=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=D1=80=D0=B0=20=D0=B2=20IoTItem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/classes/IoTItem.h | 1 + src/classes/IoTItem.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/include/classes/IoTItem.h b/include/classes/IoTItem.h index fa8533a8..c87f925c 100644 --- a/include/classes/IoTItem.h +++ b/include/classes/IoTItem.h @@ -27,6 +27,7 @@ class IoTItem { int getIntFromNet(); virtual String getValue(); long getInterval(); + bool isGlobal(); void setInterval(long interval); void setIntFromNet(int interval); diff --git a/src/classes/IoTItem.cpp b/src/classes/IoTItem.cpp index 4a4e3721..095e6c18 100644 --- a/src/classes/IoTItem.cpp +++ b/src/classes/IoTItem.cpp @@ -59,6 +59,8 @@ String IoTItem::getValue() { long IoTItem::getInterval() { return _interval; } +bool IoTItem::isGlobal() { return _global;} + //определяем тип прилетевшей величины void IoTItem::setValue(const String& valStr, bool genEvent) { value.isDecimal = isDigitDotCommaStr(valStr); From 2fc5340e97a1b0c15999c5a948e44bd34ac48293 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 7 Nov 2022 17:29:45 +0300 Subject: [PATCH 088/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=20UART=20=D0=B2=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B6=D0=B8=D0=BC=D0=B5=20=D0=BE=D0=B1=D0=BC=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=20=D1=81=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?Nextion=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=D0=B7=D0=BD=D0=B0=D0=BA=D0=B0=D0=BC=20=5Fval?= =?UTF-8?q?=20=D0=B8=20=5Ftxt=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B4=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20=D1=84=D1=83=D0=BD=D0=BA?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=B2=20=D1=81=D1=86=D0=B5=D0=BD=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B9=20=D0=B4=D0=BB=D1=8F=20=D1=80=D1=83=D1=87?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 67 ++++++++++++++++++++------- src/modules/sensors/UART/modinfo.json | 27 ++++++++++- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index e0a1ea31..3aaa31e0 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -90,30 +90,31 @@ class UART : public IoTItem { } void onRegEvent(IoTItem* eventItem) { - if (!_myUART) return; + if (!_myUART || !eventItem) return; String printStr = ""; switch (_eventFormat) { case 0: return; // не указан формат, значит не следим за событиями case 1: // формат событий IoTM с использованием json - eventItem->getNetEvent(printStr); - _myUART->println(printStr); + if (eventItem->isGlobal()) { + eventItem->getNetEvent(printStr); + _myUART->println(printStr); + } break; case 2: // формат событий для Nextion ID=Value0xFF0xFF0xFF printStr += eventItem->getID(); - printStr += "="; - if (eventItem->value.isDecimal) - printStr += eventItem->getRoundValue(); - else { + if (printStr.indexOf("_") == -1) return; // пропускаем событие, если нет используемого признака типа данных - _txt или _vol + if (printStr.indexOf("_val") > 0) { + printStr.replace("_val", ".val="); + printStr += eventItem->getValue(); + } else if (printStr.indexOf("_txt") > 0) { + printStr.replace("_txt", ".txt="); printStr += "\""; - printStr += eventItem->value.valS; + printStr += eventItem->getValue(); printStr += "\""; - } - _myUART->print(printStr); - _myUART->write(0xff); - _myUART->write(0xff); - _myUART->write(0xff); + } else return; + uartPrintFFF(printStr); break; } } @@ -122,12 +123,27 @@ class UART : public IoTItem { uartHandle(); } - void uartPrint(const String& msg) { + void uartPrintFFF(const String& msg) { + if (_myUART) { + _myUART->print(msg); + _myUART->write(0xff); + _myUART->write(0xff); + _myUART->write(0xff); + } + } + + void uartPrintln(const String& msg) { if (_myUART) { _myUART->println(msg); } } + void uartPrint(const String& msg) { + if (_myUART) { + _myUART->print(msg); + } + } + void uartPrintHex(const String& msg) { if (!_myUART) return; @@ -141,14 +157,33 @@ class UART : public IoTItem { } IoTValue execute(String command, std::vector ¶m) { - if (command == "print") { + if (command == "println") { if (param.size() == 1) { - uartPrint(param[0].valS); + if (param[0].isDecimal) uartPrintln((String)param[0].valD); + else uartPrintln(param[0].valS); + } + } else if (command == "print") { + if (param.size() == 1) { + if (param[0].isDecimal) uartPrint((String)param[0].valD); + else uartPrint(param[0].valS); } } else if (command == "printHex") { if (param.size() == 1) { uartPrintHex(param[0].valS); } + } else if (command == "printFFF") { + if (param.size() == 2) { + String strToUart = ""; + if (param[0].isDecimal) + strToUart = param[0].valD; + else + strToUart = param[0].valS; + + if (param[1].valD) + uartPrintFFF("\"" + strToUart + "\""); + else + uartPrintFFF(strToUart); + } } return {}; diff --git a/src/modules/sensors/UART/modinfo.json b/src/modules/sensors/UART/modinfo.json index 02a35f33..f9e68cda 100644 --- a/src/modules/sensors/UART/modinfo.json +++ b/src/modules/sensors/UART/modinfo.json @@ -35,8 +35,31 @@ "tx": "TX пин", "rx": "RX пин", "speed": "Скорость UART", - "eventFormat": "Выбор формата обмена сообщениями с другими контроллерами. =0 - не указан формат, значит не следим за событиями, =1 - формат событий IoTM с использованием json, =2 - формат событий для Nextion отправка событий: ID=Value0xFF0xFF0xFF прием ордеров: ID=Value" - } + "eventFormat": "Выбор формата обмена сообщениями с другими контроллерами. =0 - не указан формат, значит не следим за событиями, =1 - формат событий IoTM с использованием json, =2 - формат событий для Nextion отправка событий: ID.val=Value0xFF0xFF0xFF прием ордеров: ID=Value. Отправляться будут события тех элементов, которые имеют суффикс в ИД _val или _txt, которые влияют на передаваемый формат." + }, + "retInfo": "Содержит полученное последнее по UART сообщение.", + "funcInfo": [ + { + "name": "println", + "descr": "Отправить в UART строку текста и признак завершения строки (перевод строки).", + "params": ["Строка текста"] + }, + { + "name": "print", + "descr": "Отправить в UART строку текста.", + "params": ["Строка текста"] + }, + { + "name": "printHex", + "descr": "Отправить в UART HEX-строку.", + "params": ["HEX-строка."] + }, + { + "name": "printFFF", + "descr": "Отправить в UART текстовую строку и hex метку 3 байта 0xFF0xFF0xFF.", + "params": ["Строка текста", "1 - обернуть строку в кавычки, 0 - отправить без кавычек"] + } + ] }, "defActive": true, "usedLibs": { From 5bd898aa6b003b4e543a4266b56dda8b8894a4c5 Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 8 Nov 2022 19:59:17 +0300 Subject: [PATCH 089/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83?= =?UTF-8?q?=20UART=20=D1=81=20=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=B4=D0=BB=D1=8F=20ESP32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index 3aaa31e0..d25c542d 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -36,7 +36,7 @@ class UART : public IoTItem { _myUART->begin(_speed); #endif #ifdef ESP32 - _myUART = new HardwareSerial(2); + myUART = _myUART = new HardwareSerial(2); _myUART->begin(_speed, SERIAL_8N1, _rx, _tx); #endif } @@ -60,6 +60,8 @@ class UART : public IoTItem { String id = selectToMarker(msg, "="); String valStr = selectToMarkerLast(msg, "="); valStr.replace("\"", ""); + id.replace(".val", "_val"); + id.replace(".txt", "_txt"); generateOrder(id, valStr); break; } From 32a04e48f3b7da5fa5abcea5367a55daf77581f6 Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 10 Nov 2022 00:39:18 +0500 Subject: [PATCH 090/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=B1=D0=B0=D0=B3=20=D1=81=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B1=D0=BE=D1=80=D0=BE=D0=BC=20=D0=B6=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B7=D0=BD=D0=BE=D0=B3=D0=BE=20UART=20=D0=B4=D0=BB=D1=8F=20ES?= =?UTF-8?q?P32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 9 ++++----- src/modules/sensors/UART/modinfo.json | 2 ++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index d25c542d..884c7938 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -13,9 +13,6 @@ class UART : public IoTItem { private: - int _tx; - int _rx; - int _speed; int _eventFormat = 0; // 0 - нет приема, 1 - json IoTM, 2 - Nextion #ifdef ESP8266 @@ -26,17 +23,19 @@ class UART : public IoTItem { public: UART(String parameters) : IoTItem(parameters) { + int _tx, _rx, _speed, _line; jsonRead(parameters, "tx", _tx); jsonRead(parameters, "rx", _rx); jsonRead(parameters, "speed", _speed); + jsonRead(parameters, "line", _line); jsonRead(parameters, "eventFormat", _eventFormat); #ifdef ESP8266 - myUART = _myUART = new SoftwareSerial(_tx, _rx); + myUART = _myUART = new SoftwareSerial(_rx, _tx); _myUART->begin(_speed); #endif #ifdef ESP32 - myUART = _myUART = new HardwareSerial(2); + myUART = _myUART = new HardwareSerial(_line); _myUART->begin(_speed, SERIAL_8N1, _rx, _tx); #endif } diff --git a/src/modules/sensors/UART/modinfo.json b/src/modules/sensors/UART/modinfo.json index f9e68cda..cc6a0999 100644 --- a/src/modules/sensors/UART/modinfo.json +++ b/src/modules/sensors/UART/modinfo.json @@ -11,6 +11,7 @@ "id": "u", "tx": 12, "rx": 13, + "line": 2, "speed": 9600, "eventFormat": 0 } @@ -35,6 +36,7 @@ "tx": "TX пин", "rx": "RX пин", "speed": "Скорость UART", + "line": "Актуально только для ESP32: номер линии hardUART. =2 rx=16 tx=17", "eventFormat": "Выбор формата обмена сообщениями с другими контроллерами. =0 - не указан формат, значит не следим за событиями, =1 - формат событий IoTM с использованием json, =2 - формат событий для Nextion отправка событий: ID.val=Value0xFF0xFF0xFF прием ордеров: ID=Value. Отправляться будут события тех элементов, которые имеют суффикс в ИД _val или _txt, которые влияют на передаваемый формат." }, "retInfo": "Содержит полученное последнее по UART сообщение.", From 497c402597ce015409dc19d2201b59c299d8527b Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 10 Nov 2022 17:37:59 +0500 Subject: [PATCH 091/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BC=D0=BE=D0=B4=D0=B8=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=D0=BE=D1=80=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D1=87?= =?UTF-8?q?=D0=B8=D1=81=D0=BB=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=B2=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/Variable/modinfo.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/virtual/Variable/modinfo.json b/src/modules/virtual/Variable/modinfo.json index 5bbc918a..85f957b4 100644 --- a/src/modules/virtual/Variable/modinfo.json +++ b/src/modules/virtual/Variable/modinfo.json @@ -13,7 +13,10 @@ "descr": "Введите число", "int": "0", "val": "0.0", - "num": 2 + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0 }, { "global": 0, @@ -26,8 +29,7 @@ "page": "Ввод", "descr": "Введите время", "int": "0", - "val": "02:00", - "num": 3 + "val": "02:00" }, { "global": 0, @@ -40,8 +42,7 @@ "page": "Ввод", "descr": "Введите дату", "int": "0", - "val": "24.05.2022", - "num": 4 + "val": "24.05.2022" }, { "global": 0, @@ -54,8 +55,7 @@ "page": "Ввод", "descr": "Введите текст", "int": "0", - "val": "текст", - "num": 5 + "val": "текст" } ], "about": { From 8b4d5eddbca5e068eca6b56a6a96996dab424a40 Mon Sep 17 00:00:00 2001 From: biver Date: Thu, 10 Nov 2022 18:01:05 +0500 Subject: [PATCH 092/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B8=20=D1=81=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8=D1=8F=20ButtonOut=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=B8=D0=BD=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/exec/ButtonOut/ButtonOut.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/exec/ButtonOut/ButtonOut.cpp b/src/modules/exec/ButtonOut/ButtonOut.cpp index 46743487..e5f86172 100644 --- a/src/modules/exec/ButtonOut/ButtonOut.cpp +++ b/src/modules/exec/ButtonOut/ButtonOut.cpp @@ -15,7 +15,7 @@ class ButtonOut : public IoTItem { _round = 0; IoTgpio.pinMode(_pin, OUTPUT); - IoTgpio.digitalWrite(_pin, value.valD?HIGH:LOW); + IoTgpio.digitalWrite(_pin, _inv?!value.valD:value.valD); } void doByInterval() { From 267ceb619d96b1638869cd3aed01093b04ea3d38 Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 13 Nov 2022 20:44:31 +0500 Subject: [PATCH 093/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=B8=D0=B2?= =?UTF-8?q?=D0=B0=D0=B5=D0=BC=D1=8B=D1=85=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PrepareProject.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/PrepareProject.py b/PrepareProject.py index 601d50ad..02731c09 100644 --- a/PrepareProject.py +++ b/PrepareProject.py @@ -13,7 +13,11 @@ # python PrepareProject.py --profile <ИмяФайла> # python PrepareProject.py -p <ИмяФайла> # -# +# поддерживаемые контроллеры (профили): +# esp8266_4mb +# esp32_4mb +# esp8266_1mb +# esp8266_1mb_ota import configparser import os, json, sys, getopt From 4170c7d3e4d0ada239a0b35b4a6f5789d6e7e1c3 Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 13 Nov 2022 20:45:10 +0500 Subject: [PATCH 094/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6?= =?UTF-8?q?=D0=BA=D1=83=20=D0=BB=D1=8E=D0=B1=D1=8B=D1=85=20=D1=81=D1=83?= =?UTF-8?q?=D1=84=D1=84=D0=B8=D0=BA=D1=81=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B5=20=D1=81?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D0=B9=20=D0=BD=D0=B0=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD.=20=D0=92=20=D0=BA=D0=B0=D0=B2=D1=8B?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=D1=85=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=20.txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index 884c7938..a55cfa13 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -106,15 +106,17 @@ class UART : public IoTItem { case 2: // формат событий для Nextion ID=Value0xFF0xFF0xFF printStr += eventItem->getID(); if (printStr.indexOf("_") == -1) return; // пропускаем событие, если нет используемого признака типа данных - _txt или _vol - if (printStr.indexOf("_val") > 0) { - printStr.replace("_val", ".val="); - printStr += eventItem->getValue(); - } else if (printStr.indexOf("_txt") > 0) { - printStr.replace("_txt", ".txt="); - printStr += "\""; + + if (printStr.indexOf("_txt") > 0) { + printStr.replace("_txt", ".txt=\""); printStr += eventItem->getValue(); printStr += "\""; - } else return; + } else { + printStr.replace("_", "."); + printStr += "="; + printStr += eventItem->getValue(); + } + uartPrintFFF(printStr); break; } From 466fad0525323a5eb8d06516d96dd825e1c1b89a Mon Sep 17 00:00:00 2001 From: biver Date: Sun, 13 Nov 2022 21:32:30 +0500 Subject: [PATCH 095/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20Ina219?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/Ina219/Ina219.cpp | 136 ++++++++++++++++++++++++ src/modules/sensors/Ina219/modinfo.json | 99 +++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 src/modules/sensors/Ina219/Ina219.cpp create mode 100644 src/modules/sensors/Ina219/modinfo.json diff --git a/src/modules/sensors/Ina219/Ina219.cpp b/src/modules/sensors/Ina219/Ina219.cpp new file mode 100644 index 00000000..6ab65897 --- /dev/null +++ b/src/modules/sensors/Ina219/Ina219.cpp @@ -0,0 +1,136 @@ +/****************************************************************** + Used Adafruit INA219 Current Sensor + Support for INA219 + https://github.com/adafruit/Adafruit_INA219 + + adapted for version 4dev @Serghei63 + ******************************************************************/ + +#include "Global.h" +#include "classes/IoTItem.h" + +#include +#include + +Adafruit_INA219 ina219; + + float shuntvoltage = 0; + float busvoltage = 0; + float current_mA = 0; + float loadvoltage = 0; + float power_mW = 0; + + // shuntvoltage = ina219.getShuntVoltage_mV(); // Получение напряжение на шунте + // busvoltage = ina219.getBusVoltage_V(); // Получение значение напряжения V + // current_mA = ina219.getCurrent_mA(); // Получение значение тока в мА + // power_mW = ina219.getPower_mW(); // Получение значение мощности + // loadvoltage = busvoltage + (shuntvoltage / 1000); // Расчет напряжение на нагрузки + + class Ina219loadvoltage : public IoTItem { + public: + Ina219loadvoltage(String parameters) : IoTItem(parameters) { + + // Wire.begin(2,0); // Инициализация шины I2C для модуля E01 + } + + void doByInterval() { + + loadvoltage = busvoltage + (shuntvoltage / 1000); + value.valD = loadvoltage; + + regEvent(value.valD, "Ina219loadvoltage"); + } + + ~Ina219loadvoltage(){}; +}; + + class Ina219busvoltage : public IoTItem { + public: + Ina219busvoltage(String parameters) : IoTItem(parameters) { + + // Wire.begin(2,0); // Инициализация шины I2C для модуля E01 + } + + void doByInterval() { + + busvoltage = ina219.getBusVoltage_V(); + value.valD = busvoltage; + + regEvent(value.valD, "Ina219busvoltage"); + } + + ~Ina219busvoltage(){}; +}; + + +class Ina219curr : public IoTItem { + public: + Ina219curr(String parameters) : IoTItem(parameters) { + + // Wire.begin(2,0); // Инициализация шины I2C для модуля E01 + } + void doByInterval() { + + current_mA = ina219.getCurrent_mA(); + value.valD = current_mA; + + regEvent(value.valD, "Ina219curr"); + } + + ~Ina219curr(){}; +}; + +class Ina219shuntvoltage : public IoTItem { + public: + Ina219shuntvoltage(String parameters) : IoTItem(parameters) { + + // Wire.begin(2,0); // Инициализация шины I2C для модуля E01 + } + void doByInterval() { + + shuntvoltage = ina219.getShuntVoltage_mV(); + value.valD = shuntvoltage; + + regEvent(value.valD, "Ina219shuntvoltage"); + } + + ~Ina219shuntvoltage(){}; +}; + +class Power_mW : public IoTItem { + public: + Power_mW(String parameters) : IoTItem(parameters) { + + // Wire.begin(2,0); // Инициализация шины I2C для модуля E01 + } + void doByInterval() { + + power_mW = ina219.getPower_mW(); + value.valD = power_mW; + + regEvent(value.valD, "Ina219power"); // TODO: найти способ понимания ошибки получения данных + + } + + ~Power_mW(){}; +}; +void* getAPI_Ina219(String subtype, String param) { + if (subtype == F("Ina219curr")) { + ina219.begin(); + return new Ina219curr(param); + } else if (subtype == F("Ina219shuntvoltage")) { + ina219.begin(); + return new Ina219shuntvoltage(param); + } else if (subtype == F("power_mW")) { + ina219.begin(); + return new Power_mW(param); + } else if (subtype == F("Ina219busvoltage")) { + ina219.begin(); + return new Ina219busvoltage(param); + } else if (subtype == F("Ina219loadvoltage")) { + ina219.begin(); + return new Ina219loadvoltage(param); + } else { + return nullptr; + } +} diff --git a/src/modules/sensors/Ina219/modinfo.json b/src/modules/sensors/Ina219/modinfo.json new file mode 100644 index 00000000..1dc9638f --- /dev/null +++ b/src/modules/sensors/Ina219/modinfo.json @@ -0,0 +1,99 @@ +{ + "menuSection": "Сенсоры", + + "configItem": [{ + "global": 0, + "name": "INA219 Tок", + "type": "Reading", + "subtype": "Ina219curr", + "id": "Ina219current", + "widget": "anydatamAmp", + "page": "INA 219", + "descr": "219 Датчик тока", + "int": 10 + }, + { + "global": 0, + "name": "INA219 Напряжение", + "type": "Reading", + "subtype": "Ina219busvoltage", + "id": "Ina219busvoltage", + "widget": "anydataVlt", + "page": "INA 219", + "descr": "219 Датчик напряжения", + "int": 10 + }, + { + "global": 0, + "name": "INA219 Мощность", + "type": "Reading", + "subtype": "power_mW", + "id": "Ina219power", + "widget": "anydatamWtt", + "page": "INA 219", + "descr": "219 Мощность", + "int": 10 + }, + { + "global": 0, + "name": "INA219 Напряжение нагрузки", + "type": "Reading", + "subtype": "Ina219loadvoltage", + "id": "Ina219loadvoltage", + "widget": "anydataVlt", + "page": "INA 219", + "descr": "219 Напряжение нагрузки", + "int": 10 + }, + { + "global": 0, + "name": "INA219 Шунт", + "type": "Reading", + "subtype": "Ina219shuntvoltage", + "id": "Ina219shuntvoltage", + "widget": "anydatamVlt", + "page": "INA 219", + "descr": "219 Напряжение шунта", + "int": 10 + }], + + "about": { + "authorName": "Serghei Crasnicov", + "authorContact": "https://t.me/Serghei63", + "authorGit": "https://github.com/Serghei63", + "specialThanks": "Дмитрий , Serg", + "moduleName": "Ina219", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "subTypes": [ + "Ina219curr", + "Ina219busvoltage", + "power_mW", + "Ina219loadvoltage", + "Ina219shuntvoltage" + ], + "title": "Милливатметр постоянного тока", + "moduleDesc": "Измеряет постоянный ток до 3.2 ампера, напряжение до 26 вольт и мощность на нагрузке", + "propInfo": { + "int": "Количество секунд между опросами датчика." + } + }, + + "defActive": false, + + "usedLibs": { + "esp32_4mb": [ + "https://github.com/adafruit/Adafruit_INA219.git" + ], + + "esp8266_4mb": [ + "https://github.com/adafruit/Adafruit_INA219.git" + ] + } + + } + + \ No newline at end of file From e7e415c489906ae1cc9a77359fe82d1e7c20c3da Mon Sep 17 00:00:00 2001 From: avaksru Date: Sun, 13 Nov 2022 19:44:50 +0300 Subject: [PATCH 096/107] Multitouch --- data_svelte/items.json | 44 +++++++--- myProfile.json | 4 + platformio.ini | 1 + src/modules/API.cpp | 2 + src/modules/exec/Multitouch/Multitouch.cpp | 94 ++++++++++++++++++++++ src/modules/exec/Multitouch/modinfo.json | 51 ++++++++++++ 6 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 src/modules/exec/Multitouch/Multitouch.cpp create mode 100644 src/modules/exec/Multitouch/modinfo.json diff --git a/data_svelte/items.json b/data_svelte/items.json index 37fbeb62..789e1ab7 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -545,7 +545,6 @@ "int": 5 }, { - "global": 0, "name": "39. UART", "type": "Reading", "subtype": "UART", @@ -556,6 +555,7 @@ "tx": 12, "rx": 13, "speed": 9600, + "eventFormat": 0, "num": 39 }, { @@ -639,7 +639,25 @@ }, { "global": 0, - "name": "45. Расширитель портов Pcf8574", + "name": "45. Сенсорная кнопка", + "type": "Writing", + "subtype": "Multitouch", + "id": "impulse", + "widget": "anydataDef", + "page": "Кнопки", + "descr": "Количество нажаний", + "needSave": 0, + "int": 300, + "inv": 1, + "pin": 16, + "pinMode": "INPUT", + "debounceDelay": 50, + "PWMDelay": 500, + "num": 45 + }, + { + "global": 0, + "name": "46. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -649,11 +667,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 45 + "num": 46 }, { "global": 0, - "name": "46. PWM ESP8266", + "name": "47. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -665,11 +683,11 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 46 + "num": 47 }, { "global": 0, - "name": "47. Телеграм-Лайт", + "name": "48. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -678,14 +696,14 @@ "descr": "", "token": "", "chatID": "", - "num": 47 + "num": 48 }, { "header": "Экраны" }, { "global": 0, - "name": "48. LCD экран 2004", + "name": "49. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -697,10 +715,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 48 + "num": 49 }, { - "name": "49. LCD экран 1602", + "name": "50. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -712,11 +730,11 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 49 + "num": 50 }, { "global": 0, - "name": "50. Strip ws2812b", + "name": "51. Strip ws2812b", "type": "Reading", "subtype": "Ws2812b", "id": "strip", @@ -732,6 +750,6 @@ "min": "15", "max": "30", "idshow": "t", - "num": 50 + "num": 51 } ] \ No newline at end of file diff --git a/myProfile.json b/myProfile.json index 9918d45c..1ca84be5 100644 --- a/myProfile.json +++ b/myProfile.json @@ -173,6 +173,10 @@ "path": "src/modules/exec/Mp3", "active": true }, + { + "path": "src/modules/exec/Multitouch", + "active": true + }, { "path": "src/modules/exec/Pcf8574", "active": true diff --git a/platformio.ini b/platformio.ini index c68e8c20..3a5c9816 100644 --- a/platformio.ini +++ b/platformio.ini @@ -185,6 +185,7 @@ build_src_filter = + + + + + + + + diff --git a/src/modules/API.cpp b/src/modules/API.cpp index 5756874b..2935ae36 100644 --- a/src/modules/API.cpp +++ b/src/modules/API.cpp @@ -27,6 +27,7 @@ void* getAPI_ButtonOut(String subtype, String params); void* getAPI_IoTServo(String subtype, String params); void* getAPI_Mcp23017(String subtype, String params); void* getAPI_Mp3(String subtype, String params); +void* getAPI_Multitouch(String subtype, String params); void* getAPI_Pcf8574(String subtype, String params); void* getAPI_Pwm8266(String subtype, String params); void* getAPI_TelegramLT(String subtype, String params); @@ -62,6 +63,7 @@ if ((tmpAPI = getAPI_ButtonOut(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_IoTServo(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mcp23017(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Mp3(subtype, params)) != nullptr) return tmpAPI; +if ((tmpAPI = getAPI_Multitouch(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pcf8574(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_Pwm8266(subtype, params)) != nullptr) return tmpAPI; if ((tmpAPI = getAPI_TelegramLT(subtype, params)) != nullptr) return tmpAPI; diff --git a/src/modules/exec/Multitouch/Multitouch.cpp b/src/modules/exec/Multitouch/Multitouch.cpp new file mode 100644 index 00000000..9e6b30ba --- /dev/null +++ b/src/modules/exec/Multitouch/Multitouch.cpp @@ -0,0 +1,94 @@ +#include "Global.h" +#include "classes/IoTItem.h" + +extern IoTGpio IoTgpio; + +class Multitouch : public IoTItem +{ +private: + int _pin; + int _int; + int _inv; + String _pinMode; + int _lastButtonState = LOW; + unsigned long _lastDebounceTime = 0; + unsigned long timing; + long _debounceDelay = 50; + long _PWMDelay = 500; + int _buttonState; + int _reading; + int _count = 0; + int duration = 0; + +public: + Multitouch(String parameters) : IoTItem(parameters) + { + jsonRead(parameters, "pin", _pin); + jsonRead(parameters, "pinMode", _pinMode); + jsonRead(parameters, "debounceDelay", _debounceDelay); + jsonRead(parameters, "PWMDelay", _PWMDelay); + jsonRead(parameters, "int", _int); + jsonRead(parameters, "inv", _inv); + _round = 0; + + IoTgpio.pinMode(_pin, INPUT); + if (_pinMode == "INPUT_PULLUP") + IoTgpio.digitalWrite(_pin, HIGH); + else if (_pinMode == "INPUT_PULLDOWN") + IoTgpio.digitalWrite(_pin, LOW); + + value.valD = _buttonState = IoTgpio.digitalRead(_pin); + // сообщаем всем о стартовом статусе без генерации события + regEvent(_buttonState, "", false, false); + } + + void loop() + { + _reading = IoTgpio.digitalRead(_pin); + if (_reading != _lastButtonState) + { + _lastDebounceTime = millis(); + } + + if ((millis() - _lastDebounceTime) > _debounceDelay) + { + if (millis() - timing > _int && _reading == _inv && millis() - _lastDebounceTime > _PWMDelay) + { + timing = millis(); + duration = millis() - _lastDebounceTime - _PWMDelay; + value.valD = duration / 50; + regEvent(value.valD, "Multitouch"); + _count = -1; + } + + if (_reading != _buttonState) + { + _buttonState = _reading; + _count++; + duration = 0; + } + + if (1 < _count && millis() > _lastDebounceTime + _PWMDelay) + { + value.valD = _count / 2; + regEvent(value.valD, "Multitouch"); + _count = 0; + } + } + _lastButtonState = _reading; + } + + ~Multitouch(){}; +}; + +void *getAPI_Multitouch(String subtype, String param) +{ + if (subtype == F("Multitouch")) + { + return new Multitouch(param); + } + else + { + return nullptr; + } +} \ No newline at end of file diff --git a/src/modules/exec/Multitouch/modinfo.json b/src/modules/exec/Multitouch/modinfo.json new file mode 100644 index 00000000..26d5c6ca --- /dev/null +++ b/src/modules/exec/Multitouch/modinfo.json @@ -0,0 +1,51 @@ +{ + "menuSection": "Исполнительные устройства", + "configItem": [ + { + "global": 0, + "name": "Сенсорная кнопка", + "type": "Writing", + "subtype": "Multitouch", + "id": "impulse", + "widget": "anydataDef", + "page": "Кнопки", + "descr": "Количество нажаний", + "needSave": 0, + "int": 300, + "inv": 1, + "pin": 16, + "pinMode": "INPUT", + "debounceDelay": 50, + "PWMDelay": 500 + } + ], + "about": { + "authorName": "AVAKS", + "authorContact": "https://t.me/@avaks_dev", + "authorGit": "https://github.com/avaksru", + "specialThanks": "", + "moduleName": "Multitouch", + "moduleVersion": "1.0", + "usedRam": { + "esp32_4mb": 15, + "esp8266_4mb": 15 + }, + "title": "Модуль чтения состояния GPIO (pin)", + "moduleDesc": "Считает количество нажатий на выключатель без фиксации или сенсорную кнопку. При удержании кнопки нажатой - считает длительность нажатия. Позволяет реализовать логику работы: включения различных устройств в зависимости от количества нажатий, диммировать яркость удержанием выключателя нажатым, а так же счетчик импульсов, дверной звонок, сенсорный выключатель, концевой выключатель, датчик открытия окна, и т.п.", + "propInfo": { + "int": "Интервал отправки времени удержания кнопки (миллисекунд)", + "pin": "Укажите GPIO номер пина для чтения состояний подключенной кнопки", + "inv": "Инверсия GPIO", + "pinMode": "Может быть INPUT_PULLUP INPUT_PULLDOWN INPUT", + "debounceDelay": "Время обработки дребезга (миллисекунд)", + "PWMDelay": "Время ожидания повторного нажатия. И время после которого начитается отсчет длительности непрерывного ражатия (миллисекунд)" + } + }, + "defActive": true, + "usedLibs": { + "esp32_4mb": [], + "esp8266_4mb": [], + "esp8266_1mb": [], + "esp8266_1mb_ota": [] + } +} From d6460f43a2b1a75c5c12062985ab2ebd9bfe61b6 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 14 Nov 2022 11:52:20 +0500 Subject: [PATCH 097/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=BA=D0=BE=D0=BD=D1=84?= =?UTF-8?q?=D0=B8=D0=B3=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=BE=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/items.json | 5 +++++ myProfile.json | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/data_svelte/items.json b/data_svelte/items.json index 789e1ab7..6216f176 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -78,6 +78,10 @@ "descr": "Введите число", "int": "0", "val": "0.0", + "map": "1024,1024,1,100", + "plus": 0, + "multiply": 1, + "round": 0, "num": 5 }, { @@ -554,6 +558,7 @@ "id": "u", "tx": 12, "rx": 13, + "line": 2, "speed": 9600, "eventFormat": 0, "num": 39 diff --git a/myProfile.json b/myProfile.json index 1ca84be5..132010d9 100644 --- a/myProfile.json +++ b/myProfile.json @@ -107,6 +107,10 @@ "path": "src/modules/sensors/Hdc1080", "active": true }, + { + "path": "src/modules/sensors/Ina219", + "active": false + }, { "path": "src/modules/sensors/IoTWiegand", "active": false From 4c7b14bcc86ef70c93fb09c8ad083197ae120071 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 14 Nov 2022 19:28:14 +0300 Subject: [PATCH 098/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=82=20=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=20?= =?UTF-8?q?Nextion=20=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D1=8B=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index a55cfa13..df2501f5 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -105,14 +105,20 @@ class UART : public IoTItem { case 2: // формат событий для Nextion ID=Value0xFF0xFF0xFF printStr += eventItem->getID(); - if (printStr.indexOf("_") == -1) return; // пропускаем событие, если нет используемого признака типа данных - _txt или _vol + int indexOf_ = printStr.indexOf("_"); + if (indexOf_ == -1) return; // пропускаем событие, если нет используемого признака типа данных - _txt или _vol if (printStr.indexOf("_txt") > 0) { printStr.replace("_txt", ".txt=\""); printStr += eventItem->getValue(); printStr += "\""; + } else if (printStr.indexOf("_val") > 0) { + printStr.replace(".", ""); + printStr.replace("_val", ".val="); + printStr += eventItem->getValue(); } else { - printStr.replace("_", "."); + if (indexOf_ == printStr.length()) printStr.replace("_", ""); + else printStr.replace("_", "."); printStr += "="; printStr += eventItem->getValue(); } From 9ad3ea9a35b792779043cc7976665772cdf3806c Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 14 Nov 2022 19:31:55 +0300 Subject: [PATCH 099/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=D0=B8=D0=B4=D0=B6=D0=B5=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=20?= =?UTF-8?q?ina219?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/widgets.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/data_svelte/widgets.json b/data_svelte/widgets.json index cab0dbe2..61be1240 100644 --- a/data_svelte/widgets.json +++ b/data_svelte/widgets.json @@ -209,6 +209,27 @@ "after": "ppm", "icon": "speedometer" }, + { + "name": "anydatamAmp", + "label": "миллиАмперы", + "widget": "anydata", + "after": "mAmp", + "icon": "speedometer" + }, + { + "name": "anydatamVlt", + "label": "миллиВольты", + "widget": "anydata", + "after": "mVlt", + "icon": "speedometer" + }, + { + "name": "anydatamWt", + "label": "миллиВатты", + "widget": "anydata", + "after": "mWt", + "icon": "speedometer" + }, { "name": "nil", "label": "Без виджета" From 4606f49df003e14f1e4f46b8707625454df6d268 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 14 Nov 2022 19:33:50 +0300 Subject: [PATCH 100/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20kWh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/widgets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_svelte/widgets.json b/data_svelte/widgets.json index 61be1240..4c681bff 100644 --- a/data_svelte/widgets.json +++ b/data_svelte/widgets.json @@ -47,7 +47,7 @@ "name": "anydataWth", "label": "Энергия", "widget": "anydata", - "after": "kWt/Hr", + "after": "kWh", "icon": "speedometer" }, { From 02b2ef77a002c232fe06e03eaf0f9c775896ee8f Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 14 Nov 2022 19:37:37 +0300 Subject: [PATCH 101/107] =?UTF-8?q?=D0=9A=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D1=83=D0=B5=D0=BC=20=D0=BD=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D0=BE=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D0=B0=20test=20=D0=B4=D0=BB=D1=8F=20=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/LogingDaily/modinfo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/virtual/LogingDaily/modinfo.json b/src/modules/virtual/LogingDaily/modinfo.json index 9688920c..45623902 100644 --- a/src/modules/virtual/LogingDaily/modinfo.json +++ b/src/modules/virtual/LogingDaily/modinfo.json @@ -14,7 +14,7 @@ "int": 1, "logid": "t", "points": 365, - "test": 0 + "column": 0 } ], "about": { @@ -34,7 +34,7 @@ "int": "Интервал логирования в мнутах, частота проверки смены суток в минутах. Не рекомендуется менять", "logid": "ID накопительной величины которую будем логировать", "points": "Максимальное количество точек", - "test": "Режим тестирования - график будет обновляться не раз в сутки, а кадый заданный в int интервал" + "column": "Режим тестирования - график будет обновляться не раз в сутки, а кадый заданный в int интервал. Суточные столбики - 0, Минутные столбики - 1" } }, "defActive": true, From 45dea185c55d44a808bff6900fca26e1b1a5be52 Mon Sep 17 00:00:00 2001 From: biver Date: Mon, 14 Nov 2022 20:21:50 +0300 Subject: [PATCH 102/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=BB=D0=BE=D0=B3=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BD=D0=B0=D0=B8=D0=B5=20=D0=BF=D0=BE=20=D1=81=D0=BE=D0=B1?= =?UTF-8?q?=D1=8B=D1=82=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/virtual/Loging/Loging.cpp | 60 ++++++++++++++++++++++++- src/modules/virtual/Loging/modinfo.json | 13 ++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/modules/virtual/Loging/Loging.cpp b/src/modules/virtual/Loging/Loging.cpp index 9fb21aec..3c26c933 100644 --- a/src/modules/virtual/Loging/Loging.cpp +++ b/src/modules/virtual/Loging/Loging.cpp @@ -9,6 +9,7 @@ class Loging : public IoTItem { private: String logid; String id; + String tmpValue; String filesList = ""; int _publishType = -2; @@ -103,7 +104,56 @@ class Loging : public IoTItem { //запускаем процедуру удаления старых файлов если память переполняется deleteLastFile(); } +void SetDoByInterval(String valse) { + String value = valse; + //если значение логгирования пустое + if (value == "") { + SerialPrint("E", F("LogingEvent"), "'" + id + "' loging value is empty, return"); + return; + } + //если время не было получено из интернета + if (!isTimeSynch) { + SerialPrint("E", F("LogingEvent"), "'" + id + "' Сant loging - time not synchronized, return"); + return; + } + regEvent(value, F("LogingEvent")); + String logData; + jsonWriteInt(logData, "x", unixTime); + jsonWriteFloat(logData, "y1", value.toFloat()); + //прочитаем путь к файлу последнего сохранения + String filePath = readDataDB(id); + //если данные о файле отсутствуют, создадим новый + if (filePath == "failed" || filePath == "") { + SerialPrint("E", F("LogingEvent"), "'" + id + "' file path not found, start create new file"); + createNewFileWithData(logData); + return; + } else { + //если файл все же есть но был создан не сегодня, то создаем сегодняшний + if (getTodayDateDotFormated() != getDateDotFormatedFromUnix(getFileUnixLocalTime(filePath))) { + SerialPrint("E", F("LogingEvent"), "'" + id + "' file too old, start create new file"); + createNewFileWithData(logData); + return; + } + } + + //считаем количество строк и определяем размер файла + size_t size = 0; + int lines = countJsonObj(filePath, size); + SerialPrint("i", F("LogingEvent"), "'" + id + "' " + "lines = " + String(lines) + ", size = " + String(size)); + + //если количество строк до заданной величины и дата не менялась + if (lines <= points && !hasDayChanged()) { + //просто добавим в существующий файл новые данные + addNewDataToExistingFile(filePath, logData); + //если больше или поменялась дата то создадим следующий файл + } else { + createNewFileWithData(logData); + } + //запускаем процедуру удаления старых файлов если память переполняется + deleteLastFile(); + + } void createNewFileWithData(String &logData) { logData = logData + ","; String path = "/lg/" + id + "/" + String(unixTimeShort) + ".txt"; //создадим путь вида /lg/id/133256622333.txt @@ -258,7 +308,9 @@ class Loging : public IoTItem { difference = currentMillis - prevMillis; if (difference >= interval) { prevMillis = millis(); - this->doByInterval(); + if(interval != 0){ + this->doByInterval(); + } } } } @@ -285,6 +337,12 @@ class Loging : public IoTItem { unsigned long getFileUnixLocalTime(String path) { return gmtTimeToLocal(selectToMarkerLast(deleteToMarkerLast(path, "."), "/").toInt() + START_DATETIME); } + void setValue(const IoTValue& Value, bool genEvent = true){ + value = Value; + this->SetDoByInterval(String(value.valD)); + SerialPrint("i", "Loging", "setValue:" + String(value.valD)); + regEvent(value.valS, "Loging", false, genEvent); + } }; void *getAPI_Loging(String subtype, String param) { diff --git a/src/modules/virtual/Loging/modinfo.json b/src/modules/virtual/Loging/modinfo.json index 1fb3da7f..53a962f7 100644 --- a/src/modules/virtual/Loging/modinfo.json +++ b/src/modules/virtual/Loging/modinfo.json @@ -14,6 +14,19 @@ "int": 5, "logid": "t", "points": 300 + }, + { + "global": 0, + "name": "График по событию", + "type": "Writing", + "subtype": "Loging", + "id": "log", + "widget": "chart2", + "page": "Графики", + "descr": "Температура", + "int": 0, + "num": 1, + "points": 300 } ], "about": { From 24f23955b848835030e8f5f356b973ba573f3f57 Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 15 Nov 2022 20:01:33 +0300 Subject: [PATCH 103/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E=20=D0=BE=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=BA=D0=B0=D1=85=20ds18b20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/Ds18b20/modinfo.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/sensors/Ds18b20/modinfo.json b/src/modules/sensors/Ds18b20/modinfo.json index 708708ba..bf2ba4b0 100644 --- a/src/modules/sensors/Ds18b20/modinfo.json +++ b/src/modules/sensors/Ds18b20/modinfo.json @@ -29,7 +29,7 @@ "esp8266_4mb": 15 }, "title": "Cенсор температуры ds18b20", - "moduleDesc": "Позволяет получить значения температуры с Ds18b20.", + "moduleDesc": "Позволяет получить значения температуры с Ds18b20. О подделках: https://github.com/cpetrich/counterfeit_DS18B20", "propInfo": { "pin": "GPIO номер, к которому подключена шина данных датчиков.", "index": "Порядковый номер датчика на шине.", From a619bab16763b716453abddc20f5a3bbfdc28954 Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 15 Nov 2022 20:01:59 +0300 Subject: [PATCH 104/107] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D1=8F=D0=B5=D0=BC=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=98=D0=94=5F=20=D0=B2=20Uart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/sensors/UART/Uart.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/modules/sensors/UART/Uart.cpp b/src/modules/sensors/UART/Uart.cpp index df2501f5..a6d5040e 100644 --- a/src/modules/sensors/UART/Uart.cpp +++ b/src/modules/sensors/UART/Uart.cpp @@ -106,6 +106,7 @@ class UART : public IoTItem { case 2: // формат событий для Nextion ID=Value0xFF0xFF0xFF printStr += eventItem->getID(); int indexOf_ = printStr.indexOf("_"); + //Serial.println(printStr + " fff " + indexOf_); if (indexOf_ == -1) return; // пропускаем событие, если нет используемого признака типа данных - _txt или _vol if (printStr.indexOf("_txt") > 0) { @@ -113,11 +114,11 @@ class UART : public IoTItem { printStr += eventItem->getValue(); printStr += "\""; } else if (printStr.indexOf("_val") > 0) { + printStr += eventItem->getValue(); printStr.replace(".", ""); printStr.replace("_val", ".val="); - printStr += eventItem->getValue(); } else { - if (indexOf_ == printStr.length()) printStr.replace("_", ""); + if (indexOf_ == printStr.length()-1) printStr.replace("_", ""); else printStr.replace("_", "."); printStr += "="; printStr += eventItem->getValue(); From 3668874154cf553c71f515442431d037f9ecff9e Mon Sep 17 00:00:00 2001 From: biver Date: Tue, 15 Nov 2022 23:04:26 +0300 Subject: [PATCH 105/107] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D1=8F=D0=B5=D0=BC=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20SSID=20=D0=B8=20=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D0=BE=D0=BB=D1=8C=20WiFi=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20ge?= =?UTF-8?q?t=20http://192.168.4.1/set=3Frouterssid=3D&routerpass=3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/StandWebServer.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/StandWebServer.cpp b/src/StandWebServer.cpp index 6d1e5d58..aa609150 100644 --- a/src/StandWebServer.cpp +++ b/src/StandWebServer.cpp @@ -38,6 +38,22 @@ void standWebServerInit() { // HTTP.send(200, "text/plain", "ok"); // }); + + HTTP.on("/set", HTTP_GET, []() { + if (HTTP.hasArg(F("routerssid")) && WiFi.getMode() == WIFI_AP) { + jsonWriteStr(settingsFlashJson, F("routerssid"), HTTP.arg(F("routerssid"))); + syncSettingsFlashJson(); + HTTP.send(200, "text/plain", "ok"); + } + + if (HTTP.hasArg(F("routerpass")) && WiFi.getMode() == WIFI_AP) { + jsonWriteStr(settingsFlashJson, F("routerpass"), HTTP.arg(F("routerpass"))); + syncSettingsFlashJson(); + HTTP.send(200, "text/plain", "ok"); + } + + }); + // Добавляем функцию Update для перезаписи прошивки по WiFi при 1М(256K FileFS) и выше // httpUpdater.setup(&HTTP); From 6339db7a69f4853503686fbdf40c9e9d17ae5a1e Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <67171972+IoTManagerProject@users.noreply.github.com> Date: Tue, 22 Nov 2022 15:07:29 +0100 Subject: [PATCH 106/107] =?UTF-8?q?=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20?= =?UTF-8?q?432=20=D0=BF=D1=80=D0=B5=20=D1=80=D0=B5=D0=BB=D0=B8=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/build/bundle.css.gz | Bin 5489 -> 5494 bytes data_svelte/build/bundle.js.gz | Bin 47837 -> 48408 bytes data_svelte/index.html | 2 +- data_svelte/items.json | 211 +++++++++++++++++--------------- data_svelte/settings.json | 1 + include/Const.h | 8 +- src/Main.cpp | 30 ++--- src/utils/SerialPrint.cpp | 22 ++-- 8 files changed, 143 insertions(+), 131 deletions(-) diff --git a/data_svelte/build/bundle.css.gz b/data_svelte/build/bundle.css.gz index d1b51f4558c68f0e7ea7ca003db9fc18f2026330..ffa0892293441835fb6376b0407cf069412e14ab 100644 GIT binary patch literal 5494 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 diff --git a/data_svelte/build/bundle.js.gz b/data_svelte/build/bundle.js.gz index 982ec4265ec98055c4ced4d5479f68ffc4fcb353..eaae70a786545acb145233759330fe5e75a0ed61 100644 GIT binary patch delta 48377 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 delta 47815 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 diff --git a/data_svelte/index.html b/data_svelte/index.html index 9f21ca72..f58d8370 100644 --- a/data_svelte/index.html +++ b/data_svelte/index.html @@ -4,7 +4,7 @@ - IoT Manager 4.4.1 + IoT Manager 4.4.2 diff --git a/data_svelte/items.json b/data_svelte/items.json index 6216f176..218963dd 100644 --- a/data_svelte/items.json +++ b/data_svelte/items.json @@ -37,22 +37,35 @@ }, { "global": 0, - "name": "3. График дневного расхода", + "name": "3. График по событию", + "type": "Writing", + "subtype": "Loging", + "id": "log", + "widget": "chart2", + "page": "Графики", + "descr": "Температура", + "int": 0, + "num": 3, + "points": 300 + }, + { + "global": 0, + "name": "4. График дневного расхода", "type": "Writing", "subtype": "LogingDaily", "id": "log", "widget": "chart3", "page": "Графики", "descr": "Температура", - "num": 3, + "num": 4, "int": 1, "logid": "t", "points": 365, - "test": 0 + "column": 0 }, { "global": 0, - "name": "4. Таймер", + "name": "5. Таймер", "type": "Writing", "subtype": "Timer", "id": "timer", @@ -64,11 +77,11 @@ "ticker": 1, "repeat": 1, "needSave": 0, - "num": 4 + "num": 5 }, { "global": 0, - "name": "5. Окно ввода числа (переменная)", + "name": "6. Окно ввода числа (переменная)", "type": "Reading", "subtype": "Variable", "id": "value", @@ -82,11 +95,11 @@ "plus": 0, "multiply": 1, "round": 0, - "num": 5 + "num": 6 }, { "global": 0, - "name": "6. Окно ввода времени", + "name": "7. Окно ввода времени", "type": "Reading", "subtype": "Variable", "id": "time", @@ -96,11 +109,11 @@ "descr": "Введите время", "int": "0", "val": "02:00", - "num": 6 + "num": 7 }, { "global": 0, - "name": "7. Окно ввода даты", + "name": "8. Окно ввода даты", "type": "Reading", "subtype": "Variable", "id": "time", @@ -110,11 +123,11 @@ "descr": "Введите дату", "int": "0", "val": "24.05.2022", - "num": 7 + "num": 8 }, { "global": 0, - "name": "8. Окно ввода текста", + "name": "9. Окно ввода текста", "type": "Reading", "subtype": "Variable", "id": "txt", @@ -124,11 +137,11 @@ "descr": "Введите текст", "int": "0", "val": "текст", - "num": 8 + "num": 9 }, { "global": 0, - "name": "9. Виртуальная кнопка", + "name": "10. Виртуальная кнопка", "type": "Reading", "subtype": "VButton", "id": "vbtn", @@ -138,13 +151,13 @@ "descr": "Кнопка", "int": "0", "val": "0", - "num": 9 + "num": 10 }, { "header": "Сенсоры" }, { - "name": "10. Acs712 Ток", + "name": "11. Acs712 Ток", "type": "Reading", "subtype": "Acs712", "id": "amp", @@ -154,11 +167,11 @@ "round": 3, "pin": 39, "int": 5, - "num": 10 + "num": 11 }, { "global": 0, - "name": "11. AHTXX Температура", + "name": "12. AHTXX Температура", "type": "Reading", "subtype": "AhtXXt", "id": "Temp20", @@ -169,11 +182,11 @@ "addr": "0x38", "shtType": 1, "round": 1, - "num": 11 + "num": 12 }, { "global": 0, - "name": "12. AHTXX Влажность", + "name": "13. AHTXX Влажность", "type": "Reading", "subtype": "AhtXXh", "id": "Hum20", @@ -184,11 +197,11 @@ "addr": "0x38", "shtType": 1, "round": 1, - "num": 12 + "num": 13 }, { "global": 0, - "name": "13. Аналоговый сенсор", + "name": "14. Аналоговый сенсор", "type": "Reading", "subtype": "AnalogAdc", "id": "t", @@ -202,11 +215,11 @@ "pin": 0, "int": 15, "avgSteps": 1, - "num": 13 + "num": 14 }, { "global": 0, - "name": "14. BME280 Температура", + "name": "15. BME280 Температура", "type": "Reading", "subtype": "Bme280t", "id": "tmp3", @@ -216,11 +229,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 14 + "num": 15 }, { "global": 0, - "name": "15. BME280 Давление", + "name": "16. BME280 Давление", "type": "Reading", "subtype": "Bme280p", "id": "Press3", @@ -230,11 +243,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 15 + "num": 16 }, { "global": 0, - "name": "16. BME280 Влажность", + "name": "17. BME280 Влажность", "type": "Reading", "subtype": "Bme280h", "id": "Hum3", @@ -244,11 +257,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 16 + "num": 17 }, { "global": 0, - "name": "17. BMP280 Температура", + "name": "18. BMP280 Температура", "type": "Reading", "subtype": "Bmp280t", "id": "tmp3", @@ -258,11 +271,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 17 + "num": 18 }, { "global": 0, - "name": "18. BMP280 Давление", + "name": "19. BMP280 Давление", "type": "Reading", "subtype": "Bmp280p", "id": "Press3", @@ -272,11 +285,11 @@ "int": 15, "addr": "0x77", "round": 1, - "num": 18 + "num": 19 }, { "global": 0, - "name": "19. DHT11 Температура", + "name": "20. DHT11 Температура", "type": "Reading", "subtype": "Dht1122t", "id": "tmp3", @@ -286,11 +299,11 @@ "int": 15, "pin": 0, "senstype": "dht11", - "num": 19 + "num": 20 }, { "global": 0, - "name": "20. DHT11 Влажность", + "name": "21. DHT11 Влажность", "type": "Reading", "subtype": "Dht1122h", "id": "Hum3", @@ -300,11 +313,11 @@ "int": 15, "pin": 0, "senstype": "dht11", - "num": 20 + "num": 21 }, { "global": 0, - "name": "21. DS18B20 Температура", + "name": "22. DS18B20 Температура", "type": "Reading", "subtype": "Ds18b20", "id": "dstmp", @@ -316,11 +329,11 @@ "index": 0, "addr": "", "round": 1, - "num": 21 + "num": 22 }, { "global": 0, - "name": "22. GY21 Температура", + "name": "23. GY21 Температура", "type": "Reading", "subtype": "GY21t", "id": "tmp4", @@ -329,11 +342,11 @@ "descr": "Температура", "round": 1, "int": 15, - "num": 22 + "num": 23 }, { "global": 0, - "name": "23. GY21 Влажность", + "name": "24. GY21 Влажность", "type": "Reading", "subtype": "GY21h", "id": "Hum4", @@ -342,11 +355,11 @@ "descr": "Влажность", "round": 1, "int": 15, - "num": 23 + "num": 24 }, { "global": 0, - "name": "24. HDC1080 Температура", + "name": "25. HDC1080 Температура", "type": "Reading", "subtype": "Hdc1080t", "id": "Temp1080", @@ -356,11 +369,11 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 24 + "num": 25 }, { "global": 0, - "name": "25. HDC1080 Влажность", + "name": "26. HDC1080 Влажность", "type": "Reading", "subtype": "Hdc1080h", "id": "Hum1080", @@ -370,11 +383,11 @@ "int": 15, "addr": "0x40", "round": 1, - "num": 25 + "num": 26 }, { "global": 0, - "name": "26. MAX6675 Температура", + "name": "27. MAX6675 Температура", "type": "Reading", "subtype": "Max6675t", "id": "maxtmp", @@ -385,11 +398,11 @@ "DO": 12, "CS": 13, "CLK": 14, - "num": 26 + "num": 27 }, { "global": 0, - "name": "27. PZEM 004t Напряжение", + "name": "28. PZEM 004t Напряжение", "type": "Reading", "subtype": "Pzem004v", "id": "v", @@ -399,11 +412,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 27 + "num": 28 }, { "global": 0, - "name": "28. PZEM 004t Сила тока", + "name": "29. PZEM 004t Сила тока", "type": "Reading", "subtype": "Pzem004a", "id": "a", @@ -413,11 +426,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 28 + "num": 29 }, { "global": 0, - "name": "29. PZEM 004t Мощность", + "name": "30. PZEM 004t Мощность", "type": "Reading", "subtype": "Pzem004w", "id": "w", @@ -427,11 +440,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 29 + "num": 30 }, { "global": 0, - "name": "30. PZEM 004t Энергия", + "name": "31. PZEM 004t Энергия", "type": "Reading", "subtype": "Pzem004wh", "id": "wh", @@ -441,11 +454,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 30 + "num": 31 }, { "global": 0, - "name": "31. PZEM 004t Частота", + "name": "32. PZEM 004t Частота", "type": "Reading", "subtype": "Pzem004hz", "id": "hz", @@ -455,11 +468,11 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 31 + "num": 32 }, { "global": 0, - "name": "32. PZEM 004t Косинус", + "name": "33. PZEM 004t Косинус", "type": "Reading", "subtype": "Pzem004pf", "id": "pf", @@ -469,12 +482,12 @@ "int": 15, "addr": "0xF8", "round": 1, - "num": 32 + "num": 33 }, { "global": 0, - "name": "33. Сканер кнопок 433 MHz", - "num": 33, + "name": "34. Сканер кнопок 433 MHz", + "num": 34, "type": "Reading", "subtype": "RCswitch", "id": "rsw", @@ -484,7 +497,7 @@ }, { "global": 0, - "name": "34. Sht20 Температура", + "name": "35. Sht20 Температура", "type": "Reading", "subtype": "Sht20t", "id": "tmp2", @@ -493,11 +506,11 @@ "descr": "Температура", "int": 15, "round": 1, - "num": 34 + "num": 35 }, { "global": 0, - "name": "35. Sht20 Влажность", + "name": "36. Sht20 Влажность", "type": "Reading", "subtype": "Sht20h", "id": "Hum2", @@ -506,11 +519,11 @@ "descr": "Влажность", "int": 15, "round": 1, - "num": 35 + "num": 36 }, { "global": 0, - "name": "36. Sht30 Температура", + "name": "37. Sht30 Температура", "type": "Reading", "subtype": "Sht30t", "id": "tmp30", @@ -519,11 +532,11 @@ "descr": "SHT30 Температура", "int": 15, "round": 1, - "num": 36 + "num": 37 }, { "global": 0, - "name": "37. Sht30 Влажность", + "name": "38. Sht30 Влажность", "type": "Reading", "subtype": "Sht30h", "id": "Hum30", @@ -532,12 +545,12 @@ "descr": "SHT30 Влажность", "int": 15, "round": 1, - "num": 37 + "num": 38 }, { "global": 0, - "name": "38. HC-SR04 Ультразвуковой дальномер", - "num": 38, + "name": "39. HC-SR04 Ультразвуковой дальномер", + "num": 39, "type": "Reading", "subtype": "Sonar", "id": "sonar", @@ -549,7 +562,7 @@ "int": 5 }, { - "name": "39. UART", + "name": "40. UART", "type": "Reading", "subtype": "UART", "page": "", @@ -561,14 +574,14 @@ "line": 2, "speed": 9600, "eventFormat": 0, - "num": 39 + "num": 40 }, { "header": "Исполнительные устройства" }, { "global": 0, - "name": "40. Кнопка подключенная к пину", + "name": "41. Кнопка подключенная к пину", "type": "Writing", "subtype": "ButtonIn", "id": "btn", @@ -582,11 +595,11 @@ "pinMode": "INPUT", "debounceDelay": 50, "fixState": 0, - "num": 40 + "num": 41 }, { "global": 0, - "name": "41. Управление пином", + "name": "42. Управление пином", "type": "Writing", "subtype": "ButtonOut", "needSave": 0, @@ -597,11 +610,11 @@ "int": 0, "inv": 0, "pin": 2, - "num": 41 + "num": 42 }, { "global": 0, - "name": "42. Сервопривод", + "name": "43. Сервопривод", "type": "Writing", "subtype": "IoTServo", "id": "servo", @@ -612,11 +625,11 @@ "pin": 12, "apin": -1, "amap": "0, 4096, 0, 180", - "num": 42 + "num": 43 }, { "global": 0, - "name": "43. Расширитель портов Mcp23017", + "name": "44. Расширитель портов Mcp23017", "type": "Reading", "subtype": "Mcp23017", "id": "Mcp", @@ -626,11 +639,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 43 + "num": 44 }, { "global": 0, - "name": "44. MP3 плеер", + "name": "45. MP3 плеер", "type": "Reading", "subtype": "Mp3", "id": "mp3", @@ -640,11 +653,11 @@ "int": 1, "pins": "14,12", "volume": 20, - "num": 44 + "num": 45 }, { "global": 0, - "name": "45. Сенсорная кнопка", + "name": "46. Сенсорная кнопка", "type": "Writing", "subtype": "Multitouch", "id": "impulse", @@ -658,11 +671,11 @@ "pinMode": "INPUT", "debounceDelay": 50, "PWMDelay": 500, - "num": 45 + "num": 46 }, { "global": 0, - "name": "46. Расширитель портов Pcf8574", + "name": "47. Расширитель портов Pcf8574", "type": "Reading", "subtype": "Pcf8574", "id": "Pcf", @@ -672,11 +685,11 @@ "int": "0", "addr": "0x20", "index": 1, - "num": 46 + "num": 47 }, { "global": 0, - "name": "47. PWM ESP8266", + "name": "48. PWM ESP8266", "type": "Writing", "subtype": "Pwm8266", "id": "pwm", @@ -688,11 +701,11 @@ "freq": 5000, "val": 0, "apin": -1, - "num": 47 + "num": 48 }, { "global": 0, - "name": "48. Телеграм-Лайт", + "name": "49. Телеграм-Лайт", "type": "Writing", "subtype": "TelegramLT", "id": "tg", @@ -701,14 +714,14 @@ "descr": "", "token": "", "chatID": "", - "num": 48 + "num": 49 }, { "header": "Экраны" }, { "global": 0, - "name": "49. LCD экран 2004", + "name": "50. LCD экран 2004", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -720,10 +733,10 @@ "size": "20,4", "coord": "0,0", "id2show": "id датчика", - "num": 49 + "num": 50 }, { - "name": "50. LCD экран 1602", + "name": "51. LCD экран 1602", "type": "Reading", "subtype": "Lcd2004", "id": "Lcd", @@ -735,11 +748,11 @@ "size": "16,2", "coord": "0,0", "id2show": "id датчика", - "num": 50 + "num": 51 }, { "global": 0, - "name": "51. Strip ws2812b", + "name": "52. Strip ws2812b", "type": "Reading", "subtype": "Ws2812b", "id": "strip", @@ -755,6 +768,6 @@ "min": "15", "max": "30", "idshow": "t", - "num": 51 + "num": 52 } ] \ No newline at end of file diff --git a/data_svelte/settings.json b/data_svelte/settings.json index affb351a..b7707bf7 100644 --- a/data_svelte/settings.json +++ b/data_svelte/settings.json @@ -16,6 +16,7 @@ "serverip": "http://iotmanager.org", "log": 0, "mqttin": 0, + "i2c": 0, "pinSCL": 0, "pinSDA": 0, "i2cFreq": 100000, diff --git a/include/Const.h b/include/Const.h index 4581349c..ba4cd200 100644 --- a/include/Const.h +++ b/include/Const.h @@ -1,7 +1,7 @@ #pragma once //Версия прошивки -#define FIRMWARE_VERSION 431 +#define FIRMWARE_VERSION 432 #ifdef esp8266_1mb_ota #define FIRMWARE_NAME "esp8266_1mb_ota" @@ -53,11 +53,11 @@ enum TimerTask_t { WIFI_SCAN, TIME, TIME_SYNC, UPTIME, - UDP, // UDPP - TIMES, // периодические секундные проверки + UDP, // UDPP + TIMES, // периодические секундные проверки PTASK, ST, - END}; + END }; //задачи которые надо протащить через loop enum NotAsyncActions { diff --git a/src/Main.cpp b/src/Main.cpp index 34860c02..94ae3e25 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -59,11 +59,12 @@ void setup() { mqttInit(); // настраиваем i2c шину - int pinSCL, pinSDA, i2cFreq; + int i2c, pinSCL, pinSDA, i2cFreq; jsonRead(settingsFlashJson, "pinSCL", pinSCL, false); jsonRead(settingsFlashJson, "pinSDA", pinSDA, false); jsonRead(settingsFlashJson, "i2cFreq", i2cFreq, false); - if (pinSCL && pinSDA && i2cFreq) { + jsonRead(settingsFlashJson, "i2c", i2c, false); + if (i2c != 0) { #ifdef esp32_4mb Wire.end(); Wire.begin(pinSDA, pinSCL, (uint32_t)i2cFreq); @@ -71,6 +72,7 @@ void setup() { Wire.begin(pinSDA, pinSCL); Wire.setClock(i2cFreq); #endif + SerialPrint("i", "i2c", F("i2c pins overriding done")); } //настраиваем микроконтроллер @@ -95,21 +97,21 @@ void setup() { // настраиваем секундные обслуживания системы ts.add( - TIMES, 1000, [&](void*) { + TIMES, 1000, [&](void *) { // сохраняем значения IoTItems в файл каждую секунду, если были изменения (установлены маркеры на сохранение) if (needSaveValues) { syncValuesFlashJson(); needSaveValues = false; } - + // проверяем все элементы на тухлость for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { (*it)->checkIntFromNet(); - - //Serial.printf("[ITEM] size: %d, id: %s, int: %d, intnet: %d\n", sizeof(**it), (*it)->getID(), (*it)->getInterval(), (*it)->getIntFromNet()); - } + + // Serial.printf("[ITEM] size: %d, id: %s, int: %d, intnet: %d\n", sizeof(**it), (*it)->getID(), (*it)->getInterval(), (*it)->getIntFromNet()); + } }, - nullptr, true); + nullptr, true); // test Serial.println("-------test start--------"); @@ -152,8 +154,8 @@ void loop() { // передаем управление каждому элементу конфигурации для выполнения своих функций for (std::list::iterator it = IoTItems.begin(); it != IoTItems.end(); ++it) { (*it)->loop(); - - //if ((*it)->iAmDead) { + + // if ((*it)->iAmDead) { if (!((*it)->iAmLocal) && (*it)->getIntFromNet() == -1) { delete *it; IoTItems.erase(it); @@ -164,10 +166,10 @@ void loop() { handleOrder(); handleEvent(); -// #ifdef LOOP_DEBUG -// loopPeriod = millis() - st; -// if (loopPeriod > 2) Serial.println(loopPeriod); -// #endif + // #ifdef LOOP_DEBUG + // loopPeriod = millis() - st; + // if (loopPeriod > 2) Serial.println(loopPeriod); + // #endif } //отправка json diff --git a/src/utils/SerialPrint.cpp b/src/utils/SerialPrint.cpp index e633df9c..d057beb3 100644 --- a/src/utils/SerialPrint.cpp +++ b/src/utils/SerialPrint.cpp @@ -13,21 +13,17 @@ void SerialPrint(const String& errorLevel, const String& module, const String& m if (isNetworkActive()) { if (jsonReadInt(settingsFlashJson, F("log")) != 0) { - // String pl = "/log|" + tosend; - // standWebSocket.broadcastTXT(pl); sendStringToWs("corelg", tosend, -1); } } - if (errorLevel == "E") { - cleanString(tosend); - // создаем событие об ошибке для возможной реакции в сценарии - if (itemId != "") { - createItemFromNet(itemId + "_onError", tosend, -4); - } else { - createItemFromNet("onError", tosend, -4); - } - } - - + // if (errorLevel == "E") { + // cleanString(tosend); + // // создаем событие об ошибке для возможной реакции в сценарии + // if (itemId != "") { + // createItemFromNet(itemId + "_onError", tosend, -4); + // } else { + // createItemFromNet("onError", tosend, -4); + // } + // } } \ No newline at end of file From b49c4ecb1aec4cb47020ef9118668d0d6dd860ea Mon Sep 17 00:00:00 2001 From: Dmitry Borisenko <67171972+IoTManagerProject@users.noreply.github.com> Date: Tue, 22 Nov 2022 15:17:40 +0100 Subject: [PATCH 107/107] =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=B2=D0=B5=D0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data_svelte/build/bundle.css.gz | Bin 5494 -> 5499 bytes data_svelte/build/bundle.js.gz | Bin 48408 -> 48408 bytes data_svelte/index.html | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data_svelte/build/bundle.css.gz b/data_svelte/build/bundle.css.gz index ffa0892293441835fb6376b0407cf069412e14ab..7c46e54ff4eed49c8f64c3c1bf1c2bd469faca2a 100644 GIT binary patch literal 5499 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 DegCE1 literal 5494 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 diff --git a/data_svelte/index.html b/data_svelte/index.html index f58d8370..77e5d2fb 100644 --- a/data_svelte/index.html +++ b/data_svelte/index.html @@ -7,9 +7,9 @@ IoT Manager 4.4.2 - + - +