Compare commits
15 Commits
dcf3d30295
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f15fe0f1d | |||
| 90037587c0 | |||
| cb1014c950 | |||
| 4100931deb | |||
| 977227296e | |||
| 732a2dfa32 | |||
| 818a84bf4f | |||
| 98cc0db107 | |||
| fddc086b4e | |||
| 879418b5a9 | |||
| 1681a694d3 | |||
| 5e7f26f128 | |||
| 3772981b8f | |||
| 6f3584fe6d | |||
| b0a48e94bd |
55
.gitignore
vendored
55
.gitignore
vendored
@@ -1,23 +1,46 @@
|
|||||||
# Kotlin/Java
|
# Gradle/Java/Kotlin
|
||||||
*.class
|
.gradle/
|
||||||
*.jar
|
|
||||||
*.iml
|
|
||||||
/out/
|
|
||||||
build/
|
build/
|
||||||
|
out/
|
||||||
|
*.iml
|
||||||
|
|
||||||
# ESP32 / PlatformIO / Arduino
|
# IDEA
|
||||||
*.bin
|
|
||||||
*.elf
|
|
||||||
*.pio
|
|
||||||
*.pioenvs/
|
|
||||||
*.idf/
|
|
||||||
|
|
||||||
# Android
|
|
||||||
/.gradle/
|
|
||||||
/build/
|
|
||||||
/local.properties
|
|
||||||
.idea/
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node/npm (если есть frontend/mobile)
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Python (если будет)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# gitignore for ESP-IDF
|
||||||
|
sdkconfig
|
||||||
|
build/
|
||||||
|
sdkconfig
|
||||||
|
sdkconfig.old
|
||||||
|
Kconfig
|
||||||
|
sdkconfig.*
|
||||||
|
|
||||||
|
.git/
|
||||||
|
|
||||||
|
# ide
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.env
|
||||||
|
*.env
|
||||||
|
|
||||||
|
application.yaml
|
||||||
|
application.conf
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## Documentation
|
||||||
|
|
||||||
|
- Architecture decisions: `doc/architecture/`
|
||||||
BIN
backend/backend.zip
Normal file
BIN
backend/backend.zip
Normal file
Binary file not shown.
92
backend/build.gradle.kts
Normal file
92
backend/build.gradle.kts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env kotlin
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "1.9.22"
|
||||||
|
kotlin("plugin.serialization") version "1.9.22"
|
||||||
|
application
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("io.ktor:ktor-server-core:2.3.7")
|
||||||
|
implementation("io.ktor:ktor-server-netty:2.3.7")
|
||||||
|
implementation("io.ktor:ktor-server-content-negotiation:2.3.7")
|
||||||
|
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.7")
|
||||||
|
implementation("io.ktor:ktor-server-websockets:2.3.7")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||||
|
implementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
|
implementation("org.mariadb.jdbc:mariadb-java-client:3.3.3")
|
||||||
|
implementation("com.zaxxer:HikariCP:5.1.0")
|
||||||
|
implementation("io.ktor:ktor-server-compression:2.3.7")
|
||||||
|
testImplementation(kotlin("test"))
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("org.pavloveugene.iot.backend.ApplicationKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
manifest {
|
||||||
|
attributes["Main-Class"] = "org.pavloveugene.iot.backend.ApplicationKt"
|
||||||
|
}
|
||||||
|
|
||||||
|
from({
|
||||||
|
configurations.runtimeClasspath.get().map { zipTree(it) }
|
||||||
|
})
|
||||||
|
|
||||||
|
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
from("../db/migrations") {
|
||||||
|
into("db/migration")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("deploy") {
|
||||||
|
dependsOn("jar")
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
val jarFile = file("build/libs/backend.jar")
|
||||||
|
|
||||||
|
if (!jarFile.exists()) {
|
||||||
|
throw GradleException("Jar not found: ${jarFile.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runCommand(vararg cmd: String) {
|
||||||
|
println("Running: ${cmd.joinToString(" ")}")
|
||||||
|
|
||||||
|
val process = ProcessBuilder(*cmd)
|
||||||
|
.inheritIO()
|
||||||
|
.start()
|
||||||
|
|
||||||
|
val exitCode = process.waitFor()
|
||||||
|
if (exitCode != 0) {
|
||||||
|
throw GradleException("Command failed: ${cmd.joinToString(" ")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload
|
||||||
|
runCommand(
|
||||||
|
"scp",
|
||||||
|
jarFile.absolutePath,
|
||||||
|
"home-iot:/tmp/backend.jar"
|
||||||
|
)
|
||||||
|
|
||||||
|
// deploy
|
||||||
|
runCommand(
|
||||||
|
"ssh",
|
||||||
|
"home-iot",
|
||||||
|
"sudo mv /tmp/backend.jar /opt/iot-backend/app.jar && sudo systemctl restart iot-backend"
|
||||||
|
)
|
||||||
|
|
||||||
|
runCommand("ssh",
|
||||||
|
"home-iot",
|
||||||
|
"systemctl status iot-backend --no-pager")
|
||||||
|
|
||||||
|
println("Deploy completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
backend/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
backend/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
248
backend/gradlew
vendored
Executable file
248
backend/gradlew
vendored
Executable file
@@ -0,0 +1,248 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015 the original authors.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# https://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.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
93
backend/gradlew.bat
vendored
Normal file
93
backend/gradlew.bat
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.pavloveugene.iot.backend
|
||||||
|
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.netty.*
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
|
import org.apache.commons.logging.LogFactory
|
||||||
|
import org.pavloveugene.iot.backend.config.AppConfig
|
||||||
|
import org.pavloveugene.iot.backend.config.configureSerialization
|
||||||
|
import org.pavloveugene.iot.backend.config.configureWebSockets
|
||||||
|
import org.pavloveugene.iot.backend.db.Database
|
||||||
|
import org.pavloveugene.iot.backend.db.Migration
|
||||||
|
import org.pavloveugene.iot.backend.db.runMigrations
|
||||||
|
import java.time.Duration
|
||||||
|
import org.pavloveugene.iot.backend.routes.*
|
||||||
|
import org.pavloveugene.iot.backend.services.executeCleanup
|
||||||
|
import org.pavloveugene.iot.backend.services.runNormalizeLoop
|
||||||
|
import org.pavloveugene.iot.backend.services.startKtorServer
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
|
||||||
|
|
||||||
|
Database.init()
|
||||||
|
|
||||||
|
runMigrations()
|
||||||
|
|
||||||
|
val mode = args.firstOrNull()
|
||||||
|
|
||||||
|
when (mode) {
|
||||||
|
"--normalize-data", "normalize-data" -> {
|
||||||
|
runNormalizeLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
"--cleanup", "cleanup" -> {
|
||||||
|
executeCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
startKtorServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.pavloveugene.iot.backend.config
|
||||||
|
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
|
||||||
|
object AppConfig {
|
||||||
|
private val config = ConfigFactory.load()
|
||||||
|
|
||||||
|
val serverHost: String = config.getString("ktor.host")
|
||||||
|
val serverPort: Int = config.getInt("ktor.port")
|
||||||
|
|
||||||
|
val appName: String = config.getString("app.name")
|
||||||
|
|
||||||
|
val apiPrefix: String = config.getString("api.prefix")
|
||||||
|
val wsPath: String = config.getString("ws.path")
|
||||||
|
|
||||||
|
val dbUrl = config.getString("ktor.database.url")
|
||||||
|
val dbUser = config.getString("ktor.database.user")
|
||||||
|
val dbPassword = config.getString("ktor.database.password")
|
||||||
|
|
||||||
|
val storagePath = config.getString("iot.firmware.storage")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.pavloveugene.iot.backend.config
|
||||||
|
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
fun Application.configureSerialization() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(
|
||||||
|
Json {
|
||||||
|
prettyPrint = true
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Application.configureWebSockets() {
|
||||||
|
install(WebSockets) {
|
||||||
|
pingPeriod = Duration.ofSeconds(30)
|
||||||
|
timeout = Duration.ofSeconds(15)
|
||||||
|
maxFrameSize = Long.MAX_VALUE
|
||||||
|
masking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package org.pavloveugene.iot.backend.db
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig
|
||||||
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
|
import org.pavloveugene.iot.backend.config.AppConfig
|
||||||
|
import java.sql.Statement
|
||||||
|
import kotlin.getValue
|
||||||
|
|
||||||
|
object Database {
|
||||||
|
fun init() {
|
||||||
|
val ds = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataSource: HikariDataSource by lazy {
|
||||||
|
val config = HikariConfig().apply {
|
||||||
|
jdbcUrl = AppConfig.dbUrl
|
||||||
|
driverClassName = "org.mariadb.jdbc.Driver"
|
||||||
|
username = AppConfig.dbUser
|
||||||
|
password = AppConfig.dbPassword
|
||||||
|
|
||||||
|
maximumPoolSize = 10
|
||||||
|
minimumIdle = 2
|
||||||
|
connectionTimeout = 10000
|
||||||
|
idleTimeout = 30000
|
||||||
|
maxLifetime = 1800000
|
||||||
|
|
||||||
|
isAutoCommit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
HikariDataSource(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun execute(sql: String, params: List<Any?> = emptyList()): Int {
|
||||||
|
dataSource.connection.use { conn ->
|
||||||
|
conn.prepareStatement(sql).use { stmt ->
|
||||||
|
|
||||||
|
params.forEachIndexed { i, p ->
|
||||||
|
stmt.setObject(i + 1, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stmt.executeUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun queryOne(sql: String, params: List<Any?> = emptyList()): Map<String, Any?>? {
|
||||||
|
dataSource.connection.use { conn ->
|
||||||
|
conn.prepareStatement(sql).use { stmt ->
|
||||||
|
|
||||||
|
params.forEachIndexed { i, p ->
|
||||||
|
stmt.setObject(i + 1, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rs = stmt.executeQuery()
|
||||||
|
if (!rs.next()) return null
|
||||||
|
|
||||||
|
val meta = rs.metaData
|
||||||
|
val map = mutableMapOf<String, Any?>()
|
||||||
|
|
||||||
|
for (i in 1..meta.columnCount) {
|
||||||
|
map[meta.getColumnLabel(i)] = rs.getObject(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun query(sql: String, params: List<Any?> = emptyList()): List<Map<String, Any?>> {
|
||||||
|
dataSource.connection.use { conn ->
|
||||||
|
conn.prepareStatement(sql).use { stmt ->
|
||||||
|
|
||||||
|
params.forEachIndexed { i, p ->
|
||||||
|
stmt.setObject(i + 1, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
val rs = stmt.executeQuery()
|
||||||
|
val meta = rs.metaData
|
||||||
|
|
||||||
|
val result = mutableListOf<Map<String, Any?>>()
|
||||||
|
|
||||||
|
while (rs.next()) {
|
||||||
|
val map = mutableMapOf<String, Any?>()
|
||||||
|
|
||||||
|
for (i in 1..meta.columnCount) {
|
||||||
|
map[meta.getColumnLabel(i)] = rs.getObject(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insertAndReturnId(sql: String, params: List<Any?> = emptyList()): Long {
|
||||||
|
dataSource.connection.use { conn ->
|
||||||
|
conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS).use { stmt ->
|
||||||
|
|
||||||
|
params.forEachIndexed { i, p ->
|
||||||
|
stmt.setObject(i + 1, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.executeUpdate()
|
||||||
|
|
||||||
|
val rs = stmt.generatedKeys
|
||||||
|
if (rs.next()) {
|
||||||
|
return rs.getLong(1)
|
||||||
|
} else {
|
||||||
|
error("No generated key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package org.pavloveugene.iot.backend.db
|
||||||
|
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
data class Migration(
|
||||||
|
val version: Int,
|
||||||
|
val sql: String
|
||||||
|
)
|
||||||
|
|
||||||
|
private val log= LoggerFactory.getLogger("DB Migrations")
|
||||||
|
|
||||||
|
fun loadMigrations(): List<Migration> {
|
||||||
|
val cl = Thread.currentThread().contextClassLoader
|
||||||
|
|
||||||
|
val resources = cl.getResources("db/migration")
|
||||||
|
val result = mutableListOf<Migration>()
|
||||||
|
|
||||||
|
while (resources.hasMoreElements()) {
|
||||||
|
val url = resources.nextElement()
|
||||||
|
|
||||||
|
val uri = url.toURI()
|
||||||
|
|
||||||
|
if (uri.scheme == "file") {
|
||||||
|
// обычный запуск из IDE
|
||||||
|
val dir = java.nio.file.Paths.get(uri)
|
||||||
|
|
||||||
|
java.nio.file.Files.list(dir).forEach { path ->
|
||||||
|
val name = path.fileName.toString()
|
||||||
|
val m = parseMigrationName(name) ?: return@forEach
|
||||||
|
|
||||||
|
val sql = java.nio.file.Files.readString(path)
|
||||||
|
result.add(Migration(m, sql))
|
||||||
|
}
|
||||||
|
} else if (uri.scheme == "jar") {
|
||||||
|
// запуск из jar
|
||||||
|
val fs = java.nio.file.FileSystems.newFileSystem(uri, emptyMap<String, Any>())
|
||||||
|
val dir = fs.getPath("db/migration")
|
||||||
|
|
||||||
|
java.nio.file.Files.list(dir).forEach { path ->
|
||||||
|
val name = path.fileName.toString()
|
||||||
|
val m = parseMigrationName(name) ?: return@forEach
|
||||||
|
|
||||||
|
val sql = java.nio.file.Files.readString(path)
|
||||||
|
result.add(Migration(m, sql))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.sortedBy { it.version }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun parseMigrationName(name: String): Int? {
|
||||||
|
val regex = Regex("""V(\d+)__.*\.sql""")
|
||||||
|
val match = regex.matchEntire(name) ?: return null
|
||||||
|
return match.groupValues[1].toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runMigrations() {
|
||||||
|
val ds = Database.dataSource
|
||||||
|
|
||||||
|
ds.connection.use { conn ->
|
||||||
|
|
||||||
|
// создаём таблицу
|
||||||
|
conn.createStatement().use {
|
||||||
|
it.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
|
version INT PRIMARY KEY,
|
||||||
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
// читаем применённые
|
||||||
|
val applied = mutableSetOf<Int>()
|
||||||
|
|
||||||
|
conn.createStatement().use { st ->
|
||||||
|
val rs = st.executeQuery("SELECT version FROM schema_migrations")
|
||||||
|
while (rs.next()) {
|
||||||
|
applied.add(rs.getInt(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val migrations = loadMigrations()
|
||||||
|
|
||||||
|
for (m in migrations) {
|
||||||
|
if (m.version in applied) continue
|
||||||
|
|
||||||
|
log.info("Applying migration V${m.version}")
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn.autoCommit = false
|
||||||
|
|
||||||
|
val statements = splitSql(m.sql)
|
||||||
|
|
||||||
|
for (stmt in statements) {
|
||||||
|
|
||||||
|
log.info("Applying migration statement:\n $stmt\n========================")
|
||||||
|
|
||||||
|
conn.createStatement().use { st ->
|
||||||
|
st.execute(stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.prepareStatement(
|
||||||
|
"INSERT INTO schema_migrations(version) VALUES (?)"
|
||||||
|
).use {
|
||||||
|
it.setInt(1, m.version)
|
||||||
|
it.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
conn.rollback()
|
||||||
|
throw RuntimeException("Migration V${m.version} failed", e)
|
||||||
|
} finally {
|
||||||
|
conn.autoCommit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun splitSql(sql: String): List<String> {
|
||||||
|
return sql
|
||||||
|
.split(";")
|
||||||
|
.map { it.trim() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.pavloveugene.iot.backend.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BaseMessageDto(
|
||||||
|
val v: Int,
|
||||||
|
val id: UInt,
|
||||||
|
val t: MessageType,
|
||||||
|
val ts: Long,
|
||||||
|
val d: UInt,
|
||||||
|
val p: JsonElement
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class MessageType {
|
||||||
|
@SerialName("t") TELEMETRY,
|
||||||
|
@SerialName("e") EVENT,
|
||||||
|
@SerialName("c") COMMAND
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.pavloveugene.iot.backend.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class EventDto (
|
||||||
|
val t: String,
|
||||||
|
val v: Int,
|
||||||
|
val hp: Int,
|
||||||
|
val hl: Int,
|
||||||
|
val rs: Int,
|
||||||
|
val ip: Int,
|
||||||
|
val si: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.pavloveugene.iot.backend.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FirmwareUpdateCommandDto(
|
||||||
|
val t: String,
|
||||||
|
val u: String,
|
||||||
|
val s: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
|
||||||
|
@kotlinx.serialization.Serializable
|
||||||
|
data class MessageDto(
|
||||||
|
val v: Int,
|
||||||
|
val id: UInt,
|
||||||
|
val t: MessageType,
|
||||||
|
val ts: Long,
|
||||||
|
val d: UInt,
|
||||||
|
val p: JsonElement
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class MessageType {
|
||||||
|
@SerialName("t") TELEMETRY,
|
||||||
|
@SerialName("e") EVENT,
|
||||||
|
@SerialName("c") COMMAND
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.pavloveugene.iot.backend.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProtocolMessage(
|
||||||
|
val v: Int,
|
||||||
|
val id: String,
|
||||||
|
val type: String,
|
||||||
|
val ts: Long,
|
||||||
|
val deviceId: String,
|
||||||
|
val payload: JsonObject
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.pavloveugene.iot.backend.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TelemetryDto(
|
||||||
|
val m: String,
|
||||||
|
val s: String,
|
||||||
|
val u: String,
|
||||||
|
val v: List<List<Double>>
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package org.pavloveugene.iot.backend.routes
|
||||||
|
|
||||||
|
class ApiRoutes {
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package org.pavloveugene.iot.backend.routes
|
||||||
|
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.http.content.PartData
|
||||||
|
import io.ktor.http.content.forEachPart
|
||||||
|
import io.ktor.http.content.streamProvider
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.application.call
|
||||||
|
import io.ktor.server.request.receiveMultipart
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.response.respondFile
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.post
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.pavloveugene.iot.backend.config.AppConfig.storagePath
|
||||||
|
import org.pavloveugene.iot.backend.db.Database
|
||||||
|
import org.pavloveugene.iot.backend.services.sendUpdateCommand
|
||||||
|
import org.pavloveugene.iot.backend.services.updateUri
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger("FirmwareRoutes")
|
||||||
|
|
||||||
|
fun Route.firmwareRouting(app: Application) {
|
||||||
|
uploadFirmware(app)
|
||||||
|
getFirmware(app)
|
||||||
|
otaTrigger()
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val DEVICE_ID = "device_id"
|
||||||
|
|
||||||
|
fun Route.otaTrigger() {
|
||||||
|
post("/ota_trigger") {
|
||||||
|
|
||||||
|
val par = call.parameters[DEVICE_ID]
|
||||||
|
if (par.isNullOrBlank()) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Device id is mandatory")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
val devId = par.toLong();
|
||||||
|
log.info("OTA trigger request. id: $par")
|
||||||
|
|
||||||
|
if (Database.queryOne("select count(0) cnt from devices where id=?", listOf(devId))
|
||||||
|
?.get("cnt") as Number == 0
|
||||||
|
) {
|
||||||
|
log.error("No device has id $devId in database")
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Invalid device id")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
if (sendUpdateCommand(devId)) {
|
||||||
|
call.respond(HttpStatusCode.OK)
|
||||||
|
} else {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Something went wrong")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UploadResponse(
|
||||||
|
val status: String,
|
||||||
|
val filename: String,
|
||||||
|
val version: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Route.uploadFirmware(app: Application) {
|
||||||
|
post("/firmware_upload") {
|
||||||
|
|
||||||
|
log.info("Uploading Firmware file")
|
||||||
|
|
||||||
|
val multipart = call.receiveMultipart()
|
||||||
|
|
||||||
|
var deviceId: Long? = null
|
||||||
|
var version: Int
|
||||||
|
var fileBytes: ByteArray? = null
|
||||||
|
|
||||||
|
multipart.forEachPart { part ->
|
||||||
|
when (part) {
|
||||||
|
is PartData.FormItem -> {
|
||||||
|
when (part.name) {
|
||||||
|
DEVICE_ID -> deviceId = part.value.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is PartData.FileItem -> {
|
||||||
|
fileBytes = part.streamProvider().readBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
part.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deviceId == null) {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Missing device id")
|
||||||
|
log.error("Device id is missing")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
val bytes = fileBytes ?: run {
|
||||||
|
call.respond(HttpStatusCode.BadRequest, "Missing file")
|
||||||
|
log.error("File is missing")
|
||||||
|
return@post
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Parameters ok")
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// 👉 генерим имя файла
|
||||||
|
val baseDir = File(storagePath)
|
||||||
|
val filename = "${java.util.UUID.randomUUID()}.bin"
|
||||||
|
val file = File(baseDir, filename)
|
||||||
|
val path = file.path
|
||||||
|
|
||||||
|
file.parentFile.mkdirs()
|
||||||
|
file.writeBytes(bytes)
|
||||||
|
|
||||||
|
log.info("File created: $path")
|
||||||
|
|
||||||
|
// 👉 считаем sha256
|
||||||
|
val sha256 = java.security.MessageDigest
|
||||||
|
.getInstance("SHA-256")
|
||||||
|
.digest(bytes)
|
||||||
|
.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
log.info("sha-256= $sha256")
|
||||||
|
|
||||||
|
// 👉 сохраняем в БД
|
||||||
|
while (true) {
|
||||||
|
version = ((Database.queryOne(
|
||||||
|
"""
|
||||||
|
select coalesce(max(version), 0) + 1 as v
|
||||||
|
from firmware
|
||||||
|
where device_id = ?
|
||||||
|
""",
|
||||||
|
listOf(deviceId)
|
||||||
|
)?.get("v") as Number?)?.toInt() ?: 1)
|
||||||
|
|
||||||
|
log.info("version: $version")
|
||||||
|
|
||||||
|
try {
|
||||||
|
Database.execute(
|
||||||
|
"""
|
||||||
|
insert into firmware (device_id, version, path, sha256, size)
|
||||||
|
values (?, ?, ?, ?, ?)
|
||||||
|
""", listOf(
|
||||||
|
deviceId,
|
||||||
|
version,
|
||||||
|
path,
|
||||||
|
sha256,
|
||||||
|
bytes.size
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.info("Retry insert firmware: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
call.respond(
|
||||||
|
UploadResponse(
|
||||||
|
status = "ok",
|
||||||
|
filename = filename,
|
||||||
|
version = version
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e.message, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Route.getFirmware(app: Application) {
|
||||||
|
get(updateUri) {
|
||||||
|
val id = call.parameters["id"]!!.toInt()
|
||||||
|
|
||||||
|
val row = Database.queryOne(
|
||||||
|
"select path from firmware where id=?",
|
||||||
|
listOf(id)
|
||||||
|
)
|
||||||
|
if (row == null) {
|
||||||
|
return@get call.respond(HttpStatusCode.NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
val path = row["path"] as String
|
||||||
|
val file = File(path)
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
return@get call.respond(HttpStatusCode.NotFound)
|
||||||
|
}
|
||||||
|
call.respondFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.pavloveugene.iot.backend.routes
|
||||||
|
|
||||||
|
import MessageDto
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.pavloveugene.iot.backend.config.AppConfig
|
||||||
|
import org.pavloveugene.iot.backend.services.ProtocolService
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
fun Route.protocolRoutes() {
|
||||||
|
|
||||||
|
route(AppConfig.apiPrefix + "/protocol") {
|
||||||
|
|
||||||
|
get("/health") {
|
||||||
|
call.respond("ok")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package org.pavloveugene.iot.backend.routes
|
||||||
|
|
||||||
|
import MessageDto
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.websocket.*
|
||||||
|
import io.ktor.utils.io.CancellationException
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.pavloveugene.iot.backend.config.AppConfig
|
||||||
|
import org.pavloveugene.iot.backend.services.DeviceConnections
|
||||||
|
import org.pavloveugene.iot.backend.services.ProtocolService
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private val log= LoggerFactory.getLogger("ProtocolWebSocket")
|
||||||
|
|
||||||
|
fun Route.protocolWebSocket() {
|
||||||
|
val json = Json {
|
||||||
|
ignoreUnknownKeys = false
|
||||||
|
}
|
||||||
|
|
||||||
|
val protocolService = ProtocolService(json)
|
||||||
|
|
||||||
|
webSocket(AppConfig.wsPath) {
|
||||||
|
|
||||||
|
log.info("WS connected")
|
||||||
|
|
||||||
|
var devId: UInt? = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (frame in incoming) {
|
||||||
|
if (frame is Frame.Text) {
|
||||||
|
val text = frame.readText()
|
||||||
|
|
||||||
|
val msg = try {
|
||||||
|
json.decodeFromString<MessageDto>(text)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.info("WS decode error: ${e.message}")
|
||||||
|
safeSend("""{"error":"invalid"}""")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
devId = msg.d
|
||||||
|
protocolService.handleMessage(msg, this, call.request)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.info("WS handler error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
// нормальный shutdown — молчим
|
||||||
|
} catch (e: IOException) {
|
||||||
|
log.info("WS disconnected: ${e.message}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.info("WS error: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
devId?.let {
|
||||||
|
DeviceConnections.unregister(devId)
|
||||||
|
log.info("WS disconnected: $it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun DefaultWebSocketServerSession.safeSend(text: String) {
|
||||||
|
try {
|
||||||
|
send(text)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
import org.pavloveugene.iot.backend.db.Database
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.sql.SQLException
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger("CleanupService")
|
||||||
|
|
||||||
|
fun executeCleanup() {
|
||||||
|
val ds = Database.dataSource
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
val cutoff = start / 1000 - 60 * 60 * 24 * 2
|
||||||
|
|
||||||
|
log.info("Begin cleanup")
|
||||||
|
|
||||||
|
ds.connection.use { conn ->
|
||||||
|
conn.autoCommit = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
var total = 0
|
||||||
|
|
||||||
|
do {
|
||||||
|
val deleted = conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
delete t
|
||||||
|
from telemetry t
|
||||||
|
join (
|
||||||
|
select id
|
||||||
|
from telemetry
|
||||||
|
where ts < ? and processed = true
|
||||||
|
limit 1000
|
||||||
|
) p on p.id = t.id
|
||||||
|
""".trimIndent()
|
||||||
|
).use { ps ->
|
||||||
|
ps.setLong(1, cutoff)
|
||||||
|
ps.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
total += deleted
|
||||||
|
|
||||||
|
if (deleted > 0) {
|
||||||
|
log.info("Deleted $deleted rows (total $total)")
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
} while (deleted > 0)
|
||||||
|
|
||||||
|
} catch (e: SQLException) {
|
||||||
|
conn.rollback()
|
||||||
|
throw RuntimeException("Error during cleanup", e)
|
||||||
|
} finally {
|
||||||
|
conn.autoCommit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Cleanup complete in ${System.currentTimeMillis() - start} ms")
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
import io.ktor.server.request.ApplicationRequest
|
||||||
|
import io.ktor.websocket.WebSocketSession
|
||||||
|
|
||||||
|
data class DeviceConnection(
|
||||||
|
val session: WebSocketSession,
|
||||||
|
val request: ApplicationRequest,
|
||||||
|
var lastId: UInt = 0u,
|
||||||
|
var lastSeen: Long,
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
import io.ktor.websocket.WebSocketSession
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
object DeviceConnections {
|
||||||
|
private val map = ConcurrentHashMap<UInt, DeviceConnection>()
|
||||||
|
|
||||||
|
fun register(deviceId: UInt, connection: DeviceConnection) {
|
||||||
|
map[deviceId] = connection
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unregister(deviceId: UInt) {
|
||||||
|
map.remove(deviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(deviceId: UInt): DeviceConnection? = map[deviceId]
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
class DeviceManager {
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
import io.ktor.serialization.kotlinx.json.json
|
||||||
|
import io.ktor.server.application.install
|
||||||
|
import io.ktor.server.engine.embeddedServer
|
||||||
|
import io.ktor.server.netty.Netty
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
import io.ktor.server.websocket.WebSockets
|
||||||
|
import io.ktor.server.websocket.pingPeriod
|
||||||
|
import io.ktor.server.websocket.timeout
|
||||||
|
import io.netty.handler.codec.compression.StandardCompressionOptions.gzip
|
||||||
|
import org.pavloveugene.iot.backend.config.AppConfig
|
||||||
|
import org.pavloveugene.iot.backend.routes.protocolRoutes
|
||||||
|
import org.pavloveugene.iot.backend.routes.protocolWebSocket
|
||||||
|
import java.time.Duration
|
||||||
|
import io.ktor.server.plugins.compression.*
|
||||||
|
import org.pavloveugene.iot.backend.routes.firmwareRouting
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger("KtorServer")
|
||||||
|
|
||||||
|
fun startKtorServer() {
|
||||||
|
|
||||||
|
val server = embeddedServer(
|
||||||
|
Netty,
|
||||||
|
port = AppConfig.serverPort,
|
||||||
|
host = AppConfig.serverHost,
|
||||||
|
) {
|
||||||
|
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json()
|
||||||
|
}
|
||||||
|
|
||||||
|
install(WebSockets) {
|
||||||
|
pingPeriod = Duration.ofSeconds(15)
|
||||||
|
timeout = Duration.ofSeconds(30)
|
||||||
|
maxFrameSize = Long.MAX_VALUE
|
||||||
|
masking = false
|
||||||
|
}
|
||||||
|
|
||||||
|
routing {
|
||||||
|
log.info("CONFIG = ${application.environment.config.toMap()}")
|
||||||
|
protocolRoutes()
|
||||||
|
protocolWebSocket()
|
||||||
|
firmwareRouting(application)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Runtime.getRuntime().addShutdownHook(Thread {
|
||||||
|
log.info("Shutting down...")
|
||||||
|
try {
|
||||||
|
server.stop(1000, 2000)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.info("Shutdown error: ${e.message}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.start(wait = true)
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
import org.pavloveugene.iot.backend.db.Database
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger("NormalizeService")
|
||||||
|
|
||||||
|
fun runNormalizeLoop() {
|
||||||
|
var total = 0
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
verifyData()
|
||||||
|
do {
|
||||||
|
val count = normalizeBatch()
|
||||||
|
total += count
|
||||||
|
log.info("Processed batch: $count")
|
||||||
|
} while (count > 0)
|
||||||
|
|
||||||
|
log.info("Done. Total processed: $total in ${System.currentTimeMillis() - start} ms")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun normalizeBatch(): Int {
|
||||||
|
val ds = Database.dataSource
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
ds.connection.use { conn ->
|
||||||
|
|
||||||
|
conn.autoCommit = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
conn.createStatement().use { stmt ->
|
||||||
|
stmt.executeUpdate(
|
||||||
|
"""
|
||||||
|
create temporary table pack as
|
||||||
|
select t.id
|
||||||
|
from telemetry t
|
||||||
|
where t.processed=false and t.defective=false
|
||||||
|
order by t.id
|
||||||
|
limit 1000
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt.executeQuery("select count(0) from pack").use { rs ->
|
||||||
|
rs.next()
|
||||||
|
count = rs.getInt(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count > 0) {
|
||||||
|
|
||||||
|
stmt.executeUpdate(
|
||||||
|
"""
|
||||||
|
INSERT INTO telemetry_data (device_id, ts, metric, source, value, unit)
|
||||||
|
SELECT t.device_id
|
||||||
|
, t.ts * 1000 + CAST(JSON_UNQUOTE(JSON_EXTRACT(t.payload, CONCAT('$[', seq.i, '][0]'))) AS double)
|
||||||
|
, t.metric
|
||||||
|
, t.source
|
||||||
|
, CAST(JSON_UNQUOTE(JSON_EXTRACT(t.payload, CONCAT('$[', seq.i, '][1]'))) AS DOUBLE) * coalesce(u.multiplier, 1)
|
||||||
|
, coalesce(u.target_unit, t.unit)
|
||||||
|
FROM pack p
|
||||||
|
join telemetry t on t.id = p.id
|
||||||
|
JOIN ( SELECT 0 AS i
|
||||||
|
UNION ALL SELECT 1
|
||||||
|
UNION ALL SELECT 2
|
||||||
|
UNION ALL SELECT 3
|
||||||
|
UNION ALL SELECT 4
|
||||||
|
UNION ALL SELECT 5
|
||||||
|
UNION ALL SELECT 6
|
||||||
|
UNION ALL SELECT 7
|
||||||
|
UNION ALL SELECT 8
|
||||||
|
UNION ALL SELECT 9
|
||||||
|
UNION ALL SELECT 10
|
||||||
|
UNION ALL SELECT 11
|
||||||
|
UNION ALL SELECT 12
|
||||||
|
UNION ALL SELECT 13
|
||||||
|
UNION ALL SELECT 14
|
||||||
|
UNION ALL SELECT 15
|
||||||
|
UNION ALL SELECT 16
|
||||||
|
UNION ALL SELECT 17
|
||||||
|
UNION ALL SELECT 18
|
||||||
|
UNION ALL SELECT 19
|
||||||
|
UNION ALL SELECT 20
|
||||||
|
UNION ALL SELECT 21
|
||||||
|
UNION ALL SELECT 22
|
||||||
|
UNION ALL SELECT 23
|
||||||
|
UNION ALL SELECT 24
|
||||||
|
UNION ALL SELECT 25
|
||||||
|
UNION ALL SELECT 26
|
||||||
|
UNION ALL SELECT 27
|
||||||
|
UNION ALL SELECT 28
|
||||||
|
UNION ALL SELECT 29
|
||||||
|
UNION ALL SELECT 30
|
||||||
|
UNION ALL SELECT 31
|
||||||
|
) AS seq ON seq.i < JSON_LENGTH(t.payload)
|
||||||
|
left join units u on u.unit = t.unit
|
||||||
|
WHERE JSON_EXTRACT(t.payload, CONCAT('$[', seq.i, ']')) IS NOT NULL
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt.executeUpdate(
|
||||||
|
"""
|
||||||
|
update
|
||||||
|
telemetry t
|
||||||
|
join pack p on t.id = p.id
|
||||||
|
set t.processed = true
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.executeUpdate("drop temporary table pack")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
conn.rollback()
|
||||||
|
throw RuntimeException("Error during normalize", e)
|
||||||
|
} finally {
|
||||||
|
conn.autoCommit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyData(): Boolean {
|
||||||
|
val ds = Database.dataSource
|
||||||
|
var ret = true;
|
||||||
|
|
||||||
|
log.info("Executing verification")
|
||||||
|
|
||||||
|
ds.connection.use { conn ->
|
||||||
|
|
||||||
|
conn.autoCommit = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
conn.createStatement().use { stmt ->
|
||||||
|
stmt.executeUpdate(
|
||||||
|
"""
|
||||||
|
create temporary table err as
|
||||||
|
select t.id
|
||||||
|
, case
|
||||||
|
when t.unit = '' then 'No unit'
|
||||||
|
else 'Invalid unit'
|
||||||
|
end reason
|
||||||
|
from telemetry t
|
||||||
|
where t.processed=false and t.defective=false
|
||||||
|
and (t.unit = '' or (
|
||||||
|
t.unit not in (select unit from units)
|
||||||
|
and t.unit not in (select target_unit from units)
|
||||||
|
and t.unit not in ('v', 'a', 'm', 'g', 'kg', 'raw')
|
||||||
|
))
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt.executeQuery("select count(0) from err").use { rs ->
|
||||||
|
rs.next()
|
||||||
|
val count = rs.getInt(1)
|
||||||
|
ret = count == 0
|
||||||
|
if (ret) {
|
||||||
|
log.info("All ok!")
|
||||||
|
} else {
|
||||||
|
log.info("$count errors detected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt.executeUpdate(
|
||||||
|
"""
|
||||||
|
update telemetry t
|
||||||
|
join err e on e.id=t.id
|
||||||
|
set t.defective=true, t.defective_reason=e.reason
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
|
||||||
|
stmt.executeUpdate("drop temporary table err")
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
conn.rollback()
|
||||||
|
} finally {
|
||||||
|
conn.autoCommit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Verification complete")
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
import MessageDto
|
||||||
|
import io.ktor.server.request.ApplicationRequest
|
||||||
|
import io.ktor.websocket.WebSocketSession
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.pavloveugene.iot.backend.db.Database
|
||||||
|
import org.pavloveugene.iot.backend.dto.EventDto
|
||||||
|
import org.pavloveugene.iot.backend.dto.TelemetryDto
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger("ProtocolService")
|
||||||
|
|
||||||
|
class ProtocolService(
|
||||||
|
private val json: Json
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun handleMessage(msg: MessageDto, session: WebSocketSession, request: ApplicationRequest) {
|
||||||
|
when (msg.t) {
|
||||||
|
MessageType.TELEMETRY -> {
|
||||||
|
handleTelemetry(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType.EVENT -> {
|
||||||
|
log.info("=== EVENT ===")
|
||||||
|
log.info("${msg.p}")
|
||||||
|
handleEvent(msg, session, request)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageType.COMMAND -> {
|
||||||
|
log.info("=== COMMAND ===")
|
||||||
|
log.info("${msg.p}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun intToIp(ip: Int): String {
|
||||||
|
val reverse = Integer.reverseBytes(ip)
|
||||||
|
val bytes = byteArrayOf(
|
||||||
|
(reverse shr 24).toByte(),
|
||||||
|
(reverse shr 16).toByte(),
|
||||||
|
(reverse shr 8).toByte(),
|
||||||
|
reverse.toByte()
|
||||||
|
)
|
||||||
|
return InetAddress.getByAddress(bytes).hostAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEvent(msg: MessageDto, session: WebSocketSession, request: ApplicationRequest) {
|
||||||
|
val payload = json.decodeFromJsonElement(EventDto.serializer(), msg.p)
|
||||||
|
when (payload.t) {
|
||||||
|
"hb" -> {
|
||||||
|
log.info("=== HB devId = ${msg.d} IP = ${intToIp(payload.ip)} ===")
|
||||||
|
|
||||||
|
val connection = DeviceConnections.get(msg.d)
|
||||||
|
if (connection == null) {
|
||||||
|
DeviceConnections.register(
|
||||||
|
msg.d, DeviceConnection(
|
||||||
|
session = session,
|
||||||
|
lastSeen = System.currentTimeMillis(),
|
||||||
|
request = request
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
connection.lastSeen = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleTelemetry(msg: MessageDto) {
|
||||||
|
val ds = Database.dataSource
|
||||||
|
|
||||||
|
ds.connection.use { conn ->
|
||||||
|
conn.autoCommit = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ensure device
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO devices(id)
|
||||||
|
VALUES (?)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id
|
||||||
|
"""
|
||||||
|
).use {
|
||||||
|
it.setLong(1, msg.d.toLong())
|
||||||
|
it.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check enabled
|
||||||
|
val isEnabled = conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
SELECT is_enabled FROM devices WHERE id = ?
|
||||||
|
"""
|
||||||
|
).use {
|
||||||
|
it.setLong(1, msg.d.toLong())
|
||||||
|
val rs = it.executeQuery()
|
||||||
|
if (rs.next()) rs.getBoolean(1) else false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
log.info("device ${msg.d} locked, message ignored")
|
||||||
|
conn.commit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val payload = json.decodeFromJsonElement(TelemetryDto.serializer(), msg.p)
|
||||||
|
|
||||||
|
log.info("=== TELEMETRY ===")
|
||||||
|
log.info("device=${msg.d}")
|
||||||
|
log.info("ts=${msg.ts}")
|
||||||
|
log.info("metric=${payload.m}")
|
||||||
|
log.info("values=${payload.v}")
|
||||||
|
|
||||||
|
// insert telemetry
|
||||||
|
conn.prepareStatement(
|
||||||
|
"""
|
||||||
|
INSERT INTO telemetry(
|
||||||
|
device_id, ts, metric, source, unit, payload
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
"""
|
||||||
|
).use {
|
||||||
|
it.setLong(1, msg.d.toLong())
|
||||||
|
it.setLong(2, msg.ts)
|
||||||
|
it.setString(3, payload.m)
|
||||||
|
it.setString(4, payload.s)
|
||||||
|
it.setString(5, payload.u)
|
||||||
|
|
||||||
|
val payloadJson =
|
||||||
|
json.encodeToString(ListSerializer(ListSerializer(Double.serializer())), payload.v)
|
||||||
|
it.setString(6, payloadJson)
|
||||||
|
|
||||||
|
it.executeUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
conn.rollback()
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
conn.autoCommit = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package org.pavloveugene.iot.backend.services
|
||||||
|
|
||||||
|
import MessageDto
|
||||||
|
import MessageType
|
||||||
|
import io.ktor.server.plugins.origin
|
||||||
|
import io.ktor.util.date.getTimeMillis
|
||||||
|
import io.ktor.websocket.send
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.pavloveugene.iot.backend.db.Database
|
||||||
|
import org.pavloveugene.iot.backend.db.Database.queryOne
|
||||||
|
import org.pavloveugene.iot.backend.dto.FirmwareUpdateCommandDto
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger("OTA")
|
||||||
|
|
||||||
|
const val updateUri = "/firmware/download"
|
||||||
|
|
||||||
|
suspend fun sendUpdateCommand(
|
||||||
|
devId: Long
|
||||||
|
): Boolean {
|
||||||
|
var ret = true
|
||||||
|
|
||||||
|
log.info("Sending update command to device: $devId")
|
||||||
|
|
||||||
|
val ver = queryOne(
|
||||||
|
"""
|
||||||
|
select *
|
||||||
|
from firmware
|
||||||
|
where device_id = ?
|
||||||
|
order by version desc
|
||||||
|
limit 1
|
||||||
|
""".trimIndent(), listOf(devId)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ver == null) {
|
||||||
|
log.error("No firmware uploaded for device: $devId.")
|
||||||
|
ret = false
|
||||||
|
} else {
|
||||||
|
val firmware_id = ver["id"] as Int
|
||||||
|
val sha256 = ver["sha256"] as String
|
||||||
|
|
||||||
|
var update = queryOne(
|
||||||
|
"""
|
||||||
|
select fu.*
|
||||||
|
from firmware_updates fu
|
||||||
|
where fu.firmware_id = ?
|
||||||
|
""".trimIndent(),
|
||||||
|
listOf(firmware_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (update == null) {
|
||||||
|
val update_id = Database.insertAndReturnId(
|
||||||
|
"""
|
||||||
|
insert into firmware_updates (firmware_id, status)
|
||||||
|
values (?, ?)
|
||||||
|
""".trimIndent(),
|
||||||
|
listOf(firmware_id, "pending")
|
||||||
|
)
|
||||||
|
|
||||||
|
update = queryOne(
|
||||||
|
"""
|
||||||
|
select fu.*
|
||||||
|
from firmware_updates fu
|
||||||
|
where fu.id = ?
|
||||||
|
""".trimIndent(),
|
||||||
|
listOf(update_id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update == null) {
|
||||||
|
ret = false
|
||||||
|
log.error("No firmware sent to device: $devId. Something went wrong.")
|
||||||
|
} else {
|
||||||
|
val status = update["status"] as String
|
||||||
|
|
||||||
|
if (status == "pending") {
|
||||||
|
|
||||||
|
val deviceConnection = DeviceConnections.get(devId.toUInt())
|
||||||
|
if (deviceConnection == null) {
|
||||||
|
ret = false
|
||||||
|
log.warn("No firmware sent to device: $devId. Device not connected.")
|
||||||
|
} else {
|
||||||
|
val origin = deviceConnection.request.origin
|
||||||
|
val portPart =
|
||||||
|
if (origin.serverPort == 80 || origin.serverPort == 443) ""
|
||||||
|
else ":${origin.serverPort}"
|
||||||
|
|
||||||
|
val url = "${origin.scheme}://${origin.serverHost}$portPart$updateUri?id=${firmware_id}"
|
||||||
|
|
||||||
|
val updPayload = FirmwareUpdateCommandDto(
|
||||||
|
t = "fw",
|
||||||
|
u = url,
|
||||||
|
s = sha256,
|
||||||
|
)
|
||||||
|
|
||||||
|
deviceConnection.lastId++
|
||||||
|
|
||||||
|
val updateCommand = MessageDto(
|
||||||
|
v = 1,
|
||||||
|
id = deviceConnection.lastId,
|
||||||
|
d = devId.toUInt(),
|
||||||
|
t = MessageType.COMMAND,
|
||||||
|
ts = getTimeMillis(),
|
||||||
|
p = Json.encodeToJsonElement(FirmwareUpdateCommandDto.serializer(), updPayload),
|
||||||
|
)
|
||||||
|
|
||||||
|
val cmdJson: String = Json.encodeToJsonElement(
|
||||||
|
MessageDto.serializer(),
|
||||||
|
updateCommand
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
deviceConnection.session.send(cmdJson)
|
||||||
|
|
||||||
|
Database.execute(
|
||||||
|
"""
|
||||||
|
update firmware_updates
|
||||||
|
set status = 'sent'
|
||||||
|
where id = ?
|
||||||
|
""".trimIndent(), listOf(update["id"])
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info("Firmware update sent for device: $devId. Command: $cmdJson")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ret = false
|
||||||
|
log.error("Error updating device $devId", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package org.pavloveugene.iot.backend.utils
|
||||||
|
|
||||||
|
class JsonUtils {
|
||||||
|
}
|
||||||
16
backend/src/main/resources/application.conf.example
Normal file
16
backend/src/main/resources/application.conf.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
ktor {
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 8080
|
||||||
|
}
|
||||||
|
|
||||||
|
app {
|
||||||
|
name = "iot-backend"
|
||||||
|
}
|
||||||
|
|
||||||
|
api {
|
||||||
|
prefix = "/api/v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
ws {
|
||||||
|
path = "/ws"
|
||||||
|
}
|
||||||
11
backend/src/main/resources/logback.xml
Normal file
11
backend/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss} %-5level %logger - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
17
contract/api/common.schema.json
Normal file
17
contract/api/common.schema.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$id": "contract/api/common.schema.json",
|
||||||
|
"type": "object", "required": ["ok"],
|
||||||
|
"properties": {
|
||||||
|
"ok": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"data": {},
|
||||||
|
"error": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"code": { "type": "string" },
|
||||||
|
"message": { "type": "string" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
contract/api/stats.schema.json
Normal file
0
contract/api/stats.schema.json
Normal file
18
contract/api/timeseries.schema.json
Normal file
18
contract/api/timeseries.schema.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["points"],
|
||||||
|
"properties": {
|
||||||
|
"points": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["ts", "value"],
|
||||||
|
"properties": {
|
||||||
|
"ts": { "type": "integer" },
|
||||||
|
"value": {}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
contract/ingest.zip
Normal file
BIN
contract/ingest.zip
Normal file
Binary file not shown.
29
contract/ingest/command.schema.json
Normal file
29
contract/ingest/command.schema.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "contract/ingest/command.schema.json",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["t"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
|
||||||
|
"properties": {
|
||||||
|
"t": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["fw"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": { "t": { "const": "fw" } }
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["u", "s"],
|
||||||
|
"properties": {
|
||||||
|
"u": { "type": "string", "format": "uri" },
|
||||||
|
"s": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
77
contract/ingest/common.schema.json
Normal file
77
contract/ingest/common.schema.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "contract/ingest/common.schema.json",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["v", "t", "id", "ts", "d", "p"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"v": {
|
||||||
|
"type": "integer",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 4294967295,
|
||||||
|
"description": "Autoincrement message id. Unique per session"
|
||||||
|
},
|
||||||
|
"t": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["t", "e", "c"],
|
||||||
|
"description": "Message type (<t>elemetry, <e>vent, <c>ommand)"
|
||||||
|
},
|
||||||
|
"ts": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1600000000,
|
||||||
|
"description": "Unix time in seconds (UTC)"
|
||||||
|
},
|
||||||
|
"d": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"description": "Device ID. Unique per scope"
|
||||||
|
},
|
||||||
|
"p": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "User data. Type-specific."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"required": ["t"],
|
||||||
|
"properties": { "t": { "const": "t" } }
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["p"],
|
||||||
|
"properties": {
|
||||||
|
"p": { "$ref": "telemetry.schema.json" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"required": ["t"],
|
||||||
|
"properties": { "t": { "const": "e" } }
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["p"],
|
||||||
|
"properties": {
|
||||||
|
"p": { "$ref": "event.schema.json" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"required": ["t"],
|
||||||
|
"properties": { "t": { "const": "c" } }
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["p"],
|
||||||
|
"properties": {
|
||||||
|
"p": { "$ref": "command.schema.json" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
contract/ingest/event.schema.json
Normal file
11
contract/ingest/event.schema.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type"],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["hb"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
44
contract/ingest/telemetry.schema.json
Normal file
44
contract/ingest/telemetry.schema.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"$id": "contract/ingest/telemetry.schema.json",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["m", "s", "u", "v"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"m": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Metric type (e.g. t=temperature, h=humidity)"
|
||||||
|
},
|
||||||
|
"s": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Source identifier (sensor/channel)"
|
||||||
|
},
|
||||||
|
"u": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"description": "Unit (e.g. c, pct, v)"
|
||||||
|
},
|
||||||
|
"v": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 2,
|
||||||
|
"maxItems": 2,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Delta time (seconds) from base ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"description": "Measured value"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
contract/readme.md
Normal file
18
contract/readme.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## Контракты
|
||||||
|
|
||||||
|
В системе используются два независимых контракта:
|
||||||
|
|
||||||
|
### 1. Ingest (devices → backend)
|
||||||
|
|
||||||
|
- поток сырых данных
|
||||||
|
- оптимизирован под запись
|
||||||
|
- минимальный размер сообщений
|
||||||
|
- формат: telemetry / event
|
||||||
|
|
||||||
|
### 2. API (backend → mobile)
|
||||||
|
|
||||||
|
- агрегированные данные
|
||||||
|
- оптимизирован под чтение
|
||||||
|
- формат зависит от UI (графики, таблицы, статусы)
|
||||||
|
|
||||||
|
⚠️ Эти контракты НЕ обязаны совпадать
|
||||||
4
db/README.md
Normal file
4
db/README.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- все изменения только через migrations
|
||||||
|
- schema.sql = текущее состояние
|
||||||
|
- старые migrations не редактируем
|
||||||
|
- версия базы = schema_version
|
||||||
20
db/migrations/V1__init_protocol.sql
Normal file
20
db/migrations/V1__init_protocol.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE devices (
|
||||||
|
id BIGINT PRIMARY KEY,
|
||||||
|
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE telemetry (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
device_id BIGINT NOT NULL,
|
||||||
|
ts BIGINT NOT NULL,
|
||||||
|
metric VARCHAR(32) NOT NULL,
|
||||||
|
source VARCHAR(64) NOT NULL,
|
||||||
|
unit VARCHAR(16) NOT NULL,
|
||||||
|
payload JSON NOT NULL,
|
||||||
|
processed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
FOREIGN KEY (device_id) REFERENCES devices(id)
|
||||||
|
);
|
||||||
20
db/migrations/V2__telemetry_data.sql
Normal file
20
db/migrations/V2__telemetry_data.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE telemetry_data (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
|
||||||
|
device_id BIGINT NOT NULL,
|
||||||
|
ts BIGINT NOT NULL,
|
||||||
|
|
||||||
|
metric VARCHAR(32) NOT NULL,
|
||||||
|
source VARCHAR(64) NOT NULL,
|
||||||
|
|
||||||
|
value DOUBLE NOT NULL,
|
||||||
|
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
INDEX idx_device_metric_ts (device_id, metric, ts),
|
||||||
|
INDEX idx_ts (ts),
|
||||||
|
|
||||||
|
FOREIGN KEY (device_id) REFERENCES devices(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_processed ON telemetry(processed);
|
||||||
2
db/migrations/V3__telemetry_processed_index.sql
Normal file
2
db/migrations/V3__telemetry_processed_index.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE INDEX idx_telemetry_processed_id
|
||||||
|
ON telemetry (processed, id);
|
||||||
16
db/migrations/V4__telemetry_units.sql
Normal file
16
db/migrations/V4__telemetry_units.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
alter TABLE telemetry
|
||||||
|
add defective boolean default false,
|
||||||
|
add defective_reason varchar(64);
|
||||||
|
|
||||||
|
alter TABLE telemetry_data ADD
|
||||||
|
unit varchar (16);
|
||||||
|
|
||||||
|
create table units (
|
||||||
|
unit varchar(16) not null,
|
||||||
|
target_unit varchar(16) not null,
|
||||||
|
multiplier double not null,
|
||||||
|
primary key (unit)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into units (unit, target_unit, multiplier)
|
||||||
|
values ('c_x100', 'c', 0.01);
|
||||||
24
db/migrations/V5__firmware_updates.sql
Normal file
24
db/migrations/V5__firmware_updates.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
create table firmware (
|
||||||
|
id int not null auto_increment,
|
||||||
|
device_id bigint not null,
|
||||||
|
version int not null,
|
||||||
|
path varchar(4096) not null,
|
||||||
|
sha256 char(64),
|
||||||
|
size int,
|
||||||
|
uploaded_at timestamp not null default current_timestamp,
|
||||||
|
primary key (id),
|
||||||
|
foreign key (device_id) references devices(id),
|
||||||
|
|
||||||
|
unique(device_id, version)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table firmware_updates (
|
||||||
|
id int not null auto_increment,
|
||||||
|
firmware_id int not null,
|
||||||
|
status varchar(32) not null, -- pending / sent / applied / failed
|
||||||
|
requested_at timestamp not null default current_timestamp,
|
||||||
|
applied_at timestamp null,
|
||||||
|
|
||||||
|
primary key (id),
|
||||||
|
foreign key (firmware_id) references firmware(id)
|
||||||
|
);
|
||||||
5
db/schema.sql
Normal file
5
db/schema.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE schema_version (
|
||||||
|
version INT PRIMARY KEY,
|
||||||
|
applied_at TIMESTAMP NOT NULL,
|
||||||
|
checksum VARCHAR(64) NOT NULL
|
||||||
|
);
|
||||||
153
doc/architecture/adr-001-local-gateway.md
Normal file
153
doc/architecture/adr-001-local-gateway.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Architectural Decision Record: Local Gateway + External Backend
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Initial design considered direct communication between IoT devices (ESP32) and a backend hosted on the internet.
|
||||||
|
|
||||||
|
Concerns identified:
|
||||||
|
|
||||||
|
* High volume of small messages from devices
|
||||||
|
* Unnecessary exposure of raw telemetry to the internet
|
||||||
|
* Increased complexity on constrained devices (TLS, crypto, reconnect logic)
|
||||||
|
* Security overhead (encryption, signing on every message)
|
||||||
|
|
||||||
|
At the same time, the system does not require real-time global access to raw device data. Most data is only useful locally or in aggregated form.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Adopt a **local gateway architecture**:
|
||||||
|
|
||||||
|
```
|
||||||
|
ESP32 devices → Local Backend (LAN) → External Backend (Internet) → Mobile App
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key principles
|
||||||
|
|
||||||
|
1. **Devices communicate only within LAN**
|
||||||
|
|
||||||
|
* No direct internet access required
|
||||||
|
* No inbound connections to devices
|
||||||
|
* Devices initiate outbound connections only to local backend
|
||||||
|
|
||||||
|
2. **Local backend acts as a gateway**
|
||||||
|
|
||||||
|
* Maintains persistent secure connection (WebSocket over TLS) to external backend
|
||||||
|
* Aggregates telemetry data
|
||||||
|
* Translates and routes commands
|
||||||
|
|
||||||
|
3. **External backend is the only internet-facing component**
|
||||||
|
|
||||||
|
* Serves mobile applications
|
||||||
|
* Sends commands to local backend
|
||||||
|
* Receives aggregated data only
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
### 1. Reduced complexity on devices
|
||||||
|
|
||||||
|
ESP32 devices:
|
||||||
|
|
||||||
|
* Do not implement TLS
|
||||||
|
* Do not handle complex cryptography
|
||||||
|
* Maintain simple communication protocol
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
* Lower memory and CPU usage
|
||||||
|
* Simpler firmware
|
||||||
|
* Higher reliability
|
||||||
|
|
||||||
|
### 2. Network efficiency
|
||||||
|
|
||||||
|
Instead of many small messages:
|
||||||
|
|
||||||
|
* Local backend aggregates data
|
||||||
|
* Sends fewer, larger messages
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
* Reduced bandwidth usage
|
||||||
|
* Fewer connections
|
||||||
|
|
||||||
|
### 3. Clear trust boundary
|
||||||
|
|
||||||
|
* LAN is treated as a **controlled (conditionally trusted) environment**
|
||||||
|
* Internet is treated as **untrusted**
|
||||||
|
|
||||||
|
Security measures are concentrated at the boundary:
|
||||||
|
|
||||||
|
* TLS on gateway ↔ external backend
|
||||||
|
* Authentication and validation at gateway
|
||||||
|
|
||||||
|
### 4. Data control and privacy
|
||||||
|
|
||||||
|
* Raw telemetry remains in local network
|
||||||
|
* Only aggregated/necessary data leaves the network
|
||||||
|
* No dependency on third-party cloud for device data
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
### LAN (conditionally trusted)
|
||||||
|
|
||||||
|
Assumptions:
|
||||||
|
|
||||||
|
* Network is controlled
|
||||||
|
* Unauthorized access is unlikely but possible
|
||||||
|
|
||||||
|
Mitigations:
|
||||||
|
|
||||||
|
* Device IP whitelist
|
||||||
|
* Device identification (deviceId)
|
||||||
|
* Simple authentication (pre-shared token)
|
||||||
|
* Network segmentation (IoT VLAN)
|
||||||
|
|
||||||
|
No encryption required inside LAN.
|
||||||
|
|
||||||
|
### Internet (untrusted)
|
||||||
|
|
||||||
|
* All communication secured via TLS
|
||||||
|
* Strong authentication required
|
||||||
|
* Single controlled connection (gateway → backend)
|
||||||
|
|
||||||
|
## Command Flow
|
||||||
|
|
||||||
|
1. Mobile app → External backend
|
||||||
|
2. External backend → Local backend (via secure WebSocket)
|
||||||
|
3. Local backend → Device (LAN)
|
||||||
|
|
||||||
|
Local backend is responsible for:
|
||||||
|
|
||||||
|
* Validating commands
|
||||||
|
* Ensuring they match device capabilities
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
* Simpler device firmware
|
||||||
|
* Reduced attack surface (no exposed devices)
|
||||||
|
* Better performance and scalability
|
||||||
|
* Full control over data
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
* Requires always-on local backend
|
||||||
|
* Adds one architectural component (gateway)
|
||||||
|
* LAN cannot be treated as fully secure
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
If LAN is compromised, impact is broader than IoT telemetry leakage and should be treated as a separate security incident class.
|
||||||
|
|
||||||
|
Therefore, the system does not attempt to provide full zero-trust security within LAN, but instead applies reasonable safeguards.
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
* Optional message signing inside LAN if threat model changes
|
||||||
|
* Key rotation for device tokens
|
||||||
|
* Device revocation mechanisms
|
||||||
|
* Monitoring of gateway ↔ external connection
|
||||||
10
esp32/CMakeLists.txt
Normal file
10
esp32/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# The following five lines of boilerplate have to be in your project's
|
||||||
|
# CMakeLists in this exact order for cmake to work correctly
|
||||||
|
cmake_minimum_required(VERSION 3.22)
|
||||||
|
|
||||||
|
file(READ "${CMAKE_SOURCE_DIR}/version.txt" PROJECT_VER)
|
||||||
|
string(STRIP "${PROJECT_VER}" PROJECT_VER)
|
||||||
|
add_compile_definitions(FW_VERSION=${PROJECT_VER})
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
project(esp32)
|
||||||
32
esp32/dependencies.lock
Normal file
32
esp32/dependencies.lock
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
dependencies:
|
||||||
|
espressif/esp_websocket_client:
|
||||||
|
component_hash: c5a067a9fddea370c478017e66fac302f4b79c3d4027e9bdd42a019786cceb92
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.0'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.6.1
|
||||||
|
espressif/onewire_bus:
|
||||||
|
component_hash: d709015ba466095259228521cf1bad9c0cdaaa42a92ea5d9c88ec6c28ae89e9b
|
||||||
|
dependencies:
|
||||||
|
- name: idf
|
||||||
|
require: private
|
||||||
|
version: '>=5.0'
|
||||||
|
source:
|
||||||
|
registry_url: https://components.espressif.com/
|
||||||
|
type: service
|
||||||
|
version: 1.1.0
|
||||||
|
idf:
|
||||||
|
source:
|
||||||
|
type: idf
|
||||||
|
version: 6.0.0
|
||||||
|
direct_dependencies:
|
||||||
|
- espressif/esp_websocket_client
|
||||||
|
- espressif/onewire_bus
|
||||||
|
- idf
|
||||||
|
manifest_hash: 13e7ecd84ccc03ee20f4d74c327e5e03dc83702693f2188053e8b384ceb1adac
|
||||||
|
target: esp32
|
||||||
|
version: 3.0.0
|
||||||
35
esp32/main/CMakeLists.txt
Normal file
35
esp32/main/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
idf_component_register(
|
||||||
|
SRCS "main.cpp"
|
||||||
|
"protocol.cpp"
|
||||||
|
"wifi_manager.cpp"
|
||||||
|
"system_init.cpp"
|
||||||
|
"time_sync.cpp"
|
||||||
|
"adc_reader.cpp"
|
||||||
|
"ringbuf.cpp"
|
||||||
|
"sampler_task.cpp"
|
||||||
|
"sender_task.cpp"
|
||||||
|
"ws.cpp"
|
||||||
|
"gpio_init.cpp"
|
||||||
|
"ds18b20.cpp"
|
||||||
|
"heartbeat_task.cpp"
|
||||||
|
"fw_command.cpp"
|
||||||
|
"json_utils.cpp"
|
||||||
|
"ota.cpp"
|
||||||
|
INCLUDE_DIRS "."
|
||||||
|
REQUIRES
|
||||||
|
nvs_flash
|
||||||
|
esp_wifi
|
||||||
|
esp_netif
|
||||||
|
freertos
|
||||||
|
log
|
||||||
|
esp_timer
|
||||||
|
esp_adc
|
||||||
|
esp_websocket_client
|
||||||
|
driver
|
||||||
|
esp_driver_gpio
|
||||||
|
esp_http_client
|
||||||
|
app_update
|
||||||
|
mbedtls
|
||||||
|
)
|
||||||
|
# добавляем кастомный Kconfig
|
||||||
|
set(COMPONENT_KCONFIG "Kconfig")
|
||||||
26
esp32/main/adc_reader.cpp
Normal file
26
esp32/main/adc_reader.cpp
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#include "adc_reader.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "esp_adc/adc_oneshot.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
static adc_oneshot_unit_handle_t adc_handle;
|
||||||
|
|
||||||
|
void adc_reader_init() {
|
||||||
|
adc_oneshot_unit_init_cfg_t init_config = {};
|
||||||
|
init_config.unit_id = ADC_UNIT_1;
|
||||||
|
|
||||||
|
adc_oneshot_new_unit(&init_config, &adc_handle);
|
||||||
|
|
||||||
|
adc_oneshot_chan_cfg_t config = {};
|
||||||
|
config.atten = ADC_ATTEN_DB_12;
|
||||||
|
config.bitwidth = ADC_BITWIDTH_DEFAULT;
|
||||||
|
|
||||||
|
adc_oneshot_config_channel(adc_handle, ADC_CHANNEL_7, &config); // GPIO35
|
||||||
|
}
|
||||||
|
|
||||||
|
int adc_reader_read() {
|
||||||
|
int val = 0;
|
||||||
|
adc_oneshot_read(adc_handle, ADC_CHANNEL_7, &val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
7
esp32/main/adc_reader.h
Normal file
7
esp32/main/adc_reader.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Инициализация ADC
|
||||||
|
void adc_reader_init();
|
||||||
|
|
||||||
|
// Прочитать значение (GPIO35)
|
||||||
|
int adc_reader_read();
|
||||||
85
esp32/main/ds18b20.cpp
Normal file
85
esp32/main/ds18b20.cpp
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#include "ds18b20.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "onewire_bus.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *TAG = "ds18b20";
|
||||||
|
|
||||||
|
static onewire_bus_handle_t bus;
|
||||||
|
|
||||||
|
uint8_t ds_crc8(const uint8_t *data, int len)
|
||||||
|
{
|
||||||
|
uint8_t crc = 0;
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
uint8_t inbyte = data[i];
|
||||||
|
for (int j = 0; j < 8; j++) {
|
||||||
|
uint8_t mix = (crc ^ inbyte) & 0x01;
|
||||||
|
crc >>= 1;
|
||||||
|
if (mix) crc ^= 0x8C;
|
||||||
|
inbyte >>= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ds18b20_init(gpio_num_t pin)
|
||||||
|
{
|
||||||
|
onewire_bus_config_t bus_config = {};
|
||||||
|
bus_config.bus_gpio_num = pin;
|
||||||
|
|
||||||
|
onewire_bus_rmt_config_t rmt_config = {};
|
||||||
|
rmt_config.max_rx_bytes = 10;
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(onewire_new_bus_rmt(&bus_config, &rmt_config, &bus));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OneWire bus initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
float ds18b20_read()
|
||||||
|
{
|
||||||
|
uint8_t data[9];
|
||||||
|
uint8_t buf[4] = {0x4E, 0, 0, 0x3F}; // 10-bit (0x3F)
|
||||||
|
// reset
|
||||||
|
ESP_ERROR_CHECK(onewire_bus_reset(bus));
|
||||||
|
|
||||||
|
// SKIP ROM (один датчик)
|
||||||
|
uint8_t cmd = 0xCC;
|
||||||
|
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
|
||||||
|
//ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, buf, 4));
|
||||||
|
// CONVERT T
|
||||||
|
cmd = 0x44;
|
||||||
|
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(750));
|
||||||
|
|
||||||
|
// reset снова
|
||||||
|
ESP_ERROR_CHECK(onewire_bus_reset(bus));
|
||||||
|
|
||||||
|
// SKIP ROM
|
||||||
|
cmd = 0xCC;
|
||||||
|
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
|
||||||
|
|
||||||
|
// READ SCRATCHPAD
|
||||||
|
cmd = 0xBE;
|
||||||
|
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(onewire_bus_read_bytes(bus, data, 9));
|
||||||
|
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"RAW: %02X %02X %02X %02X %02X %02X %02X %02X | CRC %02X",
|
||||||
|
data[0], data[1], data[2], data[3],
|
||||||
|
data[4], data[5], data[6], data[7], data[8]);
|
||||||
|
|
||||||
|
uint8_t crc = ds_crc8(data, 8);
|
||||||
|
if (crc != data[8]) {
|
||||||
|
ESP_LOGW(TAG, "CRC error");
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t raw = (data[1] << 8) | data[0];
|
||||||
|
return raw / 16.0f;
|
||||||
|
}
|
||||||
|
|
||||||
6
esp32/main/ds18b20.h
Normal file
6
esp32/main/ds18b20.h
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
|
||||||
|
void ds18b20_init(gpio_num_t pin);
|
||||||
|
float ds18b20_read();
|
||||||
18
esp32/main/fw_command.cpp
Normal file
18
esp32/main/fw_command.cpp
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//
|
||||||
|
// Created by eugene on 22.04.2026.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include "fw_command.h"
|
||||||
|
#include "json_utils.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
bool parse_fw_command(const char *msg, fw_cmd_t *out) {
|
||||||
|
if (!strstr(msg, "\"t\":\"c\"")) return false;
|
||||||
|
if (!strstr(msg, "\"t\":\"fw\"")) return false;
|
||||||
|
|
||||||
|
if (!json_get_string(msg, "\"u\"", out->url, sizeof(out->url))) return false;
|
||||||
|
if (!json_get_string(msg, "\"s\"", out->sha256, sizeof(out->sha256))) return false;
|
||||||
|
|
||||||
|
out->is_fw = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
9
esp32/main/fw_command.h
Normal file
9
esp32/main/fw_command.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool is_fw;
|
||||||
|
char url[256];
|
||||||
|
char sha256[65]; // 64 + \0
|
||||||
|
} fw_cmd_t;
|
||||||
|
|
||||||
|
bool parse_fw_command(const char *msg, fw_cmd_t *out);
|
||||||
20
esp32/main/gpio_init.cpp
Normal file
20
esp32/main/gpio_init.cpp
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#include "gpio_init.h"
|
||||||
|
#include "hal/gpio_types.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
|
|
||||||
|
#define PIN_OUT GPIO_NUM_22
|
||||||
|
|
||||||
|
void init_gpio()
|
||||||
|
{
|
||||||
|
gpio_config_t io_conf = {
|
||||||
|
.pin_bit_mask = (1ULL << PIN_OUT),
|
||||||
|
.mode = GPIO_MODE_OUTPUT,
|
||||||
|
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||||
|
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||||
|
.intr_type = GPIO_INTR_DISABLE
|
||||||
|
};
|
||||||
|
|
||||||
|
gpio_config(&io_conf);
|
||||||
|
|
||||||
|
gpio_set_level(PIN_OUT, 1); // HIGH ≈ 3.3В
|
||||||
|
}
|
||||||
3
esp32/main/gpio_init.h
Normal file
3
esp32/main/gpio_init.h
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void init_gpio();
|
||||||
48
esp32/main/heartbeat_task.cpp
Normal file
48
esp32/main/heartbeat_task.cpp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#include "heartbeat_task.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include <time.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* TAG = "heartbeat";
|
||||||
|
|
||||||
|
static TaskHandle_t heartbeat_task_handle = nullptr;
|
||||||
|
|
||||||
|
static void heartbeat_task(void* arg)
|
||||||
|
{
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
if (protocol_is_connected())
|
||||||
|
{
|
||||||
|
time_t now;
|
||||||
|
time(&now);
|
||||||
|
|
||||||
|
protocol_send_event_hb((uint32_t)now);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(30000)); // 30 сек
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void heartbeat_task_start()
|
||||||
|
{
|
||||||
|
if (heartbeat_task_handle == nullptr)
|
||||||
|
{
|
||||||
|
xTaskCreate(heartbeat_task, "heartbeat", 4096, nullptr, 5, &heartbeat_task_handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void heartbeat_task_stop()
|
||||||
|
{
|
||||||
|
if (heartbeat_task_handle != nullptr)
|
||||||
|
{
|
||||||
|
vTaskDelete(heartbeat_task_handle);
|
||||||
|
heartbeat_task_handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
esp32/main/heartbeat_task.h
Normal file
4
esp32/main/heartbeat_task.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void heartbeat_task_start(void);
|
||||||
|
void heartbeat_task_stop();
|
||||||
18
esp32/main/idf_component.yml
Normal file
18
esp32/main/idf_component.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
## IDF Component Manager Manifest File
|
||||||
|
dependencies:
|
||||||
|
## Required IDF version
|
||||||
|
idf:
|
||||||
|
version: '>=4.1.0'
|
||||||
|
# # Put list of dependencies here
|
||||||
|
# # For components maintained by Espressif:
|
||||||
|
# component: "~1.0.0"
|
||||||
|
# # For 3rd party components:
|
||||||
|
# username/component: ">=1.0.0,<2.0.0"
|
||||||
|
# username2/component2:
|
||||||
|
# version: "~1.0.0"
|
||||||
|
# # For transient dependencies `public` flag can be set.
|
||||||
|
# # `public` flag doesn't have an effect dependencies of the `main` component.
|
||||||
|
# # All dependencies of `main` are public by default.
|
||||||
|
# public: true
|
||||||
|
espressif/esp_websocket_client: '*'
|
||||||
|
espressif/onewire_bus: '*'
|
||||||
30
esp32/main/json_utils.cpp
Normal file
30
esp32/main/json_utils.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#include <cstring>
|
||||||
|
#include <cstddef>
|
||||||
|
#include "json_utils.h"
|
||||||
|
|
||||||
|
bool json_get_string(const char *json, const char *key, char *out, size_t out_size) {
|
||||||
|
const char *k = strstr(json, key);
|
||||||
|
if (!k) return false;
|
||||||
|
|
||||||
|
const char *start = strchr(k, ':');
|
||||||
|
if (!start) return false;
|
||||||
|
|
||||||
|
start++;
|
||||||
|
|
||||||
|
// найти начало строки (первую кавычку)
|
||||||
|
while (*start && *start != '\"') start++;
|
||||||
|
if (!*start) return false;
|
||||||
|
|
||||||
|
start++; // после "
|
||||||
|
|
||||||
|
const char *end = strchr(start, '\"');
|
||||||
|
if (!end) return false;
|
||||||
|
|
||||||
|
size_t len = end - start;
|
||||||
|
if (len >= out_size) return false;
|
||||||
|
|
||||||
|
memcpy(out, start, len);
|
||||||
|
out[len] = '\0';
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
4
esp32/main/json_utils.h
Normal file
4
esp32/main/json_utils.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
bool json_get_string(const char *json, const char *key, char *out, size_t out_size);
|
||||||
23
esp32/main/main.cpp
Normal file
23
esp32/main/main.cpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#include "esp_log.h"
|
||||||
|
#include "system_init.h"
|
||||||
|
#include "heartbeat_task.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/projdefs.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "sampler_task.h"
|
||||||
|
#include "sender_task.h"
|
||||||
|
|
||||||
|
static const char* TAG = "iot_fish";
|
||||||
|
|
||||||
|
extern "C" void app_main(void)
|
||||||
|
{
|
||||||
|
system_init();
|
||||||
|
|
||||||
|
//sampler_task_start();
|
||||||
|
//sender_task_start();
|
||||||
|
|
||||||
|
heartbeat_task_start();
|
||||||
|
|
||||||
|
system_finalize();
|
||||||
|
}
|
||||||
165
esp32/main/ota.cpp
Normal file
165
esp32/main/ota.cpp
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#include "esp_http_client.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "ota.h"
|
||||||
|
#include "sender_task.h"
|
||||||
|
#include "sampler_task.h"
|
||||||
|
#include "ws.h"
|
||||||
|
#include "heartbeat_task.h"
|
||||||
|
#include "fw_command.h"
|
||||||
|
#include "psa/crypto.h"
|
||||||
|
|
||||||
|
static TaskHandle_t ota_task_handle = nullptr;
|
||||||
|
static char url_copy[256];
|
||||||
|
static char sha256[65];
|
||||||
|
|
||||||
|
#define TAG "OTA"
|
||||||
|
|
||||||
|
void perform_ota(void* par)
|
||||||
|
{
|
||||||
|
sampler_task_stop();
|
||||||
|
sender_task_stop();
|
||||||
|
heartbeat_task_stop();
|
||||||
|
ws_disconnect();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OTA task started");
|
||||||
|
esp_ota_handle_t ota_handle;
|
||||||
|
uint8_t buf[1024];
|
||||||
|
int read_bytes;
|
||||||
|
const esp_partition_t* partition = nullptr;
|
||||||
|
int content_length = 0;
|
||||||
|
psa_hash_operation_t op = PSA_HASH_OPERATION_INIT;
|
||||||
|
|
||||||
|
const char* url = static_cast<const char*>(par);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "URL: %s", url);
|
||||||
|
|
||||||
|
esp_http_client_config_t config = {};
|
||||||
|
config.url = url;
|
||||||
|
config.timeout_ms = 5000;
|
||||||
|
config.buffer_size = 1024;
|
||||||
|
|
||||||
|
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "HTTP init complete");
|
||||||
|
|
||||||
|
|
||||||
|
if (esp_http_client_open(client, 0) != ESP_OK)
|
||||||
|
{
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "HTTP connection established");
|
||||||
|
|
||||||
|
content_length = esp_http_client_fetch_headers(client);
|
||||||
|
ESP_LOGI(TAG, "content_length=%d", content_length);
|
||||||
|
|
||||||
|
partition = esp_ota_get_next_update_partition(nullptr);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OTA update partition selected %s (offset 0x%08x)", partition->label, partition->address);
|
||||||
|
|
||||||
|
if (esp_ota_begin(partition, OTA_SIZE_UNKNOWN, &ota_handle) != ESP_OK)
|
||||||
|
{
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
psa_crypto_init();
|
||||||
|
psa_hash_setup(&op, PSA_ALG_SHA_256);
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "OTA update started");
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
int data_read = esp_http_client_read(client, (char*)buf, sizeof(buf));
|
||||||
|
|
||||||
|
if (data_read < 0)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "read error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (data_read == 0)
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "download finished");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "write chunk: %d bytes", data_read);
|
||||||
|
|
||||||
|
if (esp_ota_write(ota_handle, buf, data_read) != ESP_OK)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "flash write failed");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (psa_hash_update(&op, buf, data_read) != PSA_SUCCESS) {
|
||||||
|
ESP_LOGE(TAG, "hash update failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "finalizing...");
|
||||||
|
|
||||||
|
if (esp_ota_end(ota_handle) == ESP_OK)
|
||||||
|
{
|
||||||
|
uint8_t hash[32];
|
||||||
|
size_t hash_len;
|
||||||
|
|
||||||
|
psa_hash_finish(&op, hash, sizeof(hash), &hash_len);
|
||||||
|
|
||||||
|
// переводим в hex
|
||||||
|
char hash_str[65];
|
||||||
|
for (int i = 0; i < 32; i++)
|
||||||
|
{
|
||||||
|
sprintf(&hash_str[i * 2], "%02x", hash[i]);
|
||||||
|
}
|
||||||
|
hash_str[64] = '\0';
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "sha256 = %s. Comparing...", hash_str);
|
||||||
|
|
||||||
|
// сравнение
|
||||||
|
if (strncmp(hash_str, sha256, 64) != 0)
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "SHA256 mismatch!");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "set boot partition");
|
||||||
|
esp_ota_set_boot_partition(partition);
|
||||||
|
ESP_LOGI(TAG, "restart pending");
|
||||||
|
esp_restart();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ESP_LOGE(TAG, "ota_end failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
esp_http_client_cleanup(client);
|
||||||
|
vTaskDelete(nullptr);
|
||||||
|
ota_task_handle = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ota_task_start(const fw_cmd_t* cmd)
|
||||||
|
{
|
||||||
|
strncpy(url_copy, cmd->url, sizeof(url_copy));
|
||||||
|
strncpy(sha256, cmd->sha256, sizeof(sha256));
|
||||||
|
if (ota_task_handle == nullptr)
|
||||||
|
{
|
||||||
|
xTaskCreate(
|
||||||
|
perform_ota,
|
||||||
|
"ota",
|
||||||
|
4096,
|
||||||
|
url_copy,
|
||||||
|
5,
|
||||||
|
&ota_task_handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ota_task_stop()
|
||||||
|
{
|
||||||
|
if (ota_task_handle != nullptr)
|
||||||
|
{
|
||||||
|
vTaskDelete(ota_task_handle);
|
||||||
|
ota_task_handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
4
esp32/main/ota.h
Normal file
4
esp32/main/ota.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "fw_command.h"
|
||||||
|
|
||||||
|
void ota_task_start(const fw_cmd_t* cmd);
|
||||||
246
esp32/main/protocol.cpp
Normal file
246
esp32/main/protocol.cpp
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
#include "protocol.h"
|
||||||
|
#include "ws.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
|
||||||
|
#define PROTOCOL_VERSION 1
|
||||||
|
|
||||||
|
static uint32_t msg_id = 1;
|
||||||
|
|
||||||
|
static inline int append(char* buf, size_t size, int pos, const char* fmt, ...)
|
||||||
|
{
|
||||||
|
if ((size_t)pos >= size) return -1;
|
||||||
|
|
||||||
|
va_list args;
|
||||||
|
va_start(args, fmt);
|
||||||
|
int written = vsnprintf(buf + pos, size - pos, fmt, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
if (written < 0 || (size_t)(pos + written) >= size)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos + written;
|
||||||
|
}
|
||||||
|
|
||||||
|
int build_telemetry(
|
||||||
|
char* buf,
|
||||||
|
size_t buf_size,
|
||||||
|
int64_t ts,
|
||||||
|
uint32_t device_id,
|
||||||
|
const char* metric,
|
||||||
|
const char* source,
|
||||||
|
const char* unit,
|
||||||
|
const int* values,
|
||||||
|
size_t count
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
// --- header ---
|
||||||
|
pos = append(buf, buf_size, pos,
|
||||||
|
"{\"v\":1,\"t\":\"t\",\"id\":%lu,\"ts\":%lld,\"d\":%lu,\"p\":{",
|
||||||
|
(unsigned long)protocol_next_id(),
|
||||||
|
(long long)ts,
|
||||||
|
(unsigned long)device_id
|
||||||
|
);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
// --- payload meta ---
|
||||||
|
pos = append(buf, buf_size, pos,
|
||||||
|
"\"m\":\"%s\",\"s\":\"%s\",\"u\":\"%s\",\"v\":[",
|
||||||
|
metric,
|
||||||
|
source,
|
||||||
|
unit
|
||||||
|
);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
// --- values ---
|
||||||
|
for (size_t i = 0; i < count ; i ++)
|
||||||
|
{
|
||||||
|
pos = append(buf, buf_size, pos,
|
||||||
|
"[%u,%d]%s",
|
||||||
|
values[i*2 + 0], // delta time
|
||||||
|
values[i*2 + 1],
|
||||||
|
(i < count - 1) ? "," : ""
|
||||||
|
);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- close ---
|
||||||
|
pos = append(buf, buf_size, pos, "]}}");
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((deprecated))
|
||||||
|
// --- TELEMETRY (single measurement) ---
|
||||||
|
int build_telemetry_single(
|
||||||
|
char* buf,
|
||||||
|
size_t buf_size,
|
||||||
|
const char* id,
|
||||||
|
const char* device_id,
|
||||||
|
int64_t ts_ms,
|
||||||
|
const char* metric,
|
||||||
|
double value,
|
||||||
|
const char* unit,
|
||||||
|
const char* source,
|
||||||
|
int64_t m_ts_ms
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
// header
|
||||||
|
pos = append(buf, buf_size, pos,
|
||||||
|
"{\"v\":%d,\"id\":\"%s\",\"type\":\"telemetry\",\"ts\":%lld,"
|
||||||
|
"\"deviceId\":\"%s\",\"payload\":{\"measurements\":[",
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
id,
|
||||||
|
(long long)ts_ms,
|
||||||
|
device_id
|
||||||
|
);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
// measurement start
|
||||||
|
pos = append(buf, buf_size, pos,
|
||||||
|
"{\"metric\":\"%s\",\"value\":%.3f",
|
||||||
|
metric,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if (unit)
|
||||||
|
{
|
||||||
|
pos = append(buf, buf_size, pos, ",\"unit\":\"%s\"", unit);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source)
|
||||||
|
{
|
||||||
|
pos = append(buf, buf_size, pos, ",\"source\":\"%s\"", source);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_ts_ms > 0)
|
||||||
|
{
|
||||||
|
pos = append(buf, buf_size, pos, ",\"ts\":%lld", (long long)m_ts_ms);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// close measurement + payload
|
||||||
|
pos = append(buf, buf_size, pos, "}]}}");
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((deprecated))
|
||||||
|
// --- EVENT ---
|
||||||
|
int build_event(
|
||||||
|
char* buf,
|
||||||
|
size_t buf_size,
|
||||||
|
const char* id,
|
||||||
|
const char* device_id,
|
||||||
|
int64_t ts_ms,
|
||||||
|
const char* name,
|
||||||
|
const char* severity,
|
||||||
|
const char* message
|
||||||
|
)
|
||||||
|
{
|
||||||
|
int pos = 0;
|
||||||
|
|
||||||
|
// header
|
||||||
|
pos = append(buf, buf_size, pos,
|
||||||
|
"{\"v\":%d,\"id\":\"%s\",\"type\":\"event\",\"ts\":%lld,"
|
||||||
|
"\"deviceId\":\"%s\",\"payload\":{",
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
id,
|
||||||
|
(long long)ts_ms,
|
||||||
|
device_id
|
||||||
|
);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
// required
|
||||||
|
pos = append(buf, buf_size, pos, "\"name\":\"%s\"", name);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
// optional
|
||||||
|
if (severity)
|
||||||
|
{
|
||||||
|
pos = append(buf, buf_size, pos, ",\"severity\":\"%s\"", severity);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message)
|
||||||
|
{
|
||||||
|
pos = append(buf, buf_size, pos, ",\"message\":\"%s\"", message);
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// close
|
||||||
|
pos = append(buf, buf_size, pos, "}}");
|
||||||
|
if (pos < 0) return -1;
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t protocol_next_id()
|
||||||
|
{
|
||||||
|
return msg_id++;
|
||||||
|
};
|
||||||
|
|
||||||
|
void protocol_send_event_hb(int64_t ts)
|
||||||
|
{
|
||||||
|
char buf[256];
|
||||||
|
|
||||||
|
wifi_ap_record_t ap;
|
||||||
|
esp_wifi_sta_get_ap_info(&ap);
|
||||||
|
|
||||||
|
int rssi = ap.rssi;
|
||||||
|
const char *ssid = (const char *)ap.ssid;
|
||||||
|
|
||||||
|
esp_netif_ip_info_t ip_info;
|
||||||
|
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
|
||||||
|
|
||||||
|
esp_netif_get_ip_info(netif, &ip_info);
|
||||||
|
|
||||||
|
uint32_t ip = ip_info.ip.addr;
|
||||||
|
|
||||||
|
size_t heap_free = heap_caps_get_free_size(MALLOC_CAP_DEFAULT);
|
||||||
|
size_t heap_largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
|
||||||
|
|
||||||
|
uint32_t id = protocol_next_id();
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf),
|
||||||
|
"{\"v\":1,\"id\":%" PRIu32 ",\"t\":\"e\",\"ts\":%" PRIu64 ",\"d\":%u,"
|
||||||
|
"\"p\":{"
|
||||||
|
"\"t\":\"hb\","
|
||||||
|
"\"v\":%u,"
|
||||||
|
"\"hp\":%u,"
|
||||||
|
"\"hl\":%u,"
|
||||||
|
"\"rs\":%d,"
|
||||||
|
"\"ip\":%" PRIu32 ","
|
||||||
|
"\"si\":\"%s\""
|
||||||
|
"}}",
|
||||||
|
id, ts, CONFIG_DEVICE_ID,
|
||||||
|
FW_VERSION,
|
||||||
|
heap_free,
|
||||||
|
heap_largest,
|
||||||
|
rssi,
|
||||||
|
ip,
|
||||||
|
ssid
|
||||||
|
);
|
||||||
|
|
||||||
|
ws_send(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool protocol_is_connected()
|
||||||
|
{
|
||||||
|
return ws_is_connected();
|
||||||
|
}
|
||||||
56
esp32/main/protocol.h
Normal file
56
esp32/main/protocol.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include "ringbuf.h"
|
||||||
|
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int build_telemetry(
|
||||||
|
char* buf,
|
||||||
|
size_t buf_size,
|
||||||
|
int64_t ts,
|
||||||
|
uint32_t device_id,
|
||||||
|
const char* metric,
|
||||||
|
const char* source,
|
||||||
|
const char* unit,
|
||||||
|
const int* values,
|
||||||
|
size_t count
|
||||||
|
);
|
||||||
|
|
||||||
|
int build_telemetry_single(
|
||||||
|
char* buf,
|
||||||
|
size_t buf_size,
|
||||||
|
const char* id,
|
||||||
|
const char* device_id,
|
||||||
|
int64_t ts_ms,
|
||||||
|
const char* metric,
|
||||||
|
double value,
|
||||||
|
const char* unit, // optional (NULL)
|
||||||
|
const char* source, // optional (NULL)
|
||||||
|
int64_t m_ts_ms // optional (0 = нет)
|
||||||
|
);
|
||||||
|
|
||||||
|
int build_event(
|
||||||
|
char* buf,
|
||||||
|
size_t buf_size,
|
||||||
|
const char* id,
|
||||||
|
const char* device_id,
|
||||||
|
int64_t ts_ms,
|
||||||
|
const char* name,
|
||||||
|
const char* severity, // optional
|
||||||
|
const char* message // optional
|
||||||
|
);
|
||||||
|
|
||||||
|
uint32_t protocol_next_id();
|
||||||
|
|
||||||
|
void protocol_send_event_hb(int64_t ts);
|
||||||
|
|
||||||
|
bool protocol_is_connected();
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
23
esp32/main/ringbuf.cpp
Normal file
23
esp32/main/ringbuf.cpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#include "ringbuf.h"
|
||||||
|
|
||||||
|
void ringbuf_init(ringbuf_t *rb) {
|
||||||
|
rb->head = 0;
|
||||||
|
for (int i = 0; i < RINGBUF_SIZE; i++) {
|
||||||
|
rb->values[i].ts_ms = 0;
|
||||||
|
rb->values[i].value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ringbuf_push(ringbuf_t *rb, uint32_t ts_ms, int v) {
|
||||||
|
rb->values[rb->head].ts_ms = ts_ms;
|
||||||
|
rb->values[rb->head].value = v;
|
||||||
|
rb->head = (rb->head + 1) % RINGBUF_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ringbuf_copy(const ringbuf_t *rb, sample_t *out) {
|
||||||
|
int idx = rb->head;
|
||||||
|
|
||||||
|
for (int i = 0; i < RINGBUF_SIZE; i++) {
|
||||||
|
out[i] = rb->values[(idx + i) % RINGBUF_SIZE];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
esp32/main/ringbuf.h
Normal file
23
esp32/main/ringbuf.h
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define RINGBUF_SIZE 10
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t ts_ms;
|
||||||
|
int value;
|
||||||
|
} sample_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
sample_t values[RINGBUF_SIZE];
|
||||||
|
int head;
|
||||||
|
} ringbuf_t;
|
||||||
|
|
||||||
|
// Инициализация
|
||||||
|
void ringbuf_init(ringbuf_t *rb);
|
||||||
|
|
||||||
|
// Добавить значение
|
||||||
|
void ringbuf_push(ringbuf_t *rb, uint32_t ts_ms, int v);
|
||||||
|
|
||||||
|
void ringbuf_copy(const ringbuf_t *rb, sample_t *out);
|
||||||
62
esp32/main/sampler_task.cpp
Normal file
62
esp32/main/sampler_task.cpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include "sampler_task.h"
|
||||||
|
#include "ringbuf.h"
|
||||||
|
#include "ds18b20.h" // добавим
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
// буфер
|
||||||
|
static ringbuf_t rb;
|
||||||
|
|
||||||
|
ringbuf_t* sampler_get_buffer()
|
||||||
|
{
|
||||||
|
return &rb;
|
||||||
|
}
|
||||||
|
|
||||||
|
static TaskHandle_t sampler_task_handle = nullptr;
|
||||||
|
|
||||||
|
static void sampler_task(void* arg)
|
||||||
|
{
|
||||||
|
const TickType_t period = pdMS_TO_TICKS(6000);
|
||||||
|
TickType_t last_wake = xTaskGetTickCount();
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
int64_t now_ms = esp_timer_get_time() / 1000;
|
||||||
|
|
||||||
|
float temp = ds18b20_read();
|
||||||
|
ringbuf_push(&rb, now_ms, (int)(temp * 100));
|
||||||
|
|
||||||
|
vTaskDelayUntil(&last_wake, period); // 6 секунд
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sampler_task_start()
|
||||||
|
{
|
||||||
|
if (sampler_task_handle == nullptr)
|
||||||
|
{
|
||||||
|
ringbuf_init(&rb);
|
||||||
|
ds18b20_init(GPIO_NUM_27); // новый драйвер
|
||||||
|
|
||||||
|
xTaskCreate(
|
||||||
|
sampler_task,
|
||||||
|
"sampler",
|
||||||
|
4096,
|
||||||
|
nullptr,
|
||||||
|
5,
|
||||||
|
&sampler_task_handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sampler_task_stop()
|
||||||
|
{
|
||||||
|
if (sampler_task_handle != nullptr)
|
||||||
|
{
|
||||||
|
vTaskDelete(sampler_task_handle);
|
||||||
|
sampler_task_handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
esp32/main/sampler_task.h
Normal file
7
esp32/main/sampler_task.h
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ringbuf.h"
|
||||||
|
|
||||||
|
void sampler_task_start();
|
||||||
|
ringbuf_t* sampler_get_buffer();
|
||||||
|
void sampler_task_stop();
|
||||||
113
esp32/main/sender_task.cpp
Normal file
113
esp32/main/sender_task.cpp
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#include "sender_task.h"
|
||||||
|
#include "sampler_task.h"
|
||||||
|
#include "ringbuf.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
|
||||||
|
#include "ws.h"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t last_send_ms = 0;
|
||||||
|
static int64_t last_send_ts = 0;
|
||||||
|
|
||||||
|
static TaskHandle_t sender_task_handle = nullptr;
|
||||||
|
|
||||||
|
static void sender_task(void* arg)
|
||||||
|
{
|
||||||
|
ringbuf_t* rb = sampler_get_buffer();
|
||||||
|
|
||||||
|
last_send_ts = time(nullptr);
|
||||||
|
last_send_ms = esp_timer_get_time() / 1000;
|
||||||
|
|
||||||
|
while (1)
|
||||||
|
{
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(50000)); // 1 мин^-1
|
||||||
|
|
||||||
|
char buf[512];
|
||||||
|
uint32_t now_ms = esp_timer_get_time() / 1000;
|
||||||
|
time_t now_ts = time(nullptr);
|
||||||
|
|
||||||
|
sample_t tmp[RINGBUF_SIZE];
|
||||||
|
ringbuf_copy(rb, tmp);
|
||||||
|
|
||||||
|
int out_values[RINGBUF_SIZE * 2];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < RINGBUF_SIZE; i++)
|
||||||
|
{
|
||||||
|
if (tmp[i].ts_ms >= last_send_ms)
|
||||||
|
{
|
||||||
|
uint32_t delta = tmp[i].ts_ms - last_send_ms;
|
||||||
|
|
||||||
|
// кладём delta + value
|
||||||
|
out_values[count * 2 + 0] = delta;
|
||||||
|
out_values[count * 2 + 1] = tmp[i].value;
|
||||||
|
count++;
|
||||||
|
if (count >= RINGBUF_SIZE) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = build_telemetry(
|
||||||
|
buf,
|
||||||
|
sizeof(buf),
|
||||||
|
last_send_ts,
|
||||||
|
1, // device_id (пока захардкожен)
|
||||||
|
"t", // metric
|
||||||
|
"ds18b20", // source
|
||||||
|
"c_x100", // unit
|
||||||
|
out_values,
|
||||||
|
count
|
||||||
|
);
|
||||||
|
|
||||||
|
if (len > 0)
|
||||||
|
{
|
||||||
|
if (ws_is_connected())
|
||||||
|
{
|
||||||
|
ws_send(buf);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("%s\n", buf); // fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
printf("build_telemetry failed\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
last_send_ms = now_ms;
|
||||||
|
last_send_ts = now_ts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sender_task_start()
|
||||||
|
{
|
||||||
|
if (sender_task_handle == nullptr)
|
||||||
|
{
|
||||||
|
xTaskCreate(
|
||||||
|
sender_task,
|
||||||
|
"sender",
|
||||||
|
4096,
|
||||||
|
nullptr,
|
||||||
|
5,
|
||||||
|
&sender_task_handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sender_task_stop()
|
||||||
|
{
|
||||||
|
if (sender_task_handle != nullptr)
|
||||||
|
{
|
||||||
|
vTaskDelete(sender_task_handle);
|
||||||
|
sender_task_handle = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
esp32/main/sender_task.h
Normal file
5
esp32/main/sender_task.h
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Запуск задачи отправки (1 Гц)
|
||||||
|
void sender_task_start();
|
||||||
|
void sender_task_stop();
|
||||||
30
esp32/main/system_init.cpp
Normal file
30
esp32/main/system_init.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#include "system_init.h"
|
||||||
|
#include "wifi_manager.h"
|
||||||
|
#include "time_sync.h"
|
||||||
|
#include "sdkconfig.h"
|
||||||
|
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "ws.h"
|
||||||
|
#include "gpio_init.h"
|
||||||
|
|
||||||
|
static const char* TAG = "system";
|
||||||
|
|
||||||
|
void system_init()
|
||||||
|
{
|
||||||
|
ESP_LOGI("SYSTEM", "Initializing system...");
|
||||||
|
|
||||||
|
init_gpio();
|
||||||
|
|
||||||
|
init_wifi(CONFIG_WIFI_SSID, CONFIG_WIFI_PASS); // Запуск WiFI
|
||||||
|
|
||||||
|
init_time(); // Установка времени
|
||||||
|
|
||||||
|
ws_go();
|
||||||
|
}
|
||||||
|
|
||||||
|
void system_finalize()
|
||||||
|
{
|
||||||
|
ESP_LOGI(TAG, "Finalizing...");
|
||||||
|
}
|
||||||
4
esp32/main/system_init.h
Normal file
4
esp32/main/system_init.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void system_init();
|
||||||
|
void system_finalize();
|
||||||
28
esp32/main/time_sync.cpp
Normal file
28
esp32/main/time_sync.cpp
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#include "time_sync.h"
|
||||||
|
#include "esp_sntp.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <time.h>
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
void init_time() {
|
||||||
|
ESP_LOGI("TIME", "Initializing SNTP");
|
||||||
|
|
||||||
|
esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
||||||
|
esp_sntp_setservername(0, "pool.ntp.org");
|
||||||
|
esp_sntp_init();
|
||||||
|
|
||||||
|
setenv("TZ", "EET-2EEST,M3.5.0/3,M10.5.0/4", 1);
|
||||||
|
tzset();
|
||||||
|
|
||||||
|
time_t now = 0;
|
||||||
|
struct tm timeinfo{}; // ← ВАЖНО
|
||||||
|
|
||||||
|
int retry = 0;
|
||||||
|
while (timeinfo.tm_year < (2020 - 1900) && retry++ < 10) {
|
||||||
|
ESP_LOGI("TIME", "Waiting for time...");
|
||||||
|
vTaskDelay(2000 / portTICK_PERIOD_MS);
|
||||||
|
time(&now);
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
esp32/main/time_sync.h
Normal file
3
esp32/main/time_sync.h
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void init_time();
|
||||||
72
esp32/main/wifi_manager.cpp
Normal file
72
esp32/main/wifi_manager.cpp
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#include "wifi_manager.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "nvs_flash.h"
|
||||||
|
|
||||||
|
static bool connected = false;
|
||||||
|
|
||||||
|
static void wifi_event_handler(void* arg,
|
||||||
|
esp_event_base_t event_base,
|
||||||
|
int32_t event_id,
|
||||||
|
void* event_data) {
|
||||||
|
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
|
||||||
|
esp_wifi_connect();
|
||||||
|
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||||
|
ESP_LOGI("WIFI", "Disconnected, reconnecting...");
|
||||||
|
connected = false;
|
||||||
|
esp_wifi_connect();
|
||||||
|
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||||
|
connected = true;
|
||||||
|
|
||||||
|
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
|
||||||
|
ESP_LOGI("WIFI", "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* s_ssid = nullptr;
|
||||||
|
static const char* s_pass = nullptr;
|
||||||
|
|
||||||
|
void init_wifi(const char* ssid, const char* pass) {
|
||||||
|
|
||||||
|
esp_err_t ret = nvs_flash_init();
|
||||||
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||||
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||||
|
ret = nvs_flash_init();
|
||||||
|
}
|
||||||
|
ESP_ERROR_CHECK(ret);
|
||||||
|
|
||||||
|
s_ssid = ssid;
|
||||||
|
s_pass = pass;
|
||||||
|
|
||||||
|
esp_netif_init();
|
||||||
|
esp_event_loop_create_default();
|
||||||
|
esp_netif_create_default_wifi_sta();
|
||||||
|
|
||||||
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
|
esp_wifi_init(&cfg);
|
||||||
|
|
||||||
|
esp_event_handler_instance_t instance_any_id;
|
||||||
|
esp_event_handler_instance_t instance_got_ip;
|
||||||
|
|
||||||
|
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
|
||||||
|
&wifi_event_handler, nullptr, &instance_any_id);
|
||||||
|
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
|
||||||
|
&wifi_event_handler, nullptr, &instance_got_ip);
|
||||||
|
|
||||||
|
wifi_config_t wifi_config = {};
|
||||||
|
|
||||||
|
strcpy(reinterpret_cast<char*>(wifi_config.sta.ssid), s_ssid);
|
||||||
|
strcpy(reinterpret_cast<char*>(wifi_config.sta.password), s_pass);
|
||||||
|
|
||||||
|
esp_wifi_set_mode(WIFI_MODE_STA);
|
||||||
|
esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
|
||||||
|
esp_wifi_start();
|
||||||
|
|
||||||
|
while(!connected) {
|
||||||
|
vTaskDelay(500 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifi_connected() {return connected;};
|
||||||
4
esp32/main/wifi_manager.h
Normal file
4
esp32/main/wifi_manager.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/event_groups.h"
|
||||||
|
void init_wifi(const char* ssid, const char* pass);
|
||||||
118
esp32/main/ws.cpp
Normal file
118
esp32/main/ws.cpp
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
#include "ws.h"
|
||||||
|
#include "esp_websocket_client.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include "sdkconfig.h"
|
||||||
|
#include "fw_command.h"
|
||||||
|
#include "ota.h"
|
||||||
|
|
||||||
|
static const char* TAG = "WS";
|
||||||
|
|
||||||
|
static esp_websocket_client_handle_t client = nullptr;
|
||||||
|
static bool connected = false;
|
||||||
|
|
||||||
|
static void ws_event_handler(void* handler_args,
|
||||||
|
esp_event_base_t base,
|
||||||
|
int32_t event_id,
|
||||||
|
void* event_data)
|
||||||
|
{
|
||||||
|
switch (event_id)
|
||||||
|
{
|
||||||
|
case WEBSOCKET_EVENT_CONNECTED:
|
||||||
|
connected = true;
|
||||||
|
ESP_LOGI(TAG, "Connected");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WEBSOCKET_EVENT_DISCONNECTED:
|
||||||
|
connected = false;
|
||||||
|
ESP_LOGI(TAG, "Disconnected");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WEBSOCKET_EVENT_ERROR:
|
||||||
|
connected = false;
|
||||||
|
ESP_LOGE(TAG, "Error");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WEBSOCKET_EVENT_DATA:
|
||||||
|
{
|
||||||
|
auto* data = (esp_websocket_event_data_t*)event_data;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "Recv: %.*s", data->data_len, (char*)data->data_ptr);
|
||||||
|
|
||||||
|
// ⚠️ делаем null-terminated копию
|
||||||
|
char buf[512]; // подбери размер под свой максимум
|
||||||
|
int len = data->data_len;
|
||||||
|
|
||||||
|
if (len >= sizeof(buf)) {
|
||||||
|
ESP_LOGW(TAG, "Message too large");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(buf, data->data_ptr, len);
|
||||||
|
buf[len] = '\0';
|
||||||
|
|
||||||
|
fw_cmd_t cmd {};
|
||||||
|
|
||||||
|
if (parse_fw_command(buf, &cmd)) {
|
||||||
|
ESP_LOGI(TAG, "FW command received");
|
||||||
|
ESP_LOGI(TAG, "URL: %s", cmd.url);
|
||||||
|
ESP_LOGI(TAG, "SHA256: %s", cmd.sha256);
|
||||||
|
|
||||||
|
ota_task_start(&cmd);
|
||||||
|
|
||||||
|
// пока просто лог, без OTA
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_init(const char* uri)
|
||||||
|
{
|
||||||
|
esp_websocket_client_config_t config = {};
|
||||||
|
config.uri = uri;
|
||||||
|
config.reconnect_timeout_ms = 5000;
|
||||||
|
config.network_timeout_ms = 5000;
|
||||||
|
|
||||||
|
client = esp_websocket_client_init(&config);
|
||||||
|
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, ws_event_handler, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_start()
|
||||||
|
{
|
||||||
|
if (client)
|
||||||
|
{
|
||||||
|
esp_websocket_client_start(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ws_is_connected()
|
||||||
|
{
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_send(const char* data)
|
||||||
|
{
|
||||||
|
if (connected && client)
|
||||||
|
{
|
||||||
|
esp_websocket_client_send_text(client, data, strlen(data), portMAX_DELAY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_go()
|
||||||
|
{
|
||||||
|
char uri[128];
|
||||||
|
|
||||||
|
snprintf(uri, sizeof(uri),
|
||||||
|
"ws://%s:%d/ws",
|
||||||
|
CONFIG_SERVER_HOST,
|
||||||
|
CONFIG_SERVER_PORT);
|
||||||
|
ws_init(uri);
|
||||||
|
ws_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ws_disconnect()
|
||||||
|
{
|
||||||
|
esp_websocket_client_stop(client);
|
||||||
|
}
|
||||||
16
esp32/main/ws.h
Normal file
16
esp32/main/ws.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void ws_init(const char* uri);
|
||||||
|
void ws_go();
|
||||||
|
void ws_start();
|
||||||
|
bool ws_is_connected();
|
||||||
|
void ws_send(const char* data);
|
||||||
|
void ws_disconnect();
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
c5a067a9fddea370c478017e66fac302f4b79c3d4027e9bdd42a019786cceb92
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
commitizen:
|
||||||
|
bump_message: 'bump(websocket): $current_version -> $new_version'
|
||||||
|
pre_bump_hooks: python ../../ci/changelog.py esp_websocket_client
|
||||||
|
tag_format: websocket-v$version
|
||||||
|
version: 1.6.1
|
||||||
|
version_files:
|
||||||
|
- idf_component.yml
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [1.6.1](https://github.com/espressif/esp-protocols/commits/websocket-v1.6.1)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix race conditions, memory leak, and data loss ([23ca97d5](https://github.com/espressif/esp-protocols/commit/23ca97d5))
|
||||||
|
- Add state check in abort_connection to prevent double-close
|
||||||
|
- Fix memory leak: free errormsg_buffer on disconnect
|
||||||
|
- Reset connection state on reconnect to prevent stale data
|
||||||
|
- Implement lock ordering for separate TX lock mode
|
||||||
|
- Read buffered data immediately after connection to prevent data loss
|
||||||
|
- Added sdkconfig.ci.tx_lock config
|
||||||
|
|
||||||
|
## [1.6.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.6.0)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add WEBSOCKET_EVENT_HEADER_RECEIVED (#827) ([18f0d028](https://github.com/espressif/esp-protocols/commit/18f0d028), [#715](https://github.com/espressif/esp-protocols/issues/715))
|
||||||
|
- enhance example with docs, pytest setup, and standalone test server - Add comprehensive README with TOC and quick start - Add pytest setup and certificate generation scripts - Add standalone WebSocket test server with TLS support - Add troubleshooting and multiple testing approaches ([cad527d2](https://github.com/espressif/esp-protocols/commit/cad527d2))
|
||||||
|
- Add websocket HTTP redirect ([ce1560ac](https://github.com/espressif/esp-protocols/commit/ce1560ac))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- remove redundant timeout check in client task loop ([1e83bee4](https://github.com/espressif/esp-protocols/commit/1e83bee4))
|
||||||
|
- fix PING timing - enable periodic PING during active traffic ([7f424325](https://github.com/espressif/esp-protocols/commit/7f424325))
|
||||||
|
- Update linux build docs on required libs ([e52a5757](https://github.com/espressif/esp-protocols/commit/e52a5757))
|
||||||
|
- clean up component dependencies - Remove unused 'json', 'nvs_flash', 'esp_stubs', dependency from Linux build configuration - Add cJSON dependency to target example's idf_component.yml ([d665e6f1](https://github.com/espressif/esp-protocols/commit/d665e6f1))
|
||||||
|
- fix relying on asprintf() to NULL strp on failure ([54eb0027](https://github.com/espressif/esp-protocols/commit/54eb0027))
|
||||||
|
- Update Remaining Websocket Echo Server (#893) ([18faeb3d](https://github.com/espressif/esp-protocols/commit/18faeb3d))
|
||||||
|
- avoid long stopping time when waiting to auto-reconnect ([2432e41d](https://github.com/espressif/esp-protocols/commit/2432e41d))
|
||||||
|
- Update Websocket Echo Server ([94bd5b07](https://github.com/espressif/esp-protocols/commit/94bd5b07))
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- ci(common): Update test component dir for IDFv6.0 ([18418c83](https://github.com/espressif/esp-protocols/commit/18418c83))
|
||||||
|
|
||||||
|
## [1.5.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.5.0)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add separate tx lock for send and receive ([250eebf](https://github.com/espressif/esp-protocols/commit/250eebf))
|
||||||
|
- add unregister event to websocket client ([ce16050](https://github.com/espressif/esp-protocols/commit/ce16050))
|
||||||
|
- add ability to reconnect after close ([19891d8](https://github.com/espressif/esp-protocols/commit/19891d8))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- release client-lock during WEBSOCKET_EVENT_DATA ([030cb75](https://github.com/espressif/esp-protocols/commit/030cb75))
|
||||||
|
|
||||||
|
## [1.4.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.4.0)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Support DS peripheral for mutual TLS ([55385ec3](https://github.com/espressif/esp-protocols/commit/55385ec3))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- wait for task on destroy ([42674b49](https://github.com/espressif/esp-protocols/commit/42674b49))
|
||||||
|
- Fix pytest to verify client correctly ([9046af8f](https://github.com/espressif/esp-protocols/commit/9046af8f))
|
||||||
|
- propagate error type ([eeeb9006](https://github.com/espressif/esp-protocols/commit/eeeb9006))
|
||||||
|
- fix example buffer leak ([5219c39d](https://github.com/espressif/esp-protocols/commit/5219c39d))
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- chore(websocket): align structure members ([beb6e57e](https://github.com/espressif/esp-protocols/commit/beb6e57e))
|
||||||
|
- chore(websocket): remove unused client variable ([15d3a01e](https://github.com/espressif/esp-protocols/commit/15d3a01e))
|
||||||
|
|
||||||
|
## [1.3.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.3.0)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- add events for begin/end thread ([d7fa24bc](https://github.com/espressif/esp-protocols/commit/d7fa24bc))
|
||||||
|
- Make example to use certificate bundle ([aecf6f80](https://github.com/espressif/esp-protocols/commit/aecf6f80))
|
||||||
|
- propagate esp_tls stack error and cert verify flags ([234f579b](https://github.com/espressif/esp-protocols/commit/234f579b))
|
||||||
|
- Add option to set and use cert_common_name in Websocket client ([3a6720de](https://github.com/espressif/esp-protocols/commit/3a6720de))
|
||||||
|
- adding support for `if_name` when using WSS transport ([333a6893](https://github.com/espressif/esp-protocols/commit/333a6893))
|
||||||
|
- allow updating reconnect timeout for retry backoffs ([bd9f0627](https://github.com/espressif/esp-protocols/commit/bd9f0627))
|
||||||
|
- allow using external tcp transport handle ([83ea2876](https://github.com/espressif/esp-protocols/commit/83ea2876))
|
||||||
|
- adding support for `keep_alive_enable` when using WSS transport ([c728eae5](https://github.com/espressif/esp-protocols/commit/c728eae5))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Prevent crash on network disconnect during send ([a453ca1f](https://github.com/espressif/esp-protocols/commit/a453ca1f))
|
||||||
|
- use proper interface to delete semaphore ([991ac40d](https://github.com/espressif/esp-protocols/commit/991ac40d))
|
||||||
|
- Move client to different state when disconnecting ([0d8f2a6d](https://github.com/espressif/esp-protocols/commit/0d8f2a6d))
|
||||||
|
- fix of websocket host example ([5ccc018a](https://github.com/espressif/esp-protocols/commit/5ccc018a))
|
||||||
|
- don't get transport from the list if external transport is used ([9d4d5d2d](https://github.com/espressif/esp-protocols/commit/9d4d5d2d))
|
||||||
|
- Fix locking issues of `esp_websocket_client_send_with_exact_opcode` API ([6393fcd7](https://github.com/espressif/esp-protocols/commit/6393fcd7))
|
||||||
|
|
||||||
|
## [1.2.3](https://github.com/espressif/esp-protocols/commits/websocket-v1.2.3)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Expanded example to demonstrate the transfer over TLS ([0d0630ed76](https://github.com/espressif/esp-protocols/commit/0d0630ed76))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- fix esp_event dependency management ([1fb02a9a60](https://github.com/espressif/esp-protocols/commit/1fb02a9a60))
|
||||||
|
- Skip warn on zero timeout and auto reconnect is disabled ([5b467cbf5c](https://github.com/espressif/esp-protocols/commit/5b467cbf5c))
|
||||||
|
- Fixed to use int return value in Tx functions ([9c54b72e1f](https://github.com/espressif/esp-protocols/commit/9c54b72e1f))
|
||||||
|
- Fixed Tx functions with DYNAMIC_BUFFER ([16174470ee](https://github.com/espressif/esp-protocols/commit/16174470ee))
|
||||||
|
- added dependency checks, sdkconfig.defaults and refined README.md ([312982e4aa](https://github.com/espressif/esp-protocols/commit/312982e4aa))
|
||||||
|
- Close websocket and dispatch event if server does not close within a reasonable amount of time. ([d85311880d](https://github.com/espressif/esp-protocols/commit/d85311880d))
|
||||||
|
- Continue waiting for TCP connection to be closed ([2b092e0db4](https://github.com/espressif/esp-protocols/commit/2b092e0db4))
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- docs(websocket): Added README for websocket host example ([2f7c58259d](https://github.com/espressif/esp-protocols/commit/2f7c58259d))
|
||||||
|
|
||||||
|
## [1.2.2](https://github.com/espressif/esp-protocols/commits/websocket-v1.2.2)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- continuation after FIN in websocket client (#460) ([774d1c75e6](https://github.com/espressif/esp-protocols/commit/774d1c75e6))
|
||||||
|
- Re-applie refs to common comps idf_component.yml ([9fe44a4504](https://github.com/espressif/esp-protocols/commit/9fe44a4504))
|
||||||
|
|
||||||
|
## [1.2.1](https://github.com/espressif/esp-protocols/commits/websocket-v1.2.1)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- consider failure if return value of `esp_websocket_client_send_with_exact_opcode` less than 0 ([f523b4d](https://github.com/espressif/esp-protocols/commit/f523b4d))
|
||||||
|
- fix of return value for `esp_websocket_client_send_with_opcode` API ([ba33588](https://github.com/espressif/esp-protocols/commit/ba33588))
|
||||||
|
|
||||||
|
## [1.2.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.2.0)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Added new API `esp_websocket_client_append_header` ([39e9725](https://github.com/espressif/esp-protocols/commit/39e9725))
|
||||||
|
- Added new APIs to support fragmented messages transmission ([fae80e2](https://github.com/espressif/esp-protocols/commit/fae80e2))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Reference common component from IDF ([74fc228](https://github.com/espressif/esp-protocols/commit/74fc228))
|
||||||
|
- Revert referencing protocol_examples_common from IDF ([b176d3a](https://github.com/espressif/esp-protocols/commit/b176d3a))
|
||||||
|
- reference protocol_examples_common from IDF ([025ede1](https://github.com/espressif/esp-protocols/commit/025ede1))
|
||||||
|
- specify override_path in example manifests ([d5e7898](https://github.com/espressif/esp-protocols/commit/d5e7898))
|
||||||
|
- Return status code correctly on esp_websocket_client_send_with_opcode ([ac8f1de](https://github.com/espressif/esp-protocols/commit/ac8f1de))
|
||||||
|
- Fix pytest exclusion, gitignore, and changelog checks ([2696221](https://github.com/espressif/esp-protocols/commit/2696221))
|
||||||
|
|
||||||
|
## [1.1.0](https://github.com/espressif/esp-protocols/commits/websocket-v1.1.0)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Added linux port for websocket ([a22391a](https://github.com/espressif/esp-protocols/commit/a22391a))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- added idf_component.yml for examples ([d273e10](https://github.com/espressif/esp-protocols/commit/d273e10))
|
||||||
|
|
||||||
|
## [1.0.1](https://github.com/espressif/esp-protocols/commits/websocket-v1.0.1)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- esp_websocket_client client allow sending 0 byte packets ([b5177cb](https://github.com/espressif/esp-protocols/commit/b5177cb))
|
||||||
|
- Cleaned up printf/format warnings (-Wno-format) ([e085826](https://github.com/espressif/esp-protocols/commit/e085826))
|
||||||
|
- Added unit tests to CI + minor fix to pass it ([c974c14](https://github.com/espressif/esp-protocols/commit/c974c14))
|
||||||
|
- Reintroduce missing CHANGELOGs ([200cbb3](https://github.com/espressif/esp-protocols/commit/200cbb3), [#235](https://github.com/espressif/esp-protocols/issues/235))
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- docs(common): updated component and example links ([f48d9b2](https://github.com/espressif/esp-protocols/commit/f48d9b2))
|
||||||
|
- docs(common): improving documentation ([ca3fce0](https://github.com/espressif/esp-protocols/commit/ca3fce0))
|
||||||
|
- Fix weird error message spacings ([8bb207e](https://github.com/espressif/esp-protocols/commit/8bb207e))
|
||||||
|
|
||||||
|
## [1.0.0](https://github.com/espressif/esp-protocols/commits/996fef7)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- esp_websocket_client: Updated version to 1.0.0 Updated tests to run agains release-v5.0 ([996fef7](https://github.com/espressif/esp-protocols/commit/996fef7))
|
||||||
|
- esp_websocket_client: * Error handling improved to show status code from server * Added new API `esp_websocket_client_set_headers` * Dispatches 'WEBSOCKET_EVENT_BEFORE_CONNECT' event before tcp connection ([d047ff5](https://github.com/espressif/esp-protocols/commit/d047ff5))
|
||||||
|
- unite all tags under common structure py test: update tags under common structure ([c6db3ea](https://github.com/espressif/esp-protocols/commit/c6db3ea))
|
||||||
|
- websocket: Support HTTP basic authorization ([1b13448](https://github.com/espressif/esp-protocols/commit/1b13448))
|
||||||
|
- Add task_name config option ([1d68884](https://github.com/espressif/esp-protocols/commit/1d68884))
|
||||||
|
- Add websocket error messages ([d68624e](https://github.com/espressif/esp-protocols/commit/d68624e))
|
||||||
|
- websocket: Added new API `esp_websocket_client_destroy_on_exit` ([f9b4790](https://github.com/espressif/esp-protocols/commit/f9b4790))
|
||||||
|
- Added badges with version of components to the respective README files ([e4c8a59](https://github.com/espressif/esp-protocols/commit/e4c8a59))
|
||||||
|
|
||||||
|
|
||||||
|
## [0.0.4](https://github.com/espressif/esp-protocols/commits/3330b96)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- websocket: make `esp_websocket_client_send_with_opcode` a public API ([3330b96](https://github.com/espressif/esp-protocols/commit/3330b96))
|
||||||
|
- websocket: updated example to use local websocket echo server ([55dc564](https://github.com/espressif/esp-protocols/commit/55dc564))
|
||||||
|
- CI: Created a common requirements.txt ([23a537b](https://github.com/espressif/esp-protocols/commit/23a537b))
|
||||||
|
- Examples: using pytest.ini from top level directory ([aee016d](https://github.com/espressif/esp-protocols/commit/aee016d))
|
||||||
|
- CI: fixing the files to be complient with pre-commit hooks ([945bd17](https://github.com/espressif/esp-protocols/commit/945bd17))
|
||||||
|
- websocket: updated example to show json data transfer ([3456781](https://github.com/espressif/esp-protocols/commit/3456781))
|
||||||
|
|
||||||
|
|
||||||
|
## [0.0.3](https://github.com/espressif/esp-protocols/commits/5c245db)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- esp_websocket_client: Upgraded version to 0.0.3 ([5c245db](https://github.com/espressif/esp-protocols/commit/5c245db))
|
||||||
|
- CI: Fix build issues ([6e4e4fa](https://github.com/espressif/esp-protocols/commit/6e4e4fa))
|
||||||
|
|
||||||
|
|
||||||
|
## [0.0.2](https://github.com/espressif/esp-protocols/commits/57afa38)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Optimize memory size for websocket client init ([4cefcd3](https://github.com/espressif/esp-protocols/commit/4cefcd3))
|
||||||
|
- allow users to attach CA bundle ([d56b5d9](https://github.com/espressif/esp-protocols/commit/d56b5d9))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Docs to refer esp-protocols ([91a177e](https://github.com/espressif/esp-protocols/commit/91a177e))
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- Bump asio/mdns/esp_websocket_client versions ([57afa38](https://github.com/espressif/esp-protocols/commit/57afa38))
|
||||||
|
- ignore format warnings ([d66f9dc](https://github.com/espressif/esp-protocols/commit/d66f9dc))
|
||||||
|
- Minor fixes here and there ([8fe2a3a](https://github.com/espressif/esp-protocols/commit/8fe2a3a))
|
||||||
|
- CI: Added CI example run job ([76298ff](https://github.com/espressif/esp-protocols/commit/76298ff))
|
||||||
|
- Implement websocket client connect error ([9e37f53](https://github.com/espressif/esp-protocols/commit/9e37f53))
|
||||||
|
- Add methods to allow get/set of websocket client ping interval ([e55f54b](https://github.com/espressif/esp-protocols/commit/e55f54b))
|
||||||
|
- esp_websocket_client: Expose frame fin flag in websocket event ([b72a9ae](https://github.com/espressif/esp-protocols/commit/b72a9ae))
|
||||||
|
|
||||||
|
|
||||||
|
## [0.0.1](https://github.com/espressif/esp-protocols/commits/80c3cf0)
|
||||||
|
|
||||||
|
### Updated
|
||||||
|
|
||||||
|
- websocket: Initial version based on IDF 5.0 ([80c3cf0](https://github.com/espressif/esp-protocols/commit/80c3cf0))
|
||||||
|
- freertos: Remove legacy data types ([b3c777a](https://github.com/espressif/esp-protocols/commit/b3c777a), [IDF@57fd78f](https://github.com/espressif/esp-idf/commit/57fd78f5baf93a368a82cf4b2e00ca17ffc09115))
|
||||||
|
- websocket: Added configs `reconnect_timeout_ms` and `network_timeout_ms` ([8ce791e](https://github.com/espressif/esp-protocols/commit/8ce791e), [IDF#8263](https://github.com/espressif/esp-idf/issues/8263), [IDF@6c26d65](https://github.com/espressif/esp-idf/commit/6c26d6520311f83c2ebe852a487c36185a429a69))
|
||||||
|
- Add http_parser (new component) dependency ([bece6e7](https://github.com/espressif/esp-protocols/commit/bece6e7), [IDF@8e94cf2](https://github.com/espressif/esp-idf/commit/8e94cf2bb1498e94045e73e649f1046111fc6f9f))
|
||||||
|
- websocket: removed deprecated API "esp_websocket_client_send" ([46bd32d](https://github.com/espressif/esp-protocols/commit/46bd32d), [IDF@7f6ab93](https://github.com/espressif/esp-idf/commit/7f6ab93f7e52bddaf4c030d7337ea5574f33381d))
|
||||||
|
- refactor (test_utils)!: separate file for memory check functions ([525c70c](https://github.com/espressif/esp-protocols/commit/525c70c), [IDF@16514f9](https://github.com/espressif/esp-idf/commit/16514f93f06cd833306459d615458536a9f2e5cd))
|
||||||
|
- Build & config: Remove leftover files from the unsupported "make" build system ([19c0455](https://github.com/espressif/esp-protocols/commit/19c0455), [IDF@766aa57](https://github.com/espressif/esp-idf/commit/766aa5708443099f3f033b739cda0e1de101cca6))
|
||||||
|
- transport: Add CONFI_WS_TRANSPORT for optimize the code size ([9118e0f](https://github.com/espressif/esp-protocols/commit/9118e0f), [IDF@8b02c90](https://github.com/espressif/esp-idf/commit/8b02c9026af32352c8c4ed23025fb42182db6cae))
|
||||||
|
- ws_client: Fix const correctness in the API config structure ([fbdbd55](https://github.com/espressif/esp-protocols/commit/fbdbd55), [IDF@70b1247](https://github.com/espressif/esp-idf/commit/70b1247a47f4583fccd8a91bf6cc532e5741e632))
|
||||||
|
- components: Remove repeated keep alive function by ssl layer function ([de7cd72](https://github.com/espressif/esp-protocols/commit/de7cd72), [IDF@c79a907](https://github.com/espressif/esp-idf/commit/c79a907e4fef0c54175ad5659bc0df45a40745c9))
|
||||||
|
- components: Support bind socket to specified interface in esp_http_client and esp_websocket_client component ([4a608ec](https://github.com/espressif/esp-protocols/commit/4a608ec), [IDF@bead359](https://github.com/espressif/esp-idf/commit/bead3599abd875d746e64cd6749574ff2c155adb))
|
||||||
|
- esp_websocket_client: Don't log the filename when logging "Websocket already stop" ([f0351ff](https://github.com/espressif/esp-protocols/commit/f0351ff), [IDF@10bde42](https://github.com/espressif/esp-idf/commit/10bde42551b479bd4bfccc9d3c6d983f8abe0b87))
|
||||||
|
- websocket: Add websocket unit tests ([9219ff7](https://github.com/espressif/esp-protocols/commit/9219ff7), [IDF@cd01a0c](https://github.com/espressif/esp-idf/commit/cd01a0ca81ef2ba5648fd7712c9bf45bbf252339))
|
||||||
|
- websockets: Set keepalive options after adding transport to the list ([86aa0b8](https://github.com/espressif/esp-protocols/commit/86aa0b8), [IDF@99805d8](https://github.com/espressif/esp-idf/commit/99805d880f41857702b3bbb35bc0dfaf7dec3aec))
|
||||||
|
- websocket: Add configurable ping interval ([1933367](https://github.com/espressif/esp-protocols/commit/1933367), [IDF@9ff9137](https://github.com/espressif/esp-idf/commit/9ff9137e7a8b64e956c1c63e95a48f4049ad571e))
|
||||||
|
- ws_transport: Add option to propagate control packets to the app ([95cf983](https://github.com/espressif/esp-protocols/commit/95cf983), [IDF#6307](https://github.com/espressif/esp-idf/issues/6307), [IDF@acc7bd2](https://github.com/espressif/esp-idf/commit/acc7bd2ca45c21033cbd02220a27c3c1ecdd5ad0))
|
||||||
|
- Add options for esp_http_client and esp_websocket_client to support keepalive ([8a6c320](https://github.com/espressif/esp-protocols/commit/8a6c320), [IDF@b53e46a](https://github.com/espressif/esp-idf/commit/b53e46a68e8671c73e8aafe2602de5ff5a77e3db))
|
||||||
|
- websocket: support mutual tls for websocket Closes https://github.com/espressif/esp-idf/issues/6059 ([d1dd6ec](https://github.com/espressif/esp-protocols/commit/d1dd6ec), [IDF#6059](https://github.com/espressif/esp-idf/issues/6059), [IDF@5ab774f](https://github.com/espressif/esp-idf/commit/5ab774f9d8e119fff56b566fa2f9bdad853bf701))
|
||||||
|
- Whitespace: Automated whitespace fixes (large commit) ([d376480](https://github.com/espressif/esp-protocols/commit/d376480), [IDF@66fb5a2](https://github.com/espressif/esp-idf/commit/66fb5a29bbdc2482d67c52e6f66b303378c9b789))
|
||||||
|
- Websocket client: avoid deadlock if stop called from event handler ([e90272c](https://github.com/espressif/esp-protocols/commit/e90272c), [IDF@c2bb076](https://github.com/espressif/esp-idf/commit/c2bb0762bb5c24cb170bc9c96fdadb86ae2f06e7))
|
||||||
|
- tcp_transport: Added internal API for underlying socket, used for custom select on connection end for WS ([6d12d06](https://github.com/espressif/esp-protocols/commit/6d12d06), [IDF@5e9f8b5](https://github.com/espressif/esp-idf/commit/5e9f8b52e7a87371370205a387b2d94e5ac6cbf9))
|
||||||
|
- ws_client: Added support for close frame, closing connection gracefully ([1455bc0](https://github.com/espressif/esp-protocols/commit/1455bc0), [IDF@b213f2c](https://github.com/espressif/esp-idf/commit/b213f2c6d3d78ba3a95005e3206d4ce370b8a649))
|
||||||
|
- driver, http_client, web_socket, tcp_transport: remove __FILE__ from log messages ([01b4f64](https://github.com/espressif/esp-protocols/commit/01b4f64), [IDF#5637](https://github.com/espressif/esp-idf/issues/5637), [IDF@caaf62b](https://github.com/espressif/esp-idf/commit/caaf62bdad965e6b58bba74171986414057f6757))
|
||||||
|
- websocket_client : fix some issues for websocket client ([6ab0aea](https://github.com/espressif/esp-protocols/commit/6ab0aea), [IDF@341e480](https://github.com/espressif/esp-idf/commit/341e48057349d92c3b8afe5f9c0fcd0aa47500b0))
|
||||||
|
- websocket: add configurable timeout for PONG not received ([b71c49c](https://github.com/espressif/esp-protocols/commit/b71c49c), [IDF@0049385](https://github.com/espressif/esp-idf/commit/0049385850daebfe2222c8f0526b896ffaeacdd9))
|
||||||
|
- websocket client: the client now aborts the connection if send fails. ([f8e3ba7](https://github.com/espressif/esp-protocols/commit/f8e3ba7), [IDF@6bebfc8](https://github.com/espressif/esp-idf/commit/6bebfc84f3ed9c96bcb331fd0d5b0bbb26ce07a4))
|
||||||
|
- ws_client: fix fragmented send setting proper opcodes ([7a5b2d5](https://github.com/espressif/esp-protocols/commit/7a5b2d5), [IDF#4974](https://github.com/espressif/esp-idf/issues/4974), [IDF@14992e6](https://github.com/espressif/esp-idf/commit/14992e62c5573d8b6076281f16b4fe11d6bc8f87))
|
||||||
|
- esp32: add implementation of esp_timer based on TG0 LAC timer ([17281a5](https://github.com/espressif/esp-protocols/commit/17281a5), [IDF@739eb05](https://github.com/espressif/esp-idf/commit/739eb05bb97736b70507e7ebcfee58e670672d23))
|
||||||
|
- tcp_transport/ws_client: websockets now correctly handle messages longer than buffer ([aec6a75](https://github.com/espressif/esp-protocols/commit/aec6a75), [IDF@ffeda30](https://github.com/espressif/esp-idf/commit/ffeda3003c92102d2d5b145c9adb3ea3105cbbda))
|
||||||
|
- websocket: added missing event data ([a6be8e2](https://github.com/espressif/esp-protocols/commit/a6be8e2), [IDF@7c0e376](https://github.com/espressif/esp-idf/commit/7c0e3765ec009acaf2ef439e98895598b5fd9aaf))
|
||||||
|
- Add User-Agent and additional headers to esp_websocket_client ([a48b0fa](https://github.com/espressif/esp-protocols/commit/a48b0fa), [IDF@9200250](https://github.com/espressif/esp-idf/commit/9200250f512146e348f84ebfc76f9e82e2070da2))
|
||||||
|
- ws_client: fix handling timeouts by websocket client. ([1fcc001](https://github.com/espressif/esp-protocols/commit/1fcc001), [IDF#4316](https://github.com/espressif/esp-idf/issues/4316), [IDF@e1f9829](https://github.com/espressif/esp-idf/commit/e1f982921a08022ca4307900fc058ccacccd26d0))
|
||||||
|
- websocket_client: fix locking mechanism in ws-client task and when sending data ([d0121b9](https://github.com/espressif/esp-protocols/commit/d0121b9), [IDF@7c5011f](https://github.com/espressif/esp-idf/commit/7c5011f411b7662feb50fd1e53114bec390d8c2e))
|
||||||
|
- ws_client: fix for not sending ping responses, updated to pass events also for PING and PONG messages, added interfaces to send both binary and text data ([f55d839](https://github.com/espressif/esp-protocols/commit/f55d839), [IDF@abf9345](https://github.com/espressif/esp-idf/commit/abf9345b85559f4a922e8387f48336fb09994041))
|
||||||
|
- websocket_client: fix URI parsing to include also query part in websocket connection path ([f5a26c4](https://github.com/espressif/esp-protocols/commit/f5a26c4), [IDF@271e6c4](https://github.com/espressif/esp-idf/commit/271e6c4c9c57ca6715c1435a71fe3974cd2b18b3))
|
||||||
|
- ws_client: fixed posting to event loop with websocket timeout ([23f6a1d](https://github.com/espressif/esp-protocols/commit/23f6a1d), [IDF@5050506](https://github.com/espressif/esp-idf/commit/50505068c45fbe97611be9b7f2c30b8160cbb9e3))
|
||||||
|
- ws_client: added subprotocol configuration option to websocket client ([2553d65](https://github.com/espressif/esp-protocols/commit/2553d65), [IDF@de6ea39](https://github.com/espressif/esp-idf/commit/de6ea396f17be820153da6acaf977c1bf11806fb))
|
||||||
|
- ws_client: fixed path config issue when ws server configured using host and path instead of uri ([67949f9](https://github.com/espressif/esp-protocols/commit/67949f9), [IDF@c0ba9e1](https://github.com/espressif/esp-idf/commit/c0ba9e19fc6cff79f5760b991c259970bd4abeab))
|
||||||
|
- ws_client: fixed transport config option when server address configured as host, port, transport rather then uri ([bfc88ab](https://github.com/espressif/esp-protocols/commit/bfc88ab), [IDF@adee25d](https://github.com/espressif/esp-idf/commit/adee25d90e100a169e959f94db23621f6ffab0e6))
|
||||||
|
- esp_wifi: wifi support new event mechanism ([4d64495](https://github.com/espressif/esp-protocols/commit/4d64495), [IDF@003a987](https://github.com/espressif/esp-idf/commit/003a9872b7de69d799e9d37521cfbcaff9b37e85))
|
||||||
|
- tools: Mass fixing of empty prototypes (for -Wstrict-prototypes) ([da74a4a](https://github.com/espressif/esp-protocols/commit/da74a4a), [IDF@afbaf74](https://github.com/espressif/esp-idf/commit/afbaf74007e89d016dbade4072bf2e7a3874139a))
|
||||||
|
- ws_client: fix double delete issue in ws client initialization ([f718676](https://github.com/espressif/esp-protocols/commit/f718676), [IDF@9b507c4](https://github.com/espressif/esp-idf/commit/9b507c45c86cf491466d705cd7896c6f6e500d0d))
|
||||||
|
- ws_client: removed dependency on internal tcp_transport header ([13a40d2](https://github.com/espressif/esp-protocols/commit/13a40d2), [IDF@d143356](https://github.com/espressif/esp-idf/commit/d1433564ecfc885f80a7a261a88ab87d227cf1c2))
|
||||||
|
- examples: use new component registration api ([35d6f9a](https://github.com/espressif/esp-protocols/commit/35d6f9a), [IDF@6771eea](https://github.com/espressif/esp-idf/commit/6771eead80534c51efb2033c04769ef5893b4838))
|
||||||
|
- esp_websocket_client: Add websocket client component ([f3a0586](https://github.com/espressif/esp-protocols/commit/f3a0586), [IDF#2829](https://github.com/espressif/esp-idf/issues/2829), [IDF@2a2d932](https://github.com/espressif/esp-idf/commit/2a2d932cfe2404057c71bc91d9d9416200e67a03))
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,21 @@
|
|||||||
|
idf_build_get_property(target IDF_TARGET)
|
||||||
|
|
||||||
|
if(NOT CONFIG_WS_TRANSPORT AND NOT CMAKE_BUILD_EARLY_EXPANSION)
|
||||||
|
message(STATUS "Websocket transport is disabled so the esp_websocket_client component will not be built")
|
||||||
|
# note: the component is still included in the build so it can become visible again in config
|
||||||
|
# without needing to re-run CMake. However no source or header files are built.
|
||||||
|
idf_component_register()
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(${IDF_TARGET} STREQUAL "linux")
|
||||||
|
idf_component_register(SRCS "esp_websocket_client.c"
|
||||||
|
INCLUDE_DIRS "include"
|
||||||
|
REQUIRES esp-tls tcp_transport http_parser esp_event
|
||||||
|
PRIV_REQUIRES esp_timer)
|
||||||
|
else()
|
||||||
|
idf_component_register(SRCS "esp_websocket_client.c"
|
||||||
|
INCLUDE_DIRS "include"
|
||||||
|
REQUIRES lwip esp-tls tcp_transport http_parser esp_event
|
||||||
|
PRIV_REQUIRES esp_timer)
|
||||||
|
endif()
|
||||||
202
esp32/managed_components/espressif__esp_websocket_client/LICENSE
Normal file
202
esp32/managed_components/espressif__esp_websocket_client/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# ESP WEBSOCKET CLIENT
|
||||||
|
|
||||||
|
[](https://components.espressif.com/components/espressif/esp_websocket_client)
|
||||||
|
|
||||||
|
The `esp-websocket_client` component is a managed component for `esp-idf` that contains implementation of [WebSocket protocol client](https://datatracker.ietf.org/doc/html/rfc6455) for ESP32
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Get started with example test [example](https://github.com/espressif/esp-protocols/tree/master/components/esp_websocket_client/examples):
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
* View the full [html documentation](https://docs.espressif.com/projects/esp-protocols/esp_websocket_client/docs/latest/index.html)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.5)
|
||||||
|
|
||||||
|
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
|
set(common_component_dir ../../../../common_components)
|
||||||
|
set(EXTRA_COMPONENT_DIRS
|
||||||
|
../..
|
||||||
|
"${common_component_dir}/linux_compat/esp_timer"
|
||||||
|
"${common_component_dir}/linux_compat/freertos"
|
||||||
|
$ENV{IDF_PATH}/examples/protocols/linux_stubs/esp_stubs
|
||||||
|
$ENV{IDF_PATH}/examples/common_components/protocol_examples_common)
|
||||||
|
|
||||||
|
set(COMPONENTS main)
|
||||||
|
project(websocket)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# ESP Websocket Client - Host Example
|
||||||
|
|
||||||
|
This example demonstrates the ESP websocket client using the `linux` target. It allows for compilation and execution of the example directly within a Linux environment.
|
||||||
|
|
||||||
|
## Compilation and Execution
|
||||||
|
|
||||||
|
To compile and execute this example on Linux need to set target `linux`
|
||||||
|
|
||||||
|
* Debian/Ubuntu: `sudo apt-get install -y libbsd-dev`
|
||||||
|
* Fedora/RHEL: `sudo dnf install libbsd-devel`
|
||||||
|
* Arch: `sudo pacman -S libbsd`
|
||||||
|
* Alpine: `sudo apk add libbsd-dev`
|
||||||
|
|
||||||
|
```
|
||||||
|
idf.py --preview set-target linux
|
||||||
|
idf.py build
|
||||||
|
./build/websocket.elf
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```
|
||||||
|
I (76826192) websocket: [APP] Startup..
|
||||||
|
I (76826193) websocket: [APP] Free memory: 4294967295 bytes
|
||||||
|
I (76826193) websocket: [APP] IDF version: v6.0-dev-2414-gab3feab1d13
|
||||||
|
I (76826195) websocket: Connecting to wss://echo.websocket.org...
|
||||||
|
W (76826195) websocket_client: `reconnect_timeout_ms` is not set, or it is less than or equal to zero, using default time out 10000 (milliseconds)
|
||||||
|
W (76826195) websocket_client: `network_timeout_ms` is not set, or it is less than or equal to zero, using default time out 10000 (milliseconds)
|
||||||
|
I (76826195) websocket: WEBSOCKET_EVENT_BEGIN
|
||||||
|
I (76826196) websocket_client: Started
|
||||||
|
I (76826294) esp-x509-crt-bundle: Certificate validated
|
||||||
|
I (76827230) websocket: WEBSOCKET_EVENT_CONNECTED
|
||||||
|
I (76827239) websocket: WEBSOCKET_EVENT_DATA
|
||||||
|
I (76827239) websocket: Received opcode=1
|
||||||
|
W (76827239) websocket: Received=Request served by 4d896d95b55478
|
||||||
|
W (76827239) websocket: Total payload length=32, data_len=32, current payload offset=0
|
||||||
|
|
||||||
|
I (76828198) websocket: Sending hello 0000
|
||||||
|
I (76828827) websocket: WEBSOCKET_EVENT_DATA
|
||||||
|
I (76828827) websocket: Received opcode=1
|
||||||
|
W (76828827) websocket: Received=hello 0000
|
||||||
|
W (76828827) websocket: Total payload length=10, data_len=10, current payload offset=0
|
||||||
|
|
||||||
|
I (76829207) websocket: Sending fragmented text message
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coverage Reporting
|
||||||
|
For generating a coverage report, it's necessary to enable `CONFIG_GCOV_ENABLED=y` option. Set the following configuration in your project's SDK configuration file (`sdkconfig.ci.coverage`, `sdkconfig.ci.linux` or via `menuconfig`):
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
idf_component_register(SRCS "websocket_linux.c"
|
||||||
|
REQUIRES esp_websocket_client protocol_examples_common esp_netif)
|
||||||
|
|
||||||
|
if(CONFIG_GCOV_ENABLED)
|
||||||
|
target_compile_options(${COMPONENT_LIB} PUBLIC --coverage -fprofile-arcs -ftest-coverage)
|
||||||
|
target_link_options(${COMPONENT_LIB} PUBLIC --coverage -fprofile-arcs -ftest-coverage)
|
||||||
|
|
||||||
|
idf_component_get_property(esp_websocket_client esp_websocket_client COMPONENT_LIB)
|
||||||
|
|
||||||
|
target_compile_options(${esp_websocket_client} PUBLIC --coverage -fprofile-arcs -ftest-coverage)
|
||||||
|
target_link_options(${esp_websocket_client} PUBLIC --coverage -fprofile-arcs -ftest-coverage)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
menu "Host-test config"
|
||||||
|
|
||||||
|
config GCOV_ENABLED
|
||||||
|
bool "Coverage analyzer"
|
||||||
|
default n
|
||||||
|
help
|
||||||
|
Enables coverage analyzing for host tests.
|
||||||
|
|
||||||
|
config WEBSOCKET_URI
|
||||||
|
string "Websocket endpoint URI"
|
||||||
|
default "wss://echo.websocket.org"
|
||||||
|
help
|
||||||
|
URL of websocket endpoint this example connects to and sends echo
|
||||||
|
|
||||||
|
config WS_OVER_TLS_SERVER_AUTH
|
||||||
|
bool "Enable WebSocket over TLS with Server Certificate Verification Only"
|
||||||
|
default y
|
||||||
|
help
|
||||||
|
Enables WebSocket connections over TLS (WSS) with server certificate verification.
|
||||||
|
The client verifies the server certificate; the server does not require a client certificate.
|
||||||
|
|
||||||
|
config WS_OVER_TLS_MUTUAL_AUTH
|
||||||
|
bool "Enable WebSocket over TLS with Server Client Mutual Authentification"
|
||||||
|
default n
|
||||||
|
help
|
||||||
|
Enables WebSocket connections over TLS (WSS) with server and client mutual certificate verification.
|
||||||
|
|
||||||
|
config WS_OVER_TLS_SKIP_COMMON_NAME_CHECK
|
||||||
|
bool "Skip common name(CN) check during TLS authentification"
|
||||||
|
default n
|
||||||
|
help
|
||||||
|
Skip Common Name (CN) check during TLS (WSS) authentication. Use only for testing.
|
||||||
|
|
||||||
|
endmenu
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user