Compare commits
15 Commits
dcf3d30295
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f15fe0f1d | |||
| 90037587c0 | |||
| cb1014c950 | |||
| 4100931deb | |||
| 977227296e | |||
| 732a2dfa32 | |||
| 818a84bf4f | |||
| 98cc0db107 | |||
| fddc086b4e | |||
| 879418b5a9 | |||
| 1681a694d3 | |||
| 5e7f26f128 | |||
| 3772981b8f | |||
| 6f3584fe6d | |||
| b0a48e94bd |
57
.gitignore
vendored
57
.gitignore
vendored
@@ -1,23 +1,46 @@
|
||||
# Kotlin/Java
|
||||
*.class
|
||||
*.jar
|
||||
*.iml
|
||||
/out/
|
||||
# Gradle/Java/Kotlin
|
||||
.gradle/
|
||||
build/
|
||||
out/
|
||||
*.iml
|
||||
|
||||
# ESP32 / PlatformIO / Arduino
|
||||
*.bin
|
||||
*.elf
|
||||
*.pio
|
||||
*.pioenvs/
|
||||
*.idf/
|
||||
|
||||
# Android
|
||||
/.gradle/
|
||||
/build/
|
||||
/local.properties
|
||||
# IDEA
|
||||
.idea/
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# OS
|
||||
.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