From cb1014c9505bea3ea0e90bde4a36a2c573de140b Mon Sep 17 00:00:00 2001 From: eugene-admin Date: Wed, 22 Apr 2026 20:11:55 +0300 Subject: [PATCH] HB, OTA etc --- backend/backend.zip | Bin 0 -> 22223 bytes backend/build.gradle.kts | 46 ++ .../pavloveugene/iot/backend/Application.kt | 44 +- .../pavloveugene/iot/backend/db/Database.kt | 90 ++- .../pavloveugene/iot/backend/dto/EventDto.kt | 8 + .../iot/backend/routes/FirmwareRoutes.kt | 134 +++++ .../iot/backend/routes/ProtocolRoutes.kt | 27 +- .../iot/backend/routes/ProtocolWebSocket.kt | 21 +- .../iot/backend/routes/Routing.kt | 31 -- .../iot/backend/routes/WsRoutes.kt | 4 - .../iot/backend/services/CleanupService.kt | 53 ++ .../iot/backend/services/DeviceConnection.kt | 8 + .../iot/backend/services/DeviceConnections.kt | 18 + .../iot/backend/services/KtorServer.kt | 54 ++ .../iot/backend/services/NormalizeService.kt | 187 +++++++ .../iot/backend/services/ProtocolService.kt | 48 +- contract/ingest.zip | Bin 0 -> 2199 bytes contract/ingest/command.schema.json | 29 + contract/ingest/event.schema.json | 43 +- db/migrations/V2__telemetry_data.sql | 20 + .../V3__telemetry_processed_index.sql | 2 + db/migrations/V4__telemetry_units.sql | 16 + db/migrations/V5__firmware_updates.sql | 24 + esp32/CMakeLists.txt | 4 +- esp32/dependencies.lock | 13 +- esp32/esp32.zip | Bin 0 -> 14532 bytes esp32/main/CMakeLists.txt | 26 +- esp32/main/ds18b20.cpp | 85 +++ esp32/main/ds18b20.h | 6 + esp32/main/fw_command.cpp | 18 + esp32/main/fw_command.h | 9 + esp32/main/gpio_init.cpp | 20 + esp32/main/gpio_init.h | 3 + esp32/main/heartbeat_task.cpp | 31 ++ esp32/main/heartbeat_task.h | 3 + esp32/main/idf_component.yml | 1 + esp32/main/json_utils.cpp | 30 + esp32/main/json_utils.h | 4 + esp32/main/main.cpp | 7 +- esp32/main/protocol.cpp | 120 ++-- esp32/main/protocol.h | 9 +- esp32/main/ringbuf.cpp | 10 +- esp32/main/ringbuf.h | 12 +- esp32/main/sampler_task.cpp | 24 +- esp32/main/sender_task.cpp | 80 ++- esp32/main/system_init.cpp | 3 + esp32/main/ws.cpp | 48 +- .../.build-test-rules.yml | 6 + .../espressif__onewire_bus/.component_hash | 1 + .../espressif__onewire_bus/CHANGELOG.md | 19 + .../espressif__onewire_bus/CHECKSUMS.json | 1 + .../espressif__onewire_bus/CMakeLists.txt | 23 + .../espressif__onewire_bus/LICENSE | 202 +++++++ .../espressif__onewire_bus/README.md | 13 + .../espressif__onewire_bus/idf_component.yml | 10 + .../include/onewire_bus.h | 96 ++++ .../include/onewire_bus_impl_rmt.h | 42 ++ .../include/onewire_bus_impl_uart.h | 42 ++ .../include/onewire_cmd.h | 12 + .../include/onewire_crc.h | 27 + .../include/onewire_device.h | 63 +++ .../include/onewire_types.h | 43 ++ .../interface/onewire_bus_interface.h | 97 ++++ .../src/onewire_bus_api.c | 47 ++ .../src/onewire_bus_impl_rmt.c | 521 ++++++++++++++++++ .../src/onewire_bus_impl_uart.c | 298 ++++++++++ .../espressif__onewire_bus/src/onewire_crc.c | 60 ++ .../src/onewire_device.c | 124 +++++ .../test_apps/CMakeLists.txt | 5 + .../test_apps/main/CMakeLists.txt | 3 + .../test_apps/main/Kconfig.projbuild | 53 ++ .../test_apps/main/idf_component.yml | 4 + .../test_apps/main/onewire_bus_test.c | 80 +++ .../test_apps/pytest_onewire_bus.py | 16 + esp32/partitions.csv | 7 + esp32/version.txt | 1 + 76 files changed, 3157 insertions(+), 232 deletions(-) create mode 100644 backend/backend.zip create mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/dto/EventDto.kt create mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/FirmwareRoutes.kt delete mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/Routing.kt delete mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/WsRoutes.kt create mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/services/CleanupService.kt create mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnection.kt create mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnections.kt create mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/services/KtorServer.kt create mode 100644 backend/src/main/kotlin/org/pavloveugene/iot/backend/services/NormalizeService.kt create mode 100644 contract/ingest.zip create mode 100644 contract/ingest/command.schema.json create mode 100644 db/migrations/V2__telemetry_data.sql create mode 100644 db/migrations/V3__telemetry_processed_index.sql create mode 100644 db/migrations/V4__telemetry_units.sql create mode 100644 db/migrations/V5__firmware_updates.sql create mode 100644 esp32/esp32.zip create mode 100644 esp32/main/ds18b20.cpp create mode 100644 esp32/main/ds18b20.h create mode 100644 esp32/main/fw_command.cpp create mode 100644 esp32/main/fw_command.h create mode 100644 esp32/main/gpio_init.cpp create mode 100644 esp32/main/gpio_init.h create mode 100644 esp32/main/heartbeat_task.cpp create mode 100644 esp32/main/heartbeat_task.h create mode 100644 esp32/main/json_utils.cpp create mode 100644 esp32/main/json_utils.h create mode 100644 esp32/managed_components/espressif__onewire_bus/.build-test-rules.yml create mode 100644 esp32/managed_components/espressif__onewire_bus/.component_hash create mode 100644 esp32/managed_components/espressif__onewire_bus/CHANGELOG.md create mode 100644 esp32/managed_components/espressif__onewire_bus/CHECKSUMS.json create mode 100644 esp32/managed_components/espressif__onewire_bus/CMakeLists.txt create mode 100644 esp32/managed_components/espressif__onewire_bus/LICENSE create mode 100644 esp32/managed_components/espressif__onewire_bus/README.md create mode 100644 esp32/managed_components/espressif__onewire_bus/idf_component.yml create mode 100644 esp32/managed_components/espressif__onewire_bus/include/onewire_bus.h create mode 100644 esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_rmt.h create mode 100644 esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_uart.h create mode 100644 esp32/managed_components/espressif__onewire_bus/include/onewire_cmd.h create mode 100644 esp32/managed_components/espressif__onewire_bus/include/onewire_crc.h create mode 100644 esp32/managed_components/espressif__onewire_bus/include/onewire_device.h create mode 100644 esp32/managed_components/espressif__onewire_bus/include/onewire_types.h create mode 100644 esp32/managed_components/espressif__onewire_bus/interface/onewire_bus_interface.h create mode 100644 esp32/managed_components/espressif__onewire_bus/src/onewire_bus_api.c create mode 100644 esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_rmt.c create mode 100644 esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_uart.c create mode 100644 esp32/managed_components/espressif__onewire_bus/src/onewire_crc.c create mode 100644 esp32/managed_components/espressif__onewire_bus/src/onewire_device.c create mode 100644 esp32/managed_components/espressif__onewire_bus/test_apps/CMakeLists.txt create mode 100644 esp32/managed_components/espressif__onewire_bus/test_apps/main/CMakeLists.txt create mode 100644 esp32/managed_components/espressif__onewire_bus/test_apps/main/Kconfig.projbuild create mode 100644 esp32/managed_components/espressif__onewire_bus/test_apps/main/idf_component.yml create mode 100644 esp32/managed_components/espressif__onewire_bus/test_apps/main/onewire_bus_test.c create mode 100644 esp32/managed_components/espressif__onewire_bus/test_apps/pytest_onewire_bus.py create mode 100644 esp32/partitions.csv create mode 100644 esp32/version.txt diff --git a/backend/backend.zip b/backend/backend.zip new file mode 100644 index 0000000000000000000000000000000000000000..cc3269e7ce840dafe2420ea5c799fc1b96539a82 GIT binary patch literal 22223 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

wzk_vQI}@`Z|bl+p18#%Sx? zf}Teo=KJ>N4A`TYu5Ly?PBqXS-Oo4ss2vnKkD5c|5iR{>EA0grX9vmznyG9_#-nT> zeVAMZ8Rc8(Mo;T*jnOnzGZuXzKd?Y;>Y-?lTfey}VVUlwqM?_5HnI>xDZ_@BC?S!U z^XQf9t67`LgN_b=6(%mf=oVEPe8jF5&LOT8_>)=I*dlWOd0y_e0NP zdbQaYKnuRG##Nkl;o3E_Di1KGlC4fwluX<7E3DU4*viok)b%=9-4zfB%TjM=edVH} zI9gpCRV2j+dA{5*uj_T(kvi`g^VXG|->}+UbIs7o=hkOdqM3FoPuL6{rl>j+j=(@2 z)lt#*$w{Qfd<$bfa#{F{e!QE+iI5c7aT-+eg>S4djECQNZBf@5+YJozk-6RC=g}L_ zaKa~muG3F@7K+Kf2ctFGK_Y@Ca+du`_kl{=h+3`0mMak!uXc$I_9mozqHh7GWBJ4V zxK^kHN$A23Q#*24y1BCTm_8a~hw?t5jsDM*f|LH=l7xb~(Ld%)st?d%n`PR!!o5n{ zJ~%v7`mjE(1XGm5qPIM*wB7?$Z0`W(NBeB3t3-C^nKFQy{C(P zO$$e?SH2qbWq4@Svx++2ZL35nmzl(zZq(z7o?Ik`Xv_FQt@21ZXe3jq=VqPBO-9EG zo2;Z;4AGiQ3D}bMy~tdwLV-%gNkzKIt=vI(_rjOcG^{q;F;TwNjxs#`n(M<63HT3gA}$`pgc zvuA~K3@u`wCdx!%NXhtQe>tYsk=ajfq@b`VlUCoMIDZ^pFz)vTYekwyLCEIahnGrK z=M>vHEuT8hwR-m+xUqA-_FfBF&_eebT8Awt9dqC)6*^=@uJn5i6_tC6Q--}^hQT@f zcrVXJ;q<{wM*{M!;Q7??izi5erxWV$1jNsgF0wCtFCs6HJ_{VaK1e0MSlzGu7$Tqen@q-UfqeC89)u0wLz z6!c`SgF~&i)hz!;FD6nJ<`OoEB4vI;olnOHp?HmM-Sa)gX~iWmw0&lm#F1bjSk(Pb z@oYYDiH?D08SKaP9d;$~!CnbojlK_dxLHE{WprUU2&uNs^zXFVQePy#k1af+>Elv; zR&S)qbvL|q0(MEHZ9GcrUI*MWZ=DzYOXe)+u)zMr^T~XpIA~1--1on)P=X-0f9wV*&t-H2pu_mx}2L!%SMac7NtCbJhgk8Iy;HN5p9_BV? zmuoHW-5;ARp#N=g@=~6aUb&6kynCO>eeA*A4lS0O;<;rNK7&2Xx~9WMv>K7&IVPg8 zqe!*~CZZT?ZK>*#^If76N#=HFR7okjyaMx5I3{LQ!r}E4!7kNXWyOkQi^Jjh&`MFF zz2o{=VyD_t)=y;|OtWa%jIfVZOWG7iI8G|s$+LxK(`i)l6WAc&c+u4fiAydrVn~5Ce_P1genb#omNKLm!XKsblKQRm6>NoN;&w@RBwnyYI zB@XkU>%9k+{Yzrglg=`%FrTkt=mu3K>b%Th_B*1)XCze2MBQchRJBM`N$H;*QL9lc zsS^btxY1#&)13FtAI6U>+Y0eb`QCaL_SvA5?kmU5o%UGV*5(oHt;(j#(z9rtY1)%X zy=_#4nT4m>UUpFp=i`rLzAZIaxgI|;7@mhZw$vM(n=yHUa?slRmGhuu&eraY_1cu1 zLj{d3UEUfotZ>_%u#cqZ$&pzql^?X)%{k zjk<~9eOpi=lGCN_0D;XtwI6}vZ?;pd+WKPTHhR+`V^N?;aQLAvB<8dUwO}9)JQ`M$6oMg@Hd`Nz1xevEF5c6a1 z-Fs?B>kz-SOPWq0f;TFr5RWVbZ(0|@f=g~}qnw%BgUF1_jBe)hgjwlp&%%Udr9W{$ zr^=l*1h1DSxvN`3RI&JIt%FF#MTpVx1^x-19j1NxPxfsJAEAGUXms5fwt7?VXRk=G zfY$9GXxA4)n?a z-R^yHlGr$=utuB`lC>qACEvbuf#}D$VIC9t+&l(%Fw6D^Bo0&2c^B6N;?%{4FBax3 zY@Jo0tteSfy6Stt^=JvLkiE(b;?)QUX4(=ag=Q93GSfLcW- zgRoxG$SY=(fW8XddXQ_h>>PC%<8Dg}-PhqFWDD98k|L=GM|~Bw)B;2kMLdn|!*q(~C!C*y4&vpZTVr=0 zeNQP*9&f9v1K|olaRrjp%d4`Q$Dm&s{~aU)usa(douApn&A{5u%H%s@n(RN$%6XUv ze@BeEM+osL8M^=CS(2fh-t>T6K=~bPDKTo_^_`8bnX`FEYAR32AwO+9F&a|>OZsP{ zk=V*DaA}5Fx$3by3WPmX`LpIv$`)hzXuPI#1>9s#btNrH@`A|=>Q=ueB_?#t=>n&m zfaq6Bft%7c=oiXAQ|h}ifJp&|*#BZ$s?TRZMSRHOyC>M`htoZRZ(9PSCVDm>YesCs zOf}57ASP(9Cb4wKLAA<2AbVqJ3vL=&$15SLC>ClZ(?WzGpi~qdDZ3{0^zmWqg{i{^DI_F=Yb`r)rtFS6i7`#)hiHWlCP54ln@{wfSO@LXLBoKX3&|g zu@!J?@AzHgog1s)H3s_M3_e=*GXGw`Ha+}VkVS;c8E-TA+S1d$an{WQfBcS zVh&WGw8794C9`F;*72I8MK z)G6yNED0iS7GY^(xDPxw#Km>O9HzYkr)EdLQ`gy*>rl(8j7@G_liYs`Z`*G|t>s1S z8dDxOoo!v{5_295bODSik&mb~)hHfzI0eI9ElTNeFDg2ix=3NIF6K$;;q96sdj7X$uw(zVG`~~D>qvIm4P6r`+DVH)ZfA9a}P&zYeheLi6RhzW6LcJ zkKE5(9?_fI<=nq>WD3o3B3H!v=y7C|>4&XVss6&}H$J>bTx&gY9XXwCch$O!`i7fe z{R{$$eYc}&*`?uTSXjY3_i^-u5I{X&eB8}YbnERn z{%64*|063%ibW$)q&RytO%8tIT7kX09(g;8i(|MUTgSUcC$CFN(8dT0k+!;M?dr=r z&PU;1MwM6fjcBU)@-$kYw)ubN$yh#1Pn?0S_m}2EySdyV>GcY3orU1Ayy2iWyMf`W z$G!5`s}T$5TI7%)A1p9tJfbv?mwwscWOe{gL8leIZY60{CUk>2xO4bTA5(EN^=nOC z>DoDL{p0d8;#?tfg`4$CCgROrlZ$qIyO?l`u!$AK48+dtapQD8XNeyPX!AogZR(08 zl_*g;1oJjEd|Sm>Uhdz|#NKiB#2Zl4%zIb=E{HCD&qaG~ATHhcVV~T^cI*KD-4ovS zK-$IGH-4H+Ud!`dp(ImIDOyBTEDr_+1SU*ATnKjA)P1t8=J(mqo`*|qB4-IJ^m@DB zdZ%LHq4QA@ic!_Jk&fhjAIH*1b$a;w#u4&52J45B*`4+L&(madM0_OpWxT9uC~Lz) zXf2M1E)dymq*51nbY120rDh|H5U9r9=Ej91htTUL7%X=nVMe?sPU)=w6tN)oem^NR zgS1#b>yRqQgCcA1IpLRZDkUD)MxXAwS<-;Q1-y-OW-2#2G~C_`V2W!(4_)|!B1m8& zE6PAZVMG7-vk4$C@V%W00o|WHqHsm=-=0zUK@S4rOi1`A#Xo#h;lIfO-srNO2?c?Z z1%J-~c&gz)A6dAf{%_ALTv4C+N&OEWUAQ9qZ_h4VktKorfiC#73Ev&+|N02S74?66 zhT)33)xS{xvqu^Jn*;eTpf~>qhig94@Zao!XBsZsnGi83;scBk94vR(hps}r~v*x0B{0w*{Zy)K}kf{mgaxH2k^hVuK+F!@FD?t?L6=z z!DXxRt^*xJU0arGzfS-z4`3+ZHSIvf{$;E3rUz|;t}V|s>i5AV0cz;MOT>X1`pZ`3 zO%DGTl3X4@{CP$Fr9c2<@X})7J8&;{+0KNtL5tnBpaFI!e^jS_HI0Bub?~z1Uk!KQ zFD3Z%y7fymzE2`hoj!Qs@2^uj5$W&HxK7#ir8K}8faiGw-+^M|%XTJ2jdD$C{wa<5 zzC;3<%>P+>d^I{i@iBPa8IWs!*{Zzb|5}E>C`G=61{f%y=ncGr2q=2HY*pU1Sbq!6 zwM*c@g#wC~zzacso!tI6|5m7Ll`??~1k|5^mv8{}Czq|t`#%2P3Uu9S6mXdUqXsW) z0E$O0Ta`Bh!T(LBt5Sx4@(BNzT87`{0xvB1)zAwF|6j>ymehOqDU$!dm0`hB0b-jG#OF;mJ;K`@JcOcvKvYiQaQeIP# zKh1r;@{fQ3;Mt@=_UmP<^5&xYD*>*P3k!}0cuFZC1dzab+0KNZsIMu^AEmQiK>=_F zPlWu{DD3`%`!&*AFJS-{2O#efJPYX8a8H@``Y>E43-eME0C@0ACgA(8(be3oYfAF- z%IW+1eVKm*^uL`MUP=I90c0V8r}g}5pNdR>C&4vxlE8%l-~hjh1~Q2*Ta`CH^WO<` z>B@k+Ta$m>{{NC!1TGSQ5BQbxuMyv8w!ahUIyd6r(g4@k;MbME#==c^|4y1eztjeo z1sE*wd!k=go?VW=ljU--u5(>_DHUKU;1>(Pci@idvVmM2513Tfxv{$PV!)jh_(cr~ z7z0qu^zRI=af5ZK3@|x?s~_Gz@&2nae|r%G&KN)u{LTWnp1EvQ-g2NE zz<+jqS1A6@_`4hWUoLCF83I=!;3ufyhmTd>%Akaf>t2i~%G?0eS_9W*j1Yb}5D@G_ Hz_) { + + Database.init() runMigrations() - val server = embeddedServer( - Netty, - port = AppConfig.serverPort, - host = AppConfig.serverHost, - ) { - install(ContentNegotiation) { - json() + val mode = args.firstOrNull() + + when (mode) { + "--normalize-data", "normalize-data" -> { + runNormalizeLoop() } - install(WebSockets) { - pingPeriod = Duration.ofSeconds(15) - timeout = Duration.ofSeconds(30) - maxFrameSize = Long.MAX_VALUE - masking = false + "--cleanup", "cleanup" -> { + executeCleanup() } - routing { - protocolRoutes() - protocolWebSocket() + else -> { + startKtorServer() } } - Runtime.getRuntime().addShutdownHook(Thread { - println("Shutting down...") - try { - server.stop(1000, 2000) - } catch (e: Exception) { - println("Shutdown error: ${e.message}") - } - }) - - server.start(wait = true) + exitProcess(0) } diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/db/Database.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/db/Database.kt index 5ae4228..5b93a45 100644 --- a/backend/src/main/kotlin/org/pavloveugene/iot/backend/db/Database.kt +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/db/Database.kt @@ -3,9 +3,13 @@ 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 { @@ -20,9 +24,93 @@ object Database { idleTimeout = 30000 maxLifetime = 1800000 - isAutoCommit = false + isAutoCommit = true } HikariDataSource(config) } + + fun execute(sql: String, params: List = 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 = emptyList()): Map? { + 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() + + for (i in 1..meta.columnCount) { + map[meta.getColumnLabel(i)] = rs.getObject(i) + } + + return map + } + } + } + + fun query(sql: String, params: List = emptyList()): List> { + 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>() + + while (rs.next()) { + val map = mutableMapOf() + + for (i in 1..meta.columnCount) { + map[meta.getColumnLabel(i)] = rs.getObject(i) + } + + result.add(map) + } + + return result + } + } + } + + fun insertAndReturnId(sql: String, params: List = 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") + } + } + } + } } \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/dto/EventDto.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/dto/EventDto.kt new file mode 100644 index 0000000..967275c --- /dev/null +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/dto/EventDto.kt @@ -0,0 +1,8 @@ +package org.pavloveugene.iot.backend.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class EventDto ( + val type: String +) \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/FirmwareRoutes.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/FirmwareRoutes.kt new file mode 100644 index 0000000..eae1ae3 --- /dev/null +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/FirmwareRoutes.kt @@ -0,0 +1,134 @@ +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.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 org.pavloveugene.iot.backend.db.Database +import java.io.File + +fun Route.firmwareRouting() { + uploadFirmware() + getFirmware() + post("/ota_trigger") { + // send WS command + } +} + +fun Route.uploadFirmware() { + post("/firmware_upload") { + 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 || fileBytes == null) { + call.respond(HttpStatusCode.BadRequest, "Missing fields") + return@post + } + + // 👉 генерим имя файла + val filename = "${java.util.UUID.randomUUID()}.bin" + val path = "storage/firmware/$filename" + + // 👉 сохраняем файл + java.io.File(path).apply { + parentFile.mkdirs() + writeBytes(fileBytes!!) + } + + // 👉 считаем sha256 + val sha256 = java.security.MessageDigest + .getInstance("SHA-256") + .digest(fileBytes!!) + .joinToString("") { "%02x".format(it) } + + // 👉 сохраняем в БД + 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) + + try { + Database.execute( + """ + insert into firmware (device_id, version, path, sha256, size) + values (?, ?, ?, ?, ?) + """, listOf( + deviceId, + version, + path, + sha256, + fileBytes.size + ) + ) + + break + } catch (e: Exception) { + println("Retry insert firmware: ${e.message}") + } + } + + call.respond( + mapOf( + "status" to "ok", + "filename" to filename, + "version" to version, + ) + ) + } +} + +fun Route.getFirmware() { + get("/firmware/download") { + 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) + } +} diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolRoutes.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolRoutes.kt index aa2ee0e..cddc218 100644 --- a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolRoutes.kt +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolRoutes.kt @@ -7,29 +7,20 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.http.* import kotlinx.serialization.json.Json -import org.pavloveugene.iot.backend.dto.TelemetryDto -import kotlinx.serialization.json.decodeFromJsonElement 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") { + + route(AppConfig.apiPrefix + "/protocol") { + get("/health") { - call.respond(HttpStatusCode.OK, mapOf("status" to "ok")) - } - - post("/message") { - val json = Json { - ignoreUnknownKeys = false - } - - val protocolService = ProtocolService(json) - - val msg = call.receive() - - protocolService.handleMessage(msg) - - call.respond(HttpStatusCode.Accepted) + call.respond("ok") } } + + } \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolWebSocket.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolWebSocket.kt index 17293ab..c027baf 100644 --- a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolWebSocket.kt +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/ProtocolWebSocket.kt @@ -4,20 +4,12 @@ import MessageDto import io.ktor.server.websocket.* import io.ktor.server.routing.* import io.ktor.websocket.* -import io.ktor.server.application.* -import io.ktor.server.response.* import io.ktor.utils.io.CancellationException import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.encodeToJsonElement import org.pavloveugene.iot.backend.config.AppConfig -import org.pavloveugene.iot.backend.dto.ProtocolMessage -import org.pavloveugene.iot.backend.dto.TelemetryDto +import org.pavloveugene.iot.backend.services.DeviceConnections import org.pavloveugene.iot.backend.services.ProtocolService import java.io.IOException -import java.time.Duration fun Route.protocolWebSocket() { val json = Json { @@ -27,8 +19,11 @@ fun Route.protocolWebSocket() { val protocolService = ProtocolService(json) webSocket(AppConfig.wsPath) { + println("WS connected") + var devId: UInt? = null + try { for (frame in incoming) { if (frame is Frame.Text) { @@ -43,7 +38,8 @@ fun Route.protocolWebSocket() { } try { - protocolService.handleMessage(msg) + devId = msg.d + protocolService.handleMessage(msg, this) } catch (e: Exception) { println("WS handler error: ${e.message}") } @@ -56,7 +52,10 @@ fun Route.protocolWebSocket() { } catch (e: Exception) { println("WS error: ${e.message}") } finally { - println("WS disconnected") + devId?.let { + DeviceConnections.unregister(devId) + println("WS disconnected: $it") + } } } } diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/Routing.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/Routing.kt deleted file mode 100644 index c3c08ba..0000000 --- a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/Routing.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.pavloveugene.iot.backend.routes - -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import org.pavloveugene.iot.backend.db.Database - -fun Application.configureRouting() { - routing { - get("/api/v1/health") { - try { - Database.dataSource.connection.use { conn -> - conn.createStatement().execute("SELECT 1") - } - call.respondText("OK") - } catch (e: Exception) { - call.respond(HttpStatusCode.InternalServerError, "DB ERROR") - } - } - get("/api/v1/ready") { - // например: - // - миграции применены - // - WS сервис готов - call.respondText("READY") - } - get("/live") { - call.respondText("ALIVE") - } - } -} \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/WsRoutes.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/WsRoutes.kt deleted file mode 100644 index a604e4f..0000000 --- a/backend/src/main/kotlin/org/pavloveugene/iot/backend/routes/WsRoutes.kt +++ /dev/null @@ -1,4 +0,0 @@ -package org.pavloveugene.iot.backend.routes - -class WsRoutes { -} \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/CleanupService.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/CleanupService.kt new file mode 100644 index 0000000..c7387b4 --- /dev/null +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/CleanupService.kt @@ -0,0 +1,53 @@ +package org.pavloveugene.iot.backend.services + +import org.pavloveugene.iot.backend.db.Database +import java.sql.SQLException + +fun executeCleanup() { + val ds = Database.dataSource + val start = System.currentTimeMillis() + val cutoff = start / 1000 - 60 * 60 * 24 * 2 + + println("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) { + println("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 + } + } + + println("Cleanup complete in ${System.currentTimeMillis() - start} ms") +} \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnection.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnection.kt new file mode 100644 index 0000000..c3cfa6b --- /dev/null +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnection.kt @@ -0,0 +1,8 @@ +package org.pavloveugene.iot.backend.services + +import io.ktor.websocket.WebSocketSession + +data class DeviceConnection( + val session: WebSocketSession, + var lastSeen: Long +) \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnections.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnections.kt new file mode 100644 index 0000000..f7912eb --- /dev/null +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/DeviceConnections.kt @@ -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() + + fun register(deviceId: UInt, connection: DeviceConnection) { + map[deviceId] = connection + } + + fun unregister(deviceId: UInt) { + map.remove(deviceId) + } + + fun get(deviceId: UInt): DeviceConnection? = map[deviceId] +} \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/KtorServer.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/KtorServer.kt new file mode 100644 index 0000000..37ad005 --- /dev/null +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/KtorServer.kt @@ -0,0 +1,54 @@ +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 + +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 { + protocolRoutes() + protocolWebSocket() + firmwareRouting() + } + } + + Runtime.getRuntime().addShutdownHook(Thread { + println("Shutting down...") + try { + server.stop(1000, 2000) + } catch (e: Exception) { + println("Shutdown error: ${e.message}") + } + }) + + server.start(wait = true) +} \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/NormalizeService.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/NormalizeService.kt new file mode 100644 index 0000000..b5cdd25 --- /dev/null +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/NormalizeService.kt @@ -0,0 +1,187 @@ +package org.pavloveugene.iot.backend.services + +import org.pavloveugene.iot.backend.db.Database + +fun runNormalizeLoop() { + var total = 0 + val start = System.currentTimeMillis() + verifyData() + do { + val count = normalizeBatch() + total += count + println("Processed batch: $count") + } while (count > 0) + + println("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; + + println("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) { + println("All ok!") + } else { + println("$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 + } + } + + println("Verification complete") + + return ret +} \ No newline at end of file diff --git a/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/ProtocolService.kt b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/ProtocolService.kt index 64c9e2c..2fb7601 100644 --- a/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/ProtocolService.kt +++ b/backend/src/main/kotlin/org/pavloveugene/iot/backend/services/ProtocolService.kt @@ -1,17 +1,20 @@ package org.pavloveugene.iot.backend.services import MessageDto +import io.ktor.server.websocket.WebSocketServerSession +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 class ProtocolService( private val json: Json ) { - fun handleMessage(msg: MessageDto) { + fun handleMessage(msg: MessageDto, session: WebSocketSession) { when (msg.t) { MessageType.TELEMETRY -> { handleTelemetry(msg) @@ -20,6 +23,7 @@ class ProtocolService( MessageType.EVENT -> { println("=== EVENT ===") println(msg.p) + handleEvent(msg, session) } MessageType.COMMAND -> { @@ -29,6 +33,27 @@ class ProtocolService( } } + private fun handleEvent(msg: MessageDto, session: WebSocketSession) { + val payload = json.decodeFromJsonElement(EventDto.serializer(), msg.p) + when (payload.type) { + "HB" -> { + println("=== HB devId = ${msg.d} ===") + + val connection = DeviceConnections.get(msg.d) + if (connection == null) { + DeviceConnections.register( + msg.d, DeviceConnection( + session = session, + lastSeen = System.currentTimeMillis() + ) + ) + } else { + connection.lastSeen = System.currentTimeMillis() + } + } + } + } + fun handleTelemetry(msg: MessageDto) { val ds = Database.dataSource @@ -37,19 +62,23 @@ class ProtocolService( try { // ensure device - conn.prepareStatement(""" + conn.prepareStatement( + """ INSERT INTO devices(id) VALUES (?) ON DUPLICATE KEY UPDATE id = id - """).use { + """ + ).use { it.setLong(1, msg.d.toLong()) it.executeUpdate() } // check enabled - val isEnabled = conn.prepareStatement(""" + val isEnabled = conn.prepareStatement( + """ SELECT is_enabled FROM devices WHERE id = ? - """).use { + """ + ).use { it.setLong(1, msg.d.toLong()) val rs = it.executeQuery() if (rs.next()) rs.getBoolean(1) else false @@ -70,18 +99,21 @@ class ProtocolService( println("values=${payload.v}") // insert telemetry - conn.prepareStatement(""" + conn.prepareStatement( + """ INSERT INTO telemetry( device_id, ts, metric, source, unit, payload ) VALUES (?, ?, ?, ?, ?, ?) - """).use { + """ + ).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) + val payloadJson = + json.encodeToString(ListSerializer(ListSerializer(Double.serializer())), payload.v) it.setString(6, payloadJson) it.executeUpdate() diff --git a/contract/ingest.zip b/contract/ingest.zip new file mode 100644 index 0000000000000000000000000000000000000000..84f0502a6693890ab3138b56a2227c10a7c4a5bb GIT binary patch literal 2199 zcmWIWW@Zs#0D%vk(_+93D8bI4z>t}jo?2X@9~#2Tz|MN(MJ5cFR&X;gvVatVi2$%6 z91I)`nVWmyhJ0aWV30u=Qj(gJnwwfuRH;{-oRONFsFziop9eNs?9=T`p{p-4K{T4l zYbQH;A2ASUeg9j#?So66Iht-i-h7k#~S&fNH^&1Flv?zxA4lX%~< zb>gI$UPrHkV!B-GpWkkoV`h2Vf zUnb-^vlX$;d9Y_*^Bs;`E?0ykxij)+WoUlgVHa9^EUhEx;r*(LPZN2WLSM8@YvEC6 zjQtQZ%gws|Eu-Z>>7O?z=B&@{=p6-#fideCc3YY?aw(ATvAt1^ub~KjV?CD?w_-l++WDGd_zip;zH&o z7Sm1nUHherF6!<3{yA5p)Z=jYL9gwC>K%Xom70Zp+BGwGA47mQJBM1<6qZ^>1_n@M zBBHr=BO;n3*cccj5z(BSpPQSXhY`nh?`~(Z1LGJ(qs8&j5MTet1_HK|;}co=)?883 zNpf^uA=GqQE^Ifug(6>E`X#BOeH)jHu6R{{|08?uWZtH`%u6P{wmE;i+W7Grqhp_# z)t@b>s5|$`a*?I}Y4+={3SGLdojY~vlhkqd^wcBYJ_ml7JoUTaTDQ}z(}K<)NYsp4 z8WpQjF0VD~@n**S`~T_~8U;6h*fBw@X3p-Knf)hD>5JURpBsPp+LP}G=Y>yoxtVqE z&!)AE-9j=yuk9%m;5nnWN2z7Q)3n`(ow8aaZ#XE+OyFrR`DDuQy5qUyqub}avQ~=-BVLHjx zcW*Z@Tv)wVdIDRD^}R3Wu6^--cujhq1^e7DUzBFQUBPpI!}Q5BULJ6g724b5`#e}c zWZ9jY@9L92O+TkNGk6G3T%jrD4u z>Rp@!9|aseugsTcUNh&}_Uoy!FWBx|GpzmkDEYwchn30g+kI9)+ms*yZSAr32ao*1|If1zmM56_pZ?7R=ivbTyb)4YNXRLdg z{<#QtSoSDz_Gt>7y3e_+;n=0Vy^l8=u|4VF=xw}k-h$(bdGejIq>|SE^P2rCp9oBB zHJExf^2F`>OUI|Tocw=vxy;{vtMXqh44!fN%&V>ECI);c%iJ&Twp(S*CsvS;dW*lE z$pN|<S)(%1y7ouQ!wvWCPAglrEeg(1fjD22hm elE&SvaC=BjZvoz{z(Rn5ffWe9FfuUof_VUil294| literal 0 HcmV?d00001 diff --git a/contract/ingest/command.schema.json b/contract/ingest/command.schema.json new file mode 100644 index 0000000..ff050e5 --- /dev/null +++ b/contract/ingest/command.schema.json @@ -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}$" } + } + } + } + ] +} \ No newline at end of file diff --git a/contract/ingest/event.schema.json b/contract/ingest/event.schema.json index 4b1d2ef..a2712ce 100644 --- a/contract/ingest/event.schema.json +++ b/contract/ingest/event.schema.json @@ -1,38 +1,11 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "contract/ingest/event.schema.json", - "allOf": [ - { "$ref": "contract/ingest/common.schema.json" }, - { - "type": "object", - "properties": { - "type": { - "const": "event" - }, - "payload": { - "type": "object", - "required": ["name"], - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "minLength": 1, - "description": "Event identifier, e.g. device.overheat" - }, - "severity": { - "type": "string", - "enum": ["debug", "info", "warn", "error", "fatal"] - }, - "message": { - "type": "string" - }, - "data": { - "type": "object", - "additionalProperties": true - } - } - } - } + "type": "object", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "enum": ["hb"] } - ] + }, + "additionalProperties": false } \ No newline at end of file diff --git a/db/migrations/V2__telemetry_data.sql b/db/migrations/V2__telemetry_data.sql new file mode 100644 index 0000000..ac565bd --- /dev/null +++ b/db/migrations/V2__telemetry_data.sql @@ -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); \ No newline at end of file diff --git a/db/migrations/V3__telemetry_processed_index.sql b/db/migrations/V3__telemetry_processed_index.sql new file mode 100644 index 0000000..059ad6e --- /dev/null +++ b/db/migrations/V3__telemetry_processed_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX idx_telemetry_processed_id +ON telemetry (processed, id); \ No newline at end of file diff --git a/db/migrations/V4__telemetry_units.sql b/db/migrations/V4__telemetry_units.sql new file mode 100644 index 0000000..3336dc4 --- /dev/null +++ b/db/migrations/V4__telemetry_units.sql @@ -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); \ No newline at end of file diff --git a/db/migrations/V5__firmware_updates.sql b/db/migrations/V5__firmware_updates.sql new file mode 100644 index 0000000..2f74913 --- /dev/null +++ b/db/migrations/V5__firmware_updates.sql @@ -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) +); \ No newline at end of file diff --git a/esp32/CMakeLists.txt b/esp32/CMakeLists.txt index 5cc9441..67c0a87 100644 --- a/esp32/CMakeLists.txt +++ b/esp32/CMakeLists.txt @@ -2,6 +2,8 @@ # 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) + include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(esp32) - diff --git a/esp32/dependencies.lock b/esp32/dependencies.lock index 22d156e..e167a3f 100644 --- a/esp32/dependencies.lock +++ b/esp32/dependencies.lock @@ -9,13 +9,24 @@ dependencies: 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: 68800db522a823ce0f8709ffb9c800760892c2d5889621f1cd9804dc45ae76f9 +manifest_hash: 13e7ecd84ccc03ee20f4d74c327e5e03dc83702693f2188053e8b384ceb1adac target: esp32 version: 3.0.0 diff --git a/esp32/esp32.zip b/esp32/esp32.zip new file mode 100644 index 0000000000000000000000000000000000000000..68b6eaaad8a37583edde7edccc6f8026add798e0 GIT binary patch literal 14532 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 Uy9By5fqpfE&h0(We};kif4mrVN&o-= literal 0 HcmV?d00001 diff --git a/esp32/main/CMakeLists.txt b/esp32/main/CMakeLists.txt index cb69c46..2f499ae 100644 --- a/esp32/main/CMakeLists.txt +++ b/esp32/main/CMakeLists.txt @@ -1,8 +1,30 @@ 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" + 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" INCLUDE_DIRS "." - REQUIRES nvs_flash esp_wifi esp_netif freertos log cjson esp_timer esp_adc esp_websocket_client + REQUIRES nvs_flash + esp_wifi + esp_netif + freertos + log + esp_timer + esp_adc + esp_websocket_client + driver + esp_driver_gpio ) # добавляем кастомный Kconfig set(COMPONENT_KCONFIG "Kconfig") \ No newline at end of file diff --git a/esp32/main/ds18b20.cpp b/esp32/main/ds18b20.cpp new file mode 100644 index 0000000..8574e05 --- /dev/null +++ b/esp32/main/ds18b20.cpp @@ -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; +} + diff --git a/esp32/main/ds18b20.h b/esp32/main/ds18b20.h new file mode 100644 index 0000000..7e1d8ae --- /dev/null +++ b/esp32/main/ds18b20.h @@ -0,0 +1,6 @@ +#pragma once + +#include "driver/gpio.h" + +void ds18b20_init(gpio_num_t pin); +float ds18b20_read(); \ No newline at end of file diff --git a/esp32/main/fw_command.cpp b/esp32/main/fw_command.cpp new file mode 100644 index 0000000..6328cab --- /dev/null +++ b/esp32/main/fw_command.cpp @@ -0,0 +1,18 @@ +// +// Created by eugene on 22.04.2026. +// + +#include "fw_command.h" +#include "json_utils.h" +#include + +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; +} \ No newline at end of file diff --git a/esp32/main/fw_command.h b/esp32/main/fw_command.h new file mode 100644 index 0000000..d912bb4 --- /dev/null +++ b/esp32/main/fw_command.h @@ -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); \ No newline at end of file diff --git a/esp32/main/gpio_init.cpp b/esp32/main/gpio_init.cpp new file mode 100644 index 0000000..9d46928 --- /dev/null +++ b/esp32/main/gpio_init.cpp @@ -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В +} \ No newline at end of file diff --git a/esp32/main/gpio_init.h b/esp32/main/gpio_init.h new file mode 100644 index 0000000..2f06718 --- /dev/null +++ b/esp32/main/gpio_init.h @@ -0,0 +1,3 @@ +#pragma once + +void init_gpio(); \ No newline at end of file diff --git a/esp32/main/heartbeat_task.cpp b/esp32/main/heartbeat_task.cpp new file mode 100644 index 0000000..5f85676 --- /dev/null +++ b/esp32/main/heartbeat_task.cpp @@ -0,0 +1,31 @@ +#include "heartbeat_task.h" + +extern "C" { +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "protocol.h" +#include +} + +static const char *TAG = "heartbeat"; + +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(10000)); // 10 сек + } +} + +void heartbeat_task_start() +{ + xTaskCreate(heartbeat_task, "heartbeat", 4096, NULL, 5, NULL); +} \ No newline at end of file diff --git a/esp32/main/heartbeat_task.h b/esp32/main/heartbeat_task.h new file mode 100644 index 0000000..b848848 --- /dev/null +++ b/esp32/main/heartbeat_task.h @@ -0,0 +1,3 @@ +#pragma once + +void heartbeat_task_start(void); \ No newline at end of file diff --git a/esp32/main/idf_component.yml b/esp32/main/idf_component.yml index 54f9049..fd486a3 100644 --- a/esp32/main/idf_component.yml +++ b/esp32/main/idf_component.yml @@ -15,3 +15,4 @@ dependencies: # # All dependencies of `main` are public by default. # public: true espressif/esp_websocket_client: '*' + espressif/onewire_bus: '*' \ No newline at end of file diff --git a/esp32/main/json_utils.cpp b/esp32/main/json_utils.cpp new file mode 100644 index 0000000..14cd8fb --- /dev/null +++ b/esp32/main/json_utils.cpp @@ -0,0 +1,30 @@ +#include +#include +#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; +} \ No newline at end of file diff --git a/esp32/main/json_utils.h b/esp32/main/json_utils.h new file mode 100644 index 0000000..2b72623 --- /dev/null +++ b/esp32/main/json_utils.h @@ -0,0 +1,4 @@ +#pragma once +#include + +bool json_get_string(const char *json, const char *key, char *out, size_t out_size); \ No newline at end of file diff --git a/esp32/main/main.cpp b/esp32/main/main.cpp index 1450cdf..a5f1589 100644 --- a/esp32/main/main.cpp +++ b/esp32/main/main.cpp @@ -1,5 +1,6 @@ #include "esp_log.h" #include "system_init.h" +#include "heartbeat_task.h" #include "freertos/FreeRTOS.h" #include "freertos/projdefs.h" #include "freertos/task.h" @@ -13,8 +14,10 @@ extern "C" void app_main(void) { system_init(); - sampler_task_start(); - sender_task_start(); + //sampler_task_start(); + //sender_task_start(); + + heartbeat_task_start(); system_finalize(); } \ No newline at end of file diff --git a/esp32/main/protocol.cpp b/esp32/main/protocol.cpp index 47b9805..ed843a5 100644 --- a/esp32/main/protocol.cpp +++ b/esp32/main/protocol.cpp @@ -1,10 +1,15 @@ #include "protocol.h" +#include "ws.h" #include #include +#include #define PROTOCOL_VERSION 1 -static inline int append(char* buf, size_t size, int pos, const char* fmt, ...) { +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; @@ -12,7 +17,8 @@ static inline int append(char* buf, size_t size, int pos, const char* fmt, ...) int written = vsnprintf(buf + pos, size - pos, fmt, args); va_end(args); - if (written < 0 || (size_t)(pos + written) >= size) { + if (written < 0 || (size_t)(pos + written) >= size) + { return -1; } @@ -22,7 +28,6 @@ static inline int append(char* buf, size_t size, int pos, const char* fmt, ...) int build_telemetry( char* buf, size_t buf_size, - uint32_t msg_id, int64_t ts, uint32_t device_id, const char* metric, @@ -30,34 +35,36 @@ int build_telemetry( 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)msg_id, - (long long)ts, - (unsigned long)device_id + "{\"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 + "\"m\":\"%s\",\"s\":\"%s\",\"u\":\"%s\",\"v\":[", + metric, + source, + unit ); if (pos < 0) return -1; // --- values --- - for (size_t i = 0; i < count; i++) { + for (size_t i = 0; i < count ; i ++) + { pos = append(buf, buf_size, pos, - "[%u,%d]%s", - (unsigned)i, // delta time (пока просто индекс) - values[i], - (i < count - 1) ? "," : "" + "[%u,%d]%s", + values[i*2 + 0], // delta time + values[i*2 + 1], + (i < count - 1) ? "," : "" ); if (pos < 0) return -1; } @@ -71,7 +78,7 @@ int build_telemetry( __attribute__((deprecated)) // --- TELEMETRY (single measurement) --- - int build_telemetry_single( +int build_telemetry_single( char* buf, size_t buf_size, const char* id, @@ -82,40 +89,44 @@ __attribute__((deprecated)) 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 + "{\"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 + "{\"metric\":\"%s\",\"value\":%.3f", + metric, + value ); if (pos < 0) return -1; // optional - if (unit) { + if (unit) + { pos = append(buf, buf_size, pos, ",\"unit\":\"%s\"", unit); if (pos < 0) return -1; } - if (source) { + if (source) + { pos = append(buf, buf_size, pos, ",\"source\":\"%s\"", source); if (pos < 0) return -1; } - if (m_ts_ms > 0) { + if (m_ts_ms > 0) + { pos = append(buf, buf_size, pos, ",\"ts\":%lld", (long long)m_ts_ms); if (pos < 0) return -1; } @@ -138,17 +149,18 @@ int build_event( 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 + "{\"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; @@ -157,12 +169,14 @@ int build_event( if (pos < 0) return -1; // optional - if (severity) { + if (severity) + { pos = append(buf, buf_size, pos, ",\"severity\":\"%s\"", severity); if (pos < 0) return -1; } - if (message) { + if (message) + { pos = append(buf, buf_size, pos, ",\"message\":\"%s\"", message); if (pos < 0) return -1; } @@ -172,4 +186,30 @@ int build_event( if (pos < 0) return -1; return pos; +} + +uint32_t protocol_next_id() +{ + return msg_id++; +}; + +void protocol_send_event_hb(int64_t ts) +{ + // формируешь JSON строго по контракту + + // пример: + char buf[128]; + + uint32_t id = protocol_next_id(); + + snprintf(buf, sizeof(buf), + "{\"v\":1,\"id\":%" PRIu32 ",\"t\":\"e\",\"ts\":%" PRIu64 ",\"d\":%u,\"p\":{\"type\":\"hb\"}}", + id, ts, CONFIG_DEVICE_ID); + + ws_send(buf); +} + +bool protocol_is_connected() +{ + return ws_is_connected(); } \ No newline at end of file diff --git a/esp32/main/protocol.h b/esp32/main/protocol.h index bd1893f..b8736cf 100644 --- a/esp32/main/protocol.h +++ b/esp32/main/protocol.h @@ -2,6 +2,8 @@ #include #include +#include "ringbuf.h" + #ifdef __cplusplus extern "C" { @@ -10,7 +12,6 @@ extern "C" { int build_telemetry( char* buf, size_t buf_size, - uint32_t msg_id, int64_t ts, uint32_t device_id, const char* metric, @@ -44,6 +45,12 @@ int build_event( const char* message // optional ); +uint32_t protocol_next_id(); + +void protocol_send_event_hb(int64_t ts); + +bool protocol_is_connected(); + #ifdef __cplusplus } #endif \ No newline at end of file diff --git a/esp32/main/ringbuf.cpp b/esp32/main/ringbuf.cpp index 93fd0fe..29cefd4 100644 --- a/esp32/main/ringbuf.cpp +++ b/esp32/main/ringbuf.cpp @@ -3,16 +3,18 @@ void ringbuf_init(ringbuf_t *rb) { rb->head = 0; for (int i = 0; i < RINGBUF_SIZE; i++) { - rb->values[i] = 0; + rb->values[i].ts_ms = 0; + rb->values[i].value = 0; } } -void ringbuf_push(ringbuf_t *rb, int v) { - rb->values[rb->head] = v; +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, int *out) { +void ringbuf_copy(const ringbuf_t *rb, sample_t *out) { int idx = rb->head; for (int i = 0; i < RINGBUF_SIZE; i++) { diff --git a/esp32/main/ringbuf.h b/esp32/main/ringbuf.h index c3f64bb..006106b 100644 --- a/esp32/main/ringbuf.h +++ b/esp32/main/ringbuf.h @@ -5,7 +5,12 @@ #define RINGBUF_SIZE 10 typedef struct { - int values[RINGBUF_SIZE]; + uint32_t ts_ms; + int value; +} sample_t; + +typedef struct { + sample_t values[RINGBUF_SIZE]; int head; } ringbuf_t; @@ -13,7 +18,6 @@ typedef struct { void ringbuf_init(ringbuf_t *rb); // Добавить значение -void ringbuf_push(ringbuf_t *rb, int v); +void ringbuf_push(ringbuf_t *rb, uint32_t ts_ms, int v); -// Скопировать данные (в порядке времени) -void ringbuf_copy(const ringbuf_t *rb, int *out); \ No newline at end of file +void ringbuf_copy(const ringbuf_t *rb, sample_t *out); \ No newline at end of file diff --git a/esp32/main/sampler_task.cpp b/esp32/main/sampler_task.cpp index e7f09da..99b5e45 100644 --- a/esp32/main/sampler_task.cpp +++ b/esp32/main/sampler_task.cpp @@ -1,37 +1,43 @@ #include "sampler_task.h" -#include "adc_reader.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; -// дать доступ другим модулям (sender потом возьмёт) ringbuf_t* sampler_get_buffer() { return &rb; } static void sampler_task(void *arg) { - while (1) { - int val = adc_reader_read(); - ringbuf_push(&rb, val); + + const TickType_t period = pdMS_TO_TICKS(6000); + TickType_t last_wake = xTaskGetTickCount(); - vTaskDelay(pdMS_TO_TICKS(100)); // 10 Гц + 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() { ringbuf_init(&rb); - adc_reader_init(); + ds18b20_init(GPIO_NUM_27); // новый драйвер xTaskCreate( sampler_task, "sampler", - 2048, + 4096, NULL, 5, NULL diff --git a/esp32/main/sender_task.cpp b/esp32/main/sender_task.cpp index d5e651e..5b97f65 100644 --- a/esp32/main/sender_task.cpp +++ b/esp32/main/sender_task.cpp @@ -5,54 +5,88 @@ #include #include + + #include "ws.h" extern "C" { #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "esp_timer.h" } -static uint32_t msg_id = 1; +static uint32_t last_send_ms = 0; +static int64_t last_send_ts = 0; -static void sender_task(void *arg) { - int data[RINGBUF_SIZE]; +static void sender_task(void* arg) +{ + ringbuf_t* rb = sampler_get_buffer(); - while (1) { - vTaskDelay(pdMS_TO_TICKS(1000)); // 1 Гц + last_send_ts = time(NULL); + 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(NULL); - ringbuf_t *rb = sampler_get_buffer(); - ringbuf_copy(rb, data); + sample_t tmp[RINGBUF_SIZE]; + ringbuf_copy(rb, tmp); - time_t now = time(NULL); + 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++; + } + } int len = build_telemetry( buf, sizeof(buf), - msg_id++, - now, - 1, // device_id (пока захардкожен) - "v", // metric - "adc35", // source - "raw", // unit - data, - RINGBUF_SIZE + last_send_ts, + 1, // device_id (пока захардкожен) + "t", // metric + "ds18b20", // source + "c_x100", // unit + out_values, + count ); - if (len > 0) { - if (ws_is_connected()) { + if (len > 0) + { + if (ws_is_connected()) + { ws_send(buf); - } else { - printf("%s\n", buf); // fallback } - } else { + 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() { +void sender_task_start() +{ xTaskCreate( sender_task, "sender", @@ -61,4 +95,4 @@ void sender_task_start() { 5, NULL ); -} \ No newline at end of file +} diff --git a/esp32/main/system_init.cpp b/esp32/main/system_init.cpp index 5104af7..d9803ef 100644 --- a/esp32/main/system_init.cpp +++ b/esp32/main/system_init.cpp @@ -7,6 +7,7 @@ #include "esp_event.h" #include "esp_netif.h" #include "ws.h" +#include "gpio_init.h" static const char* TAG = "system"; @@ -14,6 +15,8 @@ void system_init() { ESP_LOGI("SYSTEM", "Initializing system..."); + init_gpio(); + init_wifi(CONFIG_WIFI_SSID, CONFIG_WIFI_PASS); // Запуск WiFI init_time(); // Установка времени diff --git a/esp32/main/ws.cpp b/esp32/main/ws.cpp index 5b085ac..2347da0 100644 --- a/esp32/main/ws.cpp +++ b/esp32/main/ws.cpp @@ -3,19 +3,20 @@ #include "esp_log.h" #include #include "sdkconfig.h" +#include "fw_command.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, +static void ws_event_handler(void* handler_args, esp_event_base_t base, int32_t event_id, - void *event_data) + void* event_data) { - switch (event_id) { - + switch (event_id) + { case WEBSOCKET_EVENT_CONNECTED: connected = true; ESP_LOGI(TAG, "Connected"); @@ -31,11 +32,36 @@ static void ws_event_handler(void *handler_args, ESP_LOGE(TAG, "Error"); break; - case WEBSOCKET_EVENT_DATA: { - auto *data = (esp_websocket_event_data_t *)event_data; + 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 + } + break; - } + } } } @@ -52,7 +78,8 @@ void ws_init(const char* uri) void ws_start() { - if (client) { + if (client) + { esp_websocket_client_start(client); } } @@ -64,7 +91,8 @@ bool ws_is_connected() void ws_send(const char* data) { - if (connected && client) { + if (connected && client) + { esp_websocket_client_send_text(client, data, strlen(data), portMAX_DELAY); } } @@ -79,4 +107,4 @@ void ws_go() CONFIG_SERVER_PORT); ws_init(uri); ws_start(); -} \ No newline at end of file +} diff --git a/esp32/managed_components/espressif__onewire_bus/.build-test-rules.yml b/esp32/managed_components/espressif__onewire_bus/.build-test-rules.yml new file mode 100644 index 0000000..6032452 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/.build-test-rules.yml @@ -0,0 +1,6 @@ +onewire_bus/test_apps: + disable: + - if: CONFIG_NAME == "rmt" and SOC_RMT_SUPPORTED != 1 + reason: RMT backend variant requires SOC RMT support + - if: CONFIG_NAME == "uart" and SOC_UART_SUPPORTED != 1 + reason: UART backend variant requires SOC UART support diff --git a/esp32/managed_components/espressif__onewire_bus/.component_hash b/esp32/managed_components/espressif__onewire_bus/.component_hash new file mode 100644 index 0000000..da4e31c --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/.component_hash @@ -0,0 +1 @@ +d709015ba466095259228521cf1bad9c0cdaaa42a92ea5d9c88ec6c28ae89e9b \ No newline at end of file diff --git a/esp32/managed_components/espressif__onewire_bus/CHANGELOG.md b/esp32/managed_components/espressif__onewire_bus/CHANGELOG.md new file mode 100644 index 0000000..f5736e4 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/CHANGELOG.md @@ -0,0 +1,19 @@ +## 1.1.0 + +- Add UART backend support for 1-Wire bus (`onewire_new_bus_uart`) alongside the existing RMT backend. + +## 1.0.4 + +- Support `en_pull_up` config option in `onewire_bus_config_t`, which can enable the internal pull-up resistor on the GPIO pin used for the one-wire bus. This is useful when using a GPIO pin that does not have a pull-up resistor connected externally. + +## 1.0.3 + +- Improve the driver to support esp-idf v6.0 + +## 1.0.2 + +- raise recovery time to support more sensor on longer wire (d0b2b52) + +## 1.0.0 + +- Initial driver version, with the RMT driver as backend controller diff --git a/esp32/managed_components/espressif__onewire_bus/CHECKSUMS.json b/esp32/managed_components/espressif__onewire_bus/CHECKSUMS.json new file mode 100644 index 0000000..594b829 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/CHECKSUMS.json @@ -0,0 +1 @@ +{"version":"1.0","algorithm":"sha256","created_at":"2026-04-02T08:31:31.921867+00:00","files":[{"path":".build-test-rules.yml","size":272,"hash":"d34faa08f404a108c17bcaf9709561dbc978c4f4fea0b912857758064430e069"},{"path":"CHANGELOG.md","size":591,"hash":"6be143366596176ad9f2d17932154117ef861918881e5abcfce678d55331e361"},{"path":"CMakeLists.txt","size":742,"hash":"8f26f2f876456b7533884d02b72dfee3481fe8617420c34cbfacf8c03dbc187a"},{"path":"LICENSE","size":11358,"hash":"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30"},{"path":"README.md","size":675,"hash":"ebc34c6d6383f828a433fced21289cc24a1248a0383c240aabb95912170c1cc6"},{"path":"idf_component.yml","size":386,"hash":"4054b22a198bae53f75aeacced4a6577dbd067df3d07567f0ff3930164d2bb10"},{"path":"include/onewire_bus.h","size":2993,"hash":"39804a54186091e6f6b17aba786d5e78b70e9ad8e9bbd79cf31989f282dada21"},{"path":"include/onewire_bus_impl_rmt.h","size":1323,"hash":"a8aeebf3cd5f23ddd00c76ec43dfd6f1a30eee819814fb0c0d50a1e292834511"},{"path":"include/onewire_bus_impl_uart.h","size":1253,"hash":"403a3cb182173148f7999dbfbd800b9baab80f51c3f00607b1ccfc18a91af108"},{"path":"include/onewire_cmd.h","size":356,"hash":"f0de787a3337b59e6a85c8a245ef5b56710830233ceeabf5fe59d1c0a0c50b38"},{"path":"include/onewire_crc.h","size":584,"hash":"9db2a7b437a6f72a0711698c9fad5ceca60458e828557cc38a3c8fc217fd5e06"},{"path":"include/onewire_device.h","size":1799,"hash":"f6b8202100de34d4b102953839e10df33b7850c35c599fb1d5bcf33ec1305773"},{"path":"include/onewire_types.h","size":1107,"hash":"bbfbaffac8df97fe53fcf42550f63dad9e3c29fac3fb26a03d735075eed85b31"},{"path":"interface/onewire_bus_interface.h","size":3196,"hash":"b03cea2218f99845b4486b7112760e08e4b7eb6b6dcc238f8d745afc593eae19"},{"path":"src/onewire_bus_api.c","size":1512,"hash":"894fdc649d552fe090c19bd3eb9e1433db71f95b8455b86faf5463636905ab3e"},{"path":"src/onewire_bus_impl_rmt.c","size":21406,"hash":"2e48338edb5e72065c239863811e387adc7164c2bb0f344bbca7a8e4ab07f624"},{"path":"src/onewire_bus_impl_uart.c","size":13129,"hash":"00f18bf1c3ea011c0952ca8dbf898a74094985932d7da2b4c8ff15ad99244576"},{"path":"src/onewire_crc.c","size":2193,"hash":"06a7ce5ffffbd1cbf4de35155cba2d5b903f718a0fbc3478265c09a4458a4ce0"},{"path":"src/onewire_device.c","size":4934,"hash":"3612afec5795eb9f47adef0b340c33c8584dde4d66d9581621ae203a6e9e031a"},{"path":"test_apps/CMakeLists.txt","size":135,"hash":"4035fd2167868739138e691d3080616b9d3b5c69adf567fbd33fb74b36a143ce"},{"path":"test_apps/pytest_onewire_bus.py","size":722,"hash":"c00451c79c16e3e3010045c21a21cb8c8e82217b898946061a4d133d096eca11"},{"path":"test_apps/sdkconfig.ci.rmt","size":75,"hash":"e8b859d77cdf1dc4125c5cdb4681dab1219db72c845f3f1b62f6d06397d804fa"},{"path":"test_apps/sdkconfig.ci.uart","size":75,"hash":"8413491b74b639e0a4e4d29968989d8434ed44bd225664352b631b7794c56320"},{"path":"test_apps/main/CMakeLists.txt","size":127,"hash":"2c724c0cf0fe496ed895d2360a281ad0c81b398c43cdfccb290c5b627c852f9a"},{"path":"test_apps/main/Kconfig.projbuild","size":1782,"hash":"70ccb326b523ed810f1597fe51c8b12a120156bd431552d0106c4f8e468c10c1"},{"path":"test_apps/main/idf_component.yml","size":83,"hash":"76ba47c24c863880ee8e6667ccde3ca1b68454993f31fd45755d5faa2ab83edc"},{"path":"test_apps/main/onewire_bus_test.c","size":2906,"hash":"f25f3a330bcc50fb30da60470c38401b88a3858415abe56668cf386e4f49a2d4"}]} \ No newline at end of file diff --git a/esp32/managed_components/espressif__onewire_bus/CMakeLists.txt b/esp32/managed_components/espressif__onewire_bus/CMakeLists.txt new file mode 100644 index 0000000..d50212e --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/CMakeLists.txt @@ -0,0 +1,23 @@ +set(srcs "src/onewire_bus_api.c" + "src/onewire_crc.c" + "src/onewire_device.c") + +if(CONFIG_SOC_RMT_SUPPORTED) + list(APPEND srcs "src/onewire_bus_impl_rmt.c") +endif() + +if(CONFIG_SOC_UART_SUPPORTED) + list(APPEND srcs "src/onewire_bus_impl_uart.c") +endif() + +set(priv_requires) +# Starting from esp-idf v5.3, the peripheral drivers are in separate components +if("${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}" VERSION_GREATER_EQUAL "5.3") + list(APPEND priv_requires "esp_driver_rmt" "esp_driver_uart" "esp_driver_gpio") +else() + list(APPEND priv_requires "driver") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "include" "interface" + PRIV_REQUIRES ${priv_requires}) diff --git a/esp32/managed_components/espressif__onewire_bus/LICENSE b/esp32/managed_components/espressif__onewire_bus/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/LICENSE @@ -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. diff --git a/esp32/managed_components/espressif__onewire_bus/README.md b/esp32/managed_components/espressif__onewire_bus/README.md new file mode 100644 index 0000000..62d4da9 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/README.md @@ -0,0 +1,13 @@ +# Dallas 1-Wire Bus Driver + +[![Component Registry](https://components.espressif.com/components/espressif/onewire_bus/badge.svg)](https://components.espressif.com/components/espressif/onewire_bus) + +This directory contains an implementation for Dallas 1-Wire bus by different peripherals. +The following low-level backends are currently supported: + +- RMT backend (`onewire_new_bus_rmt`) +- UART backend (`onewire_new_bus_uart`) + +## Appendix + +* [DS18B20 device driver based on the 1-Wire Bus driver](https://components.espressif.com/components/espressif/ds18b20) and the [DS18B20 Example](https://github.com/espressif/esp-bsp/tree/master/components/ds18b20/examples/ds18b20-read) diff --git a/esp32/managed_components/espressif__onewire_bus/idf_component.yml b/esp32/managed_components/espressif__onewire_bus/idf_component.yml new file mode 100644 index 0000000..1ea5983 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/idf_component.yml @@ -0,0 +1,10 @@ +dependencies: + idf: '>=5.0' +description: Driver for Dallas 1-Wire bus +issues: https://github.com/espressif/idf-extra-components/issues +repository: git://github.com/espressif/idf-extra-components.git +repository_info: + commit_sha: bd6b21799cb9034e050a41b2c299b52a7e71be83 + path: onewire_bus +url: https://github.com/espressif/idf-extra-components/tree/master/onewire_bus +version: 1.1.0 diff --git a/esp32/managed_components/espressif__onewire_bus/include/onewire_bus.h b/esp32/managed_components/espressif__onewire_bus/include/onewire_bus.h new file mode 100644 index 0000000..db47456 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/include/onewire_bus.h @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "soc/soc_caps.h" +#include "onewire_types.h" +#if SOC_RMT_SUPPORTED +#include "onewire_bus_impl_rmt.h" +#endif +#if SOC_UART_SUPPORTED +#include "onewire_bus_impl_uart.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Write bytes to 1-wire bus + * + * @param[in] bus 1-Wire bus handle + * @param[in] tx_data pointer to data to be sent + * @param[in] tx_data_size size of data to be sent, in bytes + * @return + * - ESP_OK: Write bytes to 1-Wire bus successfully + * - ESP_ERR_INVALID_ARG: Write bytes to 1-Wire bus failed because of invalid argument + * - ESP_FAIL: Write bytes to 1-Wire bus failed because of other errors + */ +esp_err_t onewire_bus_write_bytes(onewire_bus_handle_t bus, const uint8_t *tx_data, uint8_t tx_data_size); + +/** + * @brief Read bytes from 1-wire bus + * + * @param[in] bus 1-wire bus handle + * @param[out] rx_buf pointer to buffer to store received data + * @param[in] rx_buf_size size of buffer to store received data, in bytes + * @return + * - ESP_OK: Read bytes from 1-Wire bus successfully + * - ESP_ERR_INVALID_ARG: Read bytes from 1-Wire bus failed because of invalid argument + * - ESP_FAIL: Read bytes from 1-Wire bus failed because of other errors + */ +esp_err_t onewire_bus_read_bytes(onewire_bus_handle_t bus, uint8_t *rx_buf, size_t rx_buf_size); + +/** + * @brief Write a bit to 1-wire bus, this is a blocking function + * + * @param[in] handle 1-wire bus handle + * @param[in] tx_bit bit to transmit, 0 for zero bit, other for one bit + * @return + * - ESP_OK Write bit to 1-wire bus successfully. + * - ESP_ERR_INVALID_ARG Invalid argument. + */ +esp_err_t onewire_bus_write_bit(onewire_bus_handle_t bus, uint8_t tx_bit); + +/** + * @brief Read a bit from 1-wire bus + * + * @param[in] handle 1-wire bus handle + * @param[out] rx_bit received bit, 0 for zero bit, 1 for one bit + * @return + * - ESP_OK Read bit from 1-wire bus successfully. + * - ESP_ERR_INVALID_ARG Invalid argument. + */ +esp_err_t onewire_bus_read_bit(onewire_bus_handle_t bus, uint8_t *rx_bit); + +/** + * @brief Send reset pulse to the bus, and check if there are devices attached to the bus + * + * @param[in] bus 1-Wire bus handle + * + * @return + * - ESP_OK: Reset 1-Wire bus successfully and find device on the bus + * - ESP_ERR_NOT_FOUND: Reset 1-Wire bus successfully but no device found on the bus + * - ESP_FAIL: Reset 1-Wire bus failed because of other errors + */ +esp_err_t onewire_bus_reset(onewire_bus_handle_t bus); + +/** + * @brief Free 1-Wire bus resources + * + * @param[in] bus 1-Wire bus handle + * + * @return + * - ESP_OK: Free resources successfully + * - ESP_FAIL: Free resources failed because error occurred + */ +esp_err_t onewire_bus_del(onewire_bus_handle_t bus); + +#ifdef __cplusplus +} +#endif diff --git a/esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_rmt.h b/esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_rmt.h new file mode 100644 index 0000000..40b93c8 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_rmt.h @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "onewire_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 1-Wire bus RMT specific configuration + */ +typedef struct { + uint32_t max_rx_bytes; /*!< Set the largest possible single receive size, + which determines the size of the internal buffer that used to save the receiving RMT symbols */ +} onewire_bus_rmt_config_t; + +/** + * @brief Create 1-Wire bus with RMT backend + * + * @note One 1-Wire bus utilizes a pair of RMT TX and RX channels + * + * @param[in] bus_config 1-Wire bus configuration + * @param[in] rmt_config RMT specific configuration + * @param[out] ret_bus Returned 1-Wire bus handle + * @return + * - ESP_OK: create 1-Wire bus handle successfully + * - ESP_ERR_INVALID_ARG: create 1-Wire bus handle failed because of invalid argument + * - ESP_ERR_NO_MEM: create 1-Wire bus handle failed because of out of memory + * - ESP_FAIL: create 1-Wire bus handle failed because some other error + */ +esp_err_t onewire_new_bus_rmt(const onewire_bus_config_t *bus_config, const onewire_bus_rmt_config_t *rmt_config, onewire_bus_handle_t *ret_bus); + +#ifdef __cplusplus +} +#endif diff --git a/esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_uart.h b/esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_uart.h new file mode 100644 index 0000000..d63b237 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/include/onewire_bus_impl_uart.h @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "onewire_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 1-Wire bus UART specific configuration + */ +typedef struct { + int uart_port_num; /*!< UART port number, e.g. UART_NUM_1 */ +} onewire_bus_uart_config_t; + +/** + * @brief Create 1-Wire bus with UART backend + * + * @note TX and RX will both be configured to bus_config->bus_gpio_num. + * And this GPIO will be configured as open-drain mode. + * + * @param[in] bus_config 1-Wire bus configuration + * @param[in] uart_config UART specific configuration + * @param[out] ret_bus Returned 1-Wire bus handle + * @return + * - ESP_OK: create 1-Wire bus handle successfully + * - ESP_ERR_INVALID_ARG: create 1-Wire bus handle failed because of invalid argument + * - ESP_ERR_NO_MEM: create 1-Wire bus handle failed because of out of memory + * - ESP_FAIL: create 1-Wire bus handle failed because some other error + */ +esp_err_t onewire_new_bus_uart(const onewire_bus_config_t *bus_config, const onewire_bus_uart_config_t *uart_config, onewire_bus_handle_t *ret_bus); + +#ifdef __cplusplus +} +#endif diff --git a/esp32/managed_components/espressif__onewire_bus/include/onewire_cmd.h b/esp32/managed_components/espressif__onewire_bus/include/onewire_cmd.h new file mode 100644 index 0000000..5a091b5 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/include/onewire_cmd.h @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#define ONEWIRE_CMD_SEARCH_NORMAL 0xF0 +#define ONEWIRE_CMD_MATCH_ROM 0x55 +#define ONEWIRE_CMD_SKIP_ROM 0xCC +#define ONEWIRE_CMD_SEARCH_ALARM 0xEC +#define ONEWIRE_CMD_READ_POWER_SUPPLY 0xB4 diff --git a/esp32/managed_components/espressif__onewire_bus/include/onewire_crc.h b/esp32/managed_components/espressif__onewire_bus/include/onewire_crc.h new file mode 100644 index 0000000..c754d1f --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/include/onewire_crc.h @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Calculate Dallas CRC8 value of a given buffer + * + * @param[in] init_crc Initial CRC value + * @param[in] input Input buffer to calculate CRC value + * @param[in] input_size Size of input buffer, in bytes + * @return CRC8 result of the input buffer + */ +uint8_t onewire_crc8(uint8_t init_crc, uint8_t *input, size_t input_size); + +#ifdef __cplusplus +} +#endif diff --git a/esp32/managed_components/espressif__onewire_bus/include/onewire_device.h b/esp32/managed_components/espressif__onewire_bus/include/onewire_device.h new file mode 100644 index 0000000..fee0c1f --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/include/onewire_device.h @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" +#include "onewire_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 1-Wire device generic type + */ +typedef struct onewire_device_t { + onewire_bus_handle_t bus; /*!< Which bus the 1-Wire device is attached to */ + onewire_device_address_t address; /*!< Device address (represented by its internal ROM ID) */ +} onewire_device_t; + +/** + * @brief Create an iterator to enumerate the 1-Wire devices on the bus + * + * @param[in] bus 1-Wire bus handle + * @param[out] ret_iter Returned created device iterator + * @return + * - ESP_OK: Create device iterator successfully + * - ESP_ERR_INVALID_ARG: Invalid argument + * - ESP_ERR_NO_MEM: No memory to create device iterator + * - ESP_FAIL: Other errors + */ +esp_err_t onewire_new_device_iter(onewire_bus_handle_t bus, onewire_device_iter_handle_t *ret_iter); + +/** + * @brief Delete the device iterator + * + * @param[in] iter Device iterator handle + * @return + * - ESP_OK: Delete device iterator successfully + * - ESP_ERR_INVALID_ARG: Invalid argument + * - ESP_FAIL: Other errors + */ +esp_err_t onewire_del_device_iter(onewire_device_iter_handle_t iter); + +/** + * @brief Get the next 1-Wire device from the iterator + * + * @param[in] iter Device iterator handle + * @param[out] dev Returned 1-Wire device handle + * @return + * - ESP_OK: Get next device successfully + * - ESP_ERR_INVALID_ARG: Invalid argument + * - ESP_ERR_NOT_FOUND: No more device to get + * - ESP_FAIL: Other errors + */ +esp_err_t onewire_device_iter_get_next(onewire_device_iter_handle_t iter, onewire_device_t *dev); + +#ifdef __cplusplus +} +#endif diff --git a/esp32/managed_components/espressif__onewire_bus/include/onewire_types.h b/esp32/managed_components/espressif__onewire_bus/include/onewire_types.h new file mode 100644 index 0000000..c10b269 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/include/onewire_types.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Type of 1-Wire bus handle + */ +typedef struct onewire_bus_t *onewire_bus_handle_t; + +/** + * @brief Type of the address for a 1-Wire compatible device + */ +typedef uint64_t onewire_device_address_t; + +/** + * @brief Type of 1-Wire device iterator handle + */ +typedef struct onewire_device_iter_t *onewire_device_iter_handle_t; + +/** + * @brief 1-Wire bus configuration + */ +typedef struct { + int bus_gpio_num; /*!< GPIO number that used by the 1-Wire bus */ + struct onewire_bus_config_flags { + uint32_t en_pull_up: 1; /*!< Set true to enable internal pull-up resistor. + Please note the internal pull-up resistor cannot provide enough current for some devices, + so external pull-up resistor is still recommended. */ + } flags; /*!< Configuration flags for the bus */ +} onewire_bus_config_t; + +#ifdef __cplusplus +} +#endif diff --git a/esp32/managed_components/espressif__onewire_bus/interface/onewire_bus_interface.h b/esp32/managed_components/espressif__onewire_bus/interface/onewire_bus_interface.h new file mode 100644 index 0000000..9920c4e --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/interface/onewire_bus_interface.h @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#pragma once + +#include +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct onewire_bus_t onewire_bus_t; /*!< Type of 1-Wire bus */ + +/** + * @brief 1-Wire bus interface definition + */ +struct onewire_bus_t { + /** + * @brief Write bytes to 1-wire bus + * + * @note This is a blocking function + * + * @param[in] bus 1-Wire bus handle + * @param[in] tx_data pointer to data to be sent + * @param[in] tx_data_size size of data to be sent, in bytes + * @return + * - ESP_OK: Write bytes to 1-Wire bus successfully + * - ESP_ERR_INVALID_ARG: Write bytes to 1-Wire bus failed because of invalid argument + * - ESP_FAIL: Write bytes to 1-Wire bus failed because of other errors + */ + esp_err_t (*write_bytes)(onewire_bus_t *bus, const uint8_t *tx_data, uint8_t tx_data_size); + + /** + * @brief Read bytes from 1-wire bus + * + * @param[in] bus 1-wire bus handle + * @param[out] rx_buf pointer to buffer to store received data + * @param[in] rx_buf_size size of buffer to store received data, in bytes + * @return + * - ESP_OK: Read bytes from 1-Wire bus successfully + * - ESP_ERR_INVALID_ARG: Read bytes from 1-Wire bus failed because of invalid argument + * - ESP_FAIL: Read bytes from 1-Wire bus failed because of other errors + */ + esp_err_t (*read_bytes)(onewire_bus_t *bus, uint8_t *rx_buf, size_t rx_buf_size); + + /** + * @brief Write a bit to 1-wire bus, this is a blocking function + * + * @param[in] handle 1-wire bus handle + * @param[in] tx_bit bit to transmit, 0 for zero bit, other for one bit + * @return + * - ESP_OK Write bit to 1-wire bus successfully. + * - ESP_ERR_INVALID_ARG Invalid argument. + */ + esp_err_t (*write_bit)(onewire_bus_handle_t handle, uint8_t tx_bit); + + /** + * @brief Read a bit from 1-wire bus + * + * @param[in] handle 1-wire bus handle + * @param[out] rx_bit received bit, 0 for zero bit, 1 for one bit + * @return + * - ESP_OK Read bit from 1-wire bus successfully. + * - ESP_ERR_INVALID_ARG Invalid argument. + */ + esp_err_t (*read_bit)(onewire_bus_handle_t handle, uint8_t *rx_bit); + + /** + * @brief Send reset pulse to the bus, and check if there are devices attached to the bus + * + * @param[in] bus 1-Wire bus handle + * + * @return + * - ESP_OK: Reset 1-Wire bus successfully and find device on the bus + * - ESP_ERR_NOT_FOUND: Reset 1-Wire bus successfully but no device found on the bus + * - ESP_FAIL: Reset 1-Wire bus failed because of other errors + */ + esp_err_t (*reset)(onewire_bus_t *bus); + + /** + * @brief Free 1-Wire bus resources + * + * @param[in] bus 1-Wire bus handle + * + * @return + * - ESP_OK: Free resources successfully + * - ESP_FAIL: Free resources failed because error occurred + */ + esp_err_t (*del)(onewire_bus_t *bus); +}; + +#ifdef __cplusplus +} +#endif diff --git a/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_api.c b/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_api.c new file mode 100644 index 0000000..3cded8f --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_api.c @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "esp_log.h" +#include "esp_check.h" +#include "onewire_types.h" +#include "onewire_bus_interface.h" + +static const char *TAG = "1-wire"; + +esp_err_t onewire_bus_reset(onewire_bus_handle_t bus) +{ + ESP_RETURN_ON_FALSE(bus, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return bus->reset(bus); +} + +esp_err_t onewire_bus_write_bytes(onewire_bus_handle_t bus, const uint8_t *tx_data, uint8_t tx_data_size) +{ + ESP_RETURN_ON_FALSE(bus && tx_data && tx_data_size, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return bus->write_bytes(bus, tx_data, tx_data_size); +} + +esp_err_t onewire_bus_read_bytes(onewire_bus_handle_t bus, uint8_t *rx_buf, size_t rx_buf_size) +{ + ESP_RETURN_ON_FALSE(bus && rx_buf && rx_buf_size, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return bus->read_bytes(bus, rx_buf, rx_buf_size); +} + +esp_err_t onewire_bus_write_bit(onewire_bus_handle_t bus, uint8_t tx_bit) +{ + ESP_RETURN_ON_FALSE(bus, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return bus->write_bit(bus, tx_bit); +} + +esp_err_t onewire_bus_read_bit(onewire_bus_handle_t bus, uint8_t *rx_bit) +{ + ESP_RETURN_ON_FALSE(bus && rx_bit, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return bus->read_bit(bus, rx_bit); +} + +esp_err_t onewire_bus_del(onewire_bus_handle_t bus) +{ + ESP_RETURN_ON_FALSE(bus, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + return bus->del(bus); +} diff --git a/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_rmt.c b/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_rmt.c new file mode 100644 index 0000000..5d79807 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_rmt.c @@ -0,0 +1,521 @@ +/* + * SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "esp_check.h" +#include "esp_attr.h" +#include "driver/rmt_tx.h" +#include "driver/rmt_rx.h" +#include "driver/gpio.h" +#include "esp_private/gpio.h" +#include "onewire_bus_impl_rmt.h" +#include "onewire_bus_interface.h" +#include "esp_idf_version.h" + +static const char *TAG = "1-wire.rmt"; + +#define ONEWIRE_RMT_RESOLUTION_HZ 1000000 // RMT channel default resolution for 1-wire bus, 1MHz, 1tick = 1us +#define ONEWIRE_RMT_DEFAULT_TRANS_QUEUE_SIZE 4 + +// the memory size of each RMT channel, in words (4 bytes) +#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 +#define ONEWIRE_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 64 +#else +#define ONEWIRE_RMT_DEFAULT_MEM_BLOCK_SYMBOLS 48 +#endif + +// for chips whose RMT RX channel doesn't support ping-pong, we need the user to tell the maximum number of bytes will be received +#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 +// one RMT symbol represents one bit, so x8 +#define ONEWIRE_RMT_RX_MEM_BLOCK_SIZE (rmt_config->max_rx_bytes * 8) +#else // otherwise, we just use one memory block, to save resources +#define ONEWIRE_RMT_RX_MEM_BLOCK_SIZE ONEWIRE_RMT_DEFAULT_MEM_BLOCK_SYMBOLS +#endif + +/* +Reset Pulse: + + | RESET_PULSE | RESET_WAIT_DURATION | + | _DURATION | | + | | | | RESET | | + | | * | | _PRESENCE | | + | | | | _DURATION | | +----------+ +-----+ +-------------- + | | | | + | | | | + | | | | + +-------------+ +-----------+ +*: RESET_PRESENCE_WAIT_DURATION +*/ +#define ONEWIRE_RESET_PULSE_DURATION 500 // duration of reset bit +#define ONEWIRE_RESET_WAIT_DURATION 200 // how long should master wait for device to show its presence +#define ONEWIRE_RESET_PRESENCE_WAIT_DURATION_MIN 15 // minimum duration for master to wait device to show its presence +#define ONEWIRE_RESET_PRESENCE_DURATION_MIN 60 // minimum duration for master to recognize device as present + +/* +Write 1 bit: + + | SLOT_START | SLOT_BIT | SLOT_RECOVERY | NEXT + | _DURATION | _DURATION | _DURATION | SLOT + | | | | +----------+ +------------------------------------- + | | + | | + | | + +------------+ + +Write 0 bit: + + | SLOT_START | SLOT_BIT | SLOT_RECOVERY | NEXT + | _DURATION | _DURATION | _DURATION | SLOT + | | | | +----------+ +------------------------- + | | + | | + | | + +------------------------+ + +Read 1 bit: + + + | SLOT_START | SLOT_BIT_DURATION | SLOT_RECOVERY | NEXT + | _DURATION | | _DURATION | SLOT + | | SLOT_BIT_ | | | + | | SAMPLE_TIME | | | +----------+ +---------------------------------------------- + | | + | | + | | + +------------+ + +Read 0 bit: + + | SLOT_START | SLOT_BIT_DURATION | SLOT_RECOVERY | NEXT + | _DURATION | | _DURATION | SLOT + | | SLOT_BIT_ | | | + | | SAMPLE_TIME | | | +----------+ | | +----------------------------- + | | | + | | PULLED DOWN | + | | BY DEVICE | + +-----------------------------+ +*/ +#define ONEWIRE_SLOT_START_DURATION 2 // bit start pulse duration +#define ONEWIRE_SLOT_BIT_DURATION 60 // duration for each bit to transmit +// refer to https://www.maximintegrated.com/en/design/technical-documents/app-notes/3/3829.html for more information +#define ONEWIRE_SLOT_RECOVERY_DURATION 5 // recovery time between each bit, should be longer in parasite power mode +#define ONEWIRE_SLOT_BIT_SAMPLE_TIME 15 // how long after bit start pulse should the master sample from the bus + +typedef struct { + onewire_bus_t base; /*!< base class */ + rmt_channel_handle_t tx_channel; /*!< rmt tx channel handler */ + rmt_channel_handle_t rx_channel; /*!< rmt rx channel handler */ + + gpio_num_t data_gpio_num; /*!< GPIO number for 1-wire bus */ + + rmt_encoder_handle_t tx_bytes_encoder; /*!< used to encode commands and data */ + rmt_encoder_handle_t tx_copy_encoder; /*!< used to encode reset pulse and bits */ + + rmt_symbol_word_t *rx_symbols_buf; /*!< hold rmt raw symbols */ + + size_t max_rx_bytes; /*!< buffer size in byte for single receive transaction */ + + QueueHandle_t receive_queue; + SemaphoreHandle_t bus_mutex; +} onewire_bus_rmt_obj_t; + +static rmt_symbol_word_t onewire_reset_pulse_symbol = { + .level0 = 0, + .duration0 = ONEWIRE_RESET_PULSE_DURATION, + .level1 = 1, + .duration1 = ONEWIRE_RESET_WAIT_DURATION +}; + +static rmt_symbol_word_t onewire_bit0_symbol = { + .level0 = 0, + .duration0 = ONEWIRE_SLOT_START_DURATION + ONEWIRE_SLOT_BIT_DURATION, + .level1 = 1, + .duration1 = ONEWIRE_SLOT_RECOVERY_DURATION +}; + +static rmt_symbol_word_t onewire_bit1_symbol = { + .level0 = 0, + .duration0 = ONEWIRE_SLOT_START_DURATION, + .level1 = 1, + .duration1 = ONEWIRE_SLOT_BIT_DURATION + ONEWIRE_SLOT_RECOVERY_DURATION +}; + +const static rmt_transmit_config_t onewire_rmt_tx_config = { + .loop_count = 0, // no transfer loop + .flags.eot_level = 1 // onewire bus should be released in IDLE +}; + +const static rmt_receive_config_t onewire_rmt_rx_config = { + .signal_range_min_ns = 1000000000 / ONEWIRE_RMT_RESOLUTION_HZ, + .signal_range_max_ns = (ONEWIRE_RESET_PULSE_DURATION + ONEWIRE_RESET_WAIT_DURATION) * 1000, +}; + +static esp_err_t onewire_bus_rmt_read_bit(onewire_bus_handle_t bus, uint8_t *rx_bit); +static esp_err_t onewire_bus_rmt_write_bit(onewire_bus_handle_t bus, uint8_t tx_bit); +static esp_err_t onewire_bus_rmt_read_bytes(onewire_bus_handle_t bus, uint8_t *rx_buf, size_t rx_buf_size); +static esp_err_t onewire_bus_rmt_write_bytes(onewire_bus_handle_t bus, const uint8_t *tx_data, uint8_t tx_data_size); +static esp_err_t onewire_bus_rmt_reset(onewire_bus_handle_t bus); +static esp_err_t onewire_bus_rmt_del(onewire_bus_handle_t bus); +static esp_err_t onewire_bus_rmt_destroy(onewire_bus_rmt_obj_t *bus_rmt); + +IRAM_ATTR +bool onewire_rmt_rx_done_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *edata, void *user_data) +{ + BaseType_t task_woken = pdFALSE; + onewire_bus_rmt_obj_t *bus_rmt = (onewire_bus_rmt_obj_t *)user_data; + + xQueueSendFromISR(bus_rmt->receive_queue, edata, &task_woken); + + return task_woken; +} + +/* +[0].0 means symbol[0].duration0 + +First reset pulse after rmt channel init: + +Bus is low | Reset | Wait | Device | Bus Idle +after init | Pulse | | Presence | + +------+ +----------- + | | | + | | | + | | | +-------------------+ +----------+ + 1 2 3 + + [0].1 [0].0 [1].1 [1].0 + + +Following reset pulses: + +Bus is high | Reset | Wait | Device | Bus Idle +after init | Pulse | | Presence | +------------+ +------+ +----------- + | | | | + | | | | + | | | | + +-------+ +----------+ + 1 2 3 4 + + [0].0 [0].1 [1].0 [1].1 +*/ +static bool onewire_rmt_check_presence_pulse(rmt_symbol_word_t *rmt_symbols, size_t symbol_num) +{ + bool ret = false; + if (symbol_num >= 2) { // there should be at lease 2 symbols(3 or 4 edges) + if (rmt_symbols[0].level1 == 1) { // bus is high before reset pulse + if (rmt_symbols[0].duration1 > ONEWIRE_RESET_PRESENCE_WAIT_DURATION_MIN && + rmt_symbols[1].duration0 > ONEWIRE_RESET_PRESENCE_DURATION_MIN) { + ret = true; + } + } else { // bus is low before reset pulse(first pulse after rmt channel init) + if (rmt_symbols[0].duration0 > ONEWIRE_RESET_PRESENCE_WAIT_DURATION_MIN && + rmt_symbols[1].duration1 > ONEWIRE_RESET_PRESENCE_DURATION_MIN) { + ret = true; + } + } + } + return ret; +} + +static void onewire_rmt_decode_data(rmt_symbol_word_t *rmt_symbols, size_t symbol_num, uint8_t *rx_buf, size_t rx_buf_size) +{ + size_t byte_pos = 0; + size_t bit_pos = 0; + for (size_t i = 0; i < symbol_num; i ++) { + if (rmt_symbols[i].duration0 > ONEWIRE_SLOT_BIT_SAMPLE_TIME) { // 0 bit + rx_buf[byte_pos] &= ~(1 << bit_pos); // LSB first + } else { // 1 bit + rx_buf[byte_pos] |= 1 << bit_pos; + } + bit_pos ++; + if (bit_pos >= 8) { + bit_pos = 0; + byte_pos ++; + if (byte_pos >= rx_buf_size) { + break; + } + } + } +} + +esp_err_t onewire_new_bus_rmt(const onewire_bus_config_t *bus_config, const onewire_bus_rmt_config_t *rmt_config, onewire_bus_handle_t *ret_bus) +{ + esp_err_t ret = ESP_OK; + onewire_bus_rmt_obj_t *bus_rmt = NULL; + ESP_RETURN_ON_FALSE(bus_config && rmt_config && ret_bus, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + + bus_rmt = calloc(1, sizeof(onewire_bus_rmt_obj_t)); + ESP_RETURN_ON_FALSE(bus_rmt, ESP_ERR_NO_MEM, TAG, "no mem for onewire_bus_rmt_obj_t"); + bus_rmt->data_gpio_num = GPIO_NUM_NC; + + // create rmt bytes encoder to transmit 1-wire commands and data + rmt_bytes_encoder_config_t bytes_encoder_config = { + .bit0 = onewire_bit0_symbol, + .bit1 = onewire_bit1_symbol, + .flags.msb_first = 0, + }; + ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &bus_rmt->tx_bytes_encoder), + err, TAG, "create bytes encoder failed"); + + // create rmt copy encoder to transmit 1-wire reset pulse or bits + rmt_copy_encoder_config_t copy_encoder_config = {}; + ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(©_encoder_config, &bus_rmt->tx_copy_encoder), + err, TAG, "create copy encoder failed"); + + // create RX and TX channels and bind them to the same GPIO + rmt_rx_channel_config_t onewire_rx_channel_cfg = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = ONEWIRE_RMT_RESOLUTION_HZ, + .gpio_num = bus_config->bus_gpio_num, + .mem_block_symbols = ONEWIRE_RMT_RX_MEM_BLOCK_SIZE, + }; + ESP_GOTO_ON_ERROR(rmt_new_rx_channel(&onewire_rx_channel_cfg, &bus_rmt->rx_channel), + err, TAG, "create rmt rx channel failed"); + + rmt_tx_channel_config_t onewire_tx_channel_cfg = { + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = ONEWIRE_RMT_RESOLUTION_HZ, + .gpio_num = bus_config->bus_gpio_num, + .mem_block_symbols = ONEWIRE_RMT_DEFAULT_MEM_BLOCK_SYMBOLS, + .trans_queue_depth = ONEWIRE_RMT_DEFAULT_TRANS_QUEUE_SIZE, +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(6, 0, 0) + .flags.io_loop_back = true, + .flags.io_od_mode = true, +#endif + }; + ESP_GOTO_ON_ERROR(rmt_new_tx_channel(&onewire_tx_channel_cfg, &bus_rmt->tx_channel), + err, TAG, "create rmt tx channel failed"); + + bus_rmt->data_gpio_num = bus_config->bus_gpio_num; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // enable open-drain mode for 1-wire bus + gpio_od_enable(bus_rmt->data_gpio_num); +#endif + + if (bus_config->flags.en_pull_up) { + // enable internal pull-up resistor and disable pull-down resistor + gpio_set_pull_mode(bus_rmt->data_gpio_num, GPIO_PULLUP_ONLY); + } else { + // disable internal pull-up and pull-down resistors + gpio_set_pull_mode(bus_rmt->data_gpio_num, GPIO_FLOATING); + } + + // allocate rmt rx symbol buffer, one RMT symbol represents one bit, so x8 + bus_rmt->rx_symbols_buf = malloc(rmt_config->max_rx_bytes * sizeof(rmt_symbol_word_t) * 8); + ESP_GOTO_ON_FALSE(bus_rmt->rx_symbols_buf, ESP_ERR_NO_MEM, err, TAG, "no mem to store received RMT symbols"); + bus_rmt->max_rx_bytes = rmt_config->max_rx_bytes; + + bus_rmt->receive_queue = xQueueCreate(1, sizeof(rmt_rx_done_event_data_t)); + ESP_GOTO_ON_FALSE(bus_rmt->receive_queue, ESP_ERR_NO_MEM, err, TAG, "receive queue creation failed"); + + bus_rmt->bus_mutex = xSemaphoreCreateMutex(); + ESP_GOTO_ON_FALSE(bus_rmt->bus_mutex, ESP_ERR_NO_MEM, err, TAG, "bus mutex creation failed"); + + // register rmt rx done callback + rmt_rx_event_callbacks_t cbs = { + .on_recv_done = onewire_rmt_rx_done_callback + }; + ESP_GOTO_ON_ERROR(rmt_rx_register_event_callbacks(bus_rmt->rx_channel, &cbs, bus_rmt), + err, TAG, "enable rmt rx channel failed"); + + // enable rmt channels + ESP_GOTO_ON_ERROR(rmt_enable(bus_rmt->rx_channel), err, TAG, "enable rmt rx channel failed"); + ESP_GOTO_ON_ERROR(rmt_enable(bus_rmt->tx_channel), err, TAG, "enable rmt tx channel failed"); + + // release the bus by sending a special RMT symbol + static rmt_symbol_word_t release_symbol = { + .level0 = 1, + .duration0 = 1, + .level1 = 1, + .duration1 = 0, + }; + ESP_GOTO_ON_ERROR(rmt_transmit(bus_rmt->tx_channel, bus_rmt->tx_copy_encoder, &release_symbol, + sizeof(release_symbol), &onewire_rmt_tx_config), err, TAG, "release bus failed"); + + bus_rmt->base.del = onewire_bus_rmt_del; + bus_rmt->base.reset = onewire_bus_rmt_reset; + bus_rmt->base.write_bit = onewire_bus_rmt_write_bit; + bus_rmt->base.write_bytes = onewire_bus_rmt_write_bytes; + bus_rmt->base.read_bit = onewire_bus_rmt_read_bit; + bus_rmt->base.read_bytes = onewire_bus_rmt_read_bytes; + *ret_bus = &bus_rmt->base; + + return ret; + +err: + if (bus_rmt) { + onewire_bus_rmt_destroy(bus_rmt); + } + + return ret; +} + +static esp_err_t onewire_bus_rmt_destroy(onewire_bus_rmt_obj_t *bus_rmt) +{ + if (bus_rmt->tx_bytes_encoder) { + rmt_del_encoder(bus_rmt->tx_bytes_encoder); + } + if (bus_rmt->tx_copy_encoder) { + rmt_del_encoder(bus_rmt->tx_copy_encoder); + } + if (bus_rmt->rx_channel) { + rmt_disable(bus_rmt->rx_channel); + rmt_del_channel(bus_rmt->rx_channel); + } + if (bus_rmt->tx_channel) { + rmt_disable(bus_rmt->tx_channel); + rmt_del_channel(bus_rmt->tx_channel); + } + if (bus_rmt->receive_queue) { + vQueueDelete(bus_rmt->receive_queue); + } + if (bus_rmt->bus_mutex) { + vSemaphoreDelete(bus_rmt->bus_mutex); + } + if (bus_rmt->rx_symbols_buf) { + free(bus_rmt->rx_symbols_buf); + } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (bus_rmt->data_gpio_num != GPIO_NUM_NC) { + gpio_od_disable(bus_rmt->data_gpio_num); + } +#endif + free(bus_rmt); + return ESP_OK; +} + +static esp_err_t onewire_bus_rmt_del(onewire_bus_handle_t bus) +{ + onewire_bus_rmt_obj_t *bus_rmt = __containerof(bus, onewire_bus_rmt_obj_t, base); + return onewire_bus_rmt_destroy(bus_rmt); +} + +static esp_err_t onewire_bus_rmt_reset(onewire_bus_handle_t bus) +{ + onewire_bus_rmt_obj_t *bus_rmt = __containerof(bus, onewire_bus_rmt_obj_t, base); + esp_err_t ret = ESP_OK; + + xSemaphoreTake(bus_rmt->bus_mutex, portMAX_DELAY); + // send reset pulse while receive presence pulse + ESP_GOTO_ON_ERROR(rmt_receive(bus_rmt->rx_channel, bus_rmt->rx_symbols_buf, sizeof(rmt_symbol_word_t) * 2, &onewire_rmt_rx_config), + err, TAG, "1-wire reset pulse receive failed"); + ESP_GOTO_ON_ERROR(rmt_transmit(bus_rmt->tx_channel, bus_rmt->tx_copy_encoder, &onewire_reset_pulse_symbol, sizeof(onewire_reset_pulse_symbol), &onewire_rmt_tx_config), + err, TAG, "1-wire reset pulse transmit failed"); + + // wait and check presence pulse + rmt_rx_done_event_data_t rmt_rx_evt_data; + ESP_GOTO_ON_FALSE(xQueueReceive(bus_rmt->receive_queue, &rmt_rx_evt_data, pdMS_TO_TICKS(1000)) == pdPASS, + ESP_ERR_TIMEOUT, err, TAG, "1-wire reset pulse receive timeout"); + if (onewire_rmt_check_presence_pulse(rmt_rx_evt_data.received_symbols, rmt_rx_evt_data.num_symbols) == false) { + ret = ESP_ERR_NOT_FOUND; + } + +err: + xSemaphoreGive(bus_rmt->bus_mutex); + return ret; +} + +static esp_err_t onewire_bus_rmt_write_bytes(onewire_bus_handle_t bus, const uint8_t *tx_data, uint8_t tx_data_size) +{ + onewire_bus_rmt_obj_t *bus_rmt = __containerof(bus, onewire_bus_rmt_obj_t, base); + esp_err_t ret = ESP_OK; + + xSemaphoreTake(bus_rmt->bus_mutex, portMAX_DELAY); + // transmit data with the bytes encoder + ESP_GOTO_ON_ERROR(rmt_transmit(bus_rmt->tx_channel, bus_rmt->tx_bytes_encoder, tx_data, tx_data_size, &onewire_rmt_tx_config), + err, TAG, "1-wire data transmit failed"); + // wait the transmission to complete + ESP_GOTO_ON_ERROR(rmt_tx_wait_all_done(bus_rmt->tx_channel, 50), err, TAG, "wait for 1-wire data transmit failed"); + +err: + xSemaphoreGive(bus_rmt->bus_mutex); + return ret; +} + +// While receiving data, we use rmt transmit channel to send 0xFF to generate read pulse, +// at the same time, receive channel is used to record weather the bus is pulled down by device. +static esp_err_t onewire_bus_rmt_read_bytes(onewire_bus_handle_t bus, uint8_t *rx_buf, size_t rx_buf_size) +{ + onewire_bus_rmt_obj_t *bus_rmt = __containerof(bus, onewire_bus_rmt_obj_t, base); + esp_err_t ret = ESP_OK; + ESP_RETURN_ON_FALSE(rx_buf_size <= bus_rmt->max_rx_bytes, ESP_ERR_INVALID_ARG, TAG, "rx_buf_size too large for buffer to hold"); + memset(rx_buf, 0, rx_buf_size); + + xSemaphoreTake(bus_rmt->bus_mutex, portMAX_DELAY); + + // transmit one bits to generate read clock + uint8_t tx_buffer[rx_buf_size]; + memset(tx_buffer, 0xFF, rx_buf_size); + // transmit 1 bits while receiving + ESP_GOTO_ON_ERROR(rmt_receive(bus_rmt->rx_channel, bus_rmt->rx_symbols_buf, rx_buf_size * 8 * sizeof(rmt_symbol_word_t), &onewire_rmt_rx_config), + err, TAG, "1-wire data receive failed"); + ESP_GOTO_ON_ERROR(rmt_transmit(bus_rmt->tx_channel, bus_rmt->tx_bytes_encoder, tx_buffer, sizeof(tx_buffer), &onewire_rmt_tx_config), + err, TAG, "1-wire data transmit failed"); + + // wait the transmission finishes and decode data + rmt_rx_done_event_data_t rmt_rx_evt_data; + ESP_GOTO_ON_FALSE(xQueueReceive(bus_rmt->receive_queue, &rmt_rx_evt_data, pdMS_TO_TICKS(1000)) == pdPASS, ESP_ERR_TIMEOUT, + err, TAG, "1-wire data receive timeout"); + onewire_rmt_decode_data(rmt_rx_evt_data.received_symbols, rmt_rx_evt_data.num_symbols, rx_buf, rx_buf_size); + +err: + xSemaphoreGive(bus_rmt->bus_mutex); + return ret; +} + +static esp_err_t onewire_bus_rmt_write_bit(onewire_bus_handle_t bus, uint8_t tx_bit) +{ + onewire_bus_rmt_obj_t *bus_rmt = __containerof(bus, onewire_bus_rmt_obj_t, base); + const rmt_symbol_word_t *symbol_to_transmit = tx_bit ? &onewire_bit1_symbol : &onewire_bit0_symbol; + esp_err_t ret = ESP_OK; + + xSemaphoreTake(bus_rmt->bus_mutex, portMAX_DELAY); + + // transmit bit + ESP_GOTO_ON_ERROR(rmt_transmit(bus_rmt->tx_channel, bus_rmt->tx_copy_encoder, symbol_to_transmit, sizeof(rmt_symbol_word_t), &onewire_rmt_tx_config), + err, TAG, "1-wire bit transmit failed"); + // wait the transmission to complete + ESP_GOTO_ON_ERROR(rmt_tx_wait_all_done(bus_rmt->tx_channel, 50), err, TAG, "wait for 1-wire bit transmit failed"); + +err: + xSemaphoreGive(bus_rmt->bus_mutex); + return ret; +} + +static esp_err_t onewire_bus_rmt_read_bit(onewire_bus_handle_t bus, uint8_t *rx_bit) +{ + onewire_bus_rmt_obj_t *bus_rmt = __containerof(bus, onewire_bus_rmt_obj_t, base); + esp_err_t ret = ESP_OK; + + xSemaphoreTake(bus_rmt->bus_mutex, portMAX_DELAY); + + // transmit 1 bit while receiving + ESP_GOTO_ON_ERROR(rmt_receive(bus_rmt->rx_channel, bus_rmt->rx_symbols_buf, sizeof(rmt_symbol_word_t), &onewire_rmt_rx_config), + err, TAG, "1-wire bit receive failed"); + ESP_GOTO_ON_ERROR(rmt_transmit(bus_rmt->tx_channel, bus_rmt->tx_copy_encoder, &onewire_bit1_symbol, sizeof(rmt_symbol_word_t), &onewire_rmt_tx_config), + err, TAG, "1-wire bit transmit failed"); + + // wait the transmission finishes and decode data + rmt_rx_done_event_data_t rmt_rx_evt_data; + ESP_GOTO_ON_FALSE(xQueueReceive(bus_rmt->receive_queue, &rmt_rx_evt_data, pdMS_TO_TICKS(1000)) == pdPASS, ESP_ERR_TIMEOUT, + err, TAG, "1-wire bit receive timeout"); + uint8_t rx_buffer = 0; + onewire_rmt_decode_data(rmt_rx_evt_data.received_symbols, rmt_rx_evt_data.num_symbols, &rx_buffer, sizeof(rx_buffer)); + *rx_bit = rx_buffer & 0x01; + +err: + xSemaphoreGive(bus_rmt->bus_mutex); + return ret; +} diff --git a/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_uart.c b/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_uart.c new file mode 100644 index 0000000..182f265 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/src/onewire_bus_impl_uart.c @@ -0,0 +1,298 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" +#include "esp_check.h" +#include "driver/uart.h" +#include "driver/gpio.h" +#include "onewire_bus_impl_uart.h" +#include "onewire_bus_interface.h" +#include "esp_idf_version.h" + +static const char *TAG = "1-wire.uart"; + +#define ONEWIRE_UART_DEFAULT_TIMEOUT_MS 100 +// refer to https://www.analog.com/en/resources/technical-articles/using-a-uart-to-implement-a-1wire-bus-master.html for more information +#define ONEWIRE_UART_BAUD_RESET 9600 // baud rate for reset pulse and presence detect +#define ONEWIRE_UART_BAUD_SLOT 115200 // baud rate for read and write operations + +#define ONEWIRE_UART_RESET_TX 0xF0 // TX value for reset pulse and presence detect +#define ONEWIRE_UART_RESET_RX_NO_DEVICE 0xF0 // RX value when no device is present + +#define ONEWIRE_UART_SLOT_TX_WRITE_1 0xFF // TX value for write 1 +#define ONEWIRE_UART_SLOT_TX_WRITE_0 0x00 // TX value for write 0 +#define ONEWIRE_UART_SLOT_TX_READ 0xFF // TX value for read +#define ONEWIRE_UART_SLOT_RX_READ_1 0xFF // RX value when read 1 + +typedef struct { + onewire_bus_t base; /*!< base class */ + uart_port_t uart_port_num; /*!< UART port number */ + gpio_num_t data_gpio_num; /*!< GPIO number for 1-wire bus */ + uint32_t current_baud_rate; /*!< Note: the baud rate returned by uart_get_baudrate() could have a slight deviation from the user-configured baud rate. + That's why we store the configured baud rate here. */ + SemaphoreHandle_t bus_mutex; +} onewire_bus_uart_obj_t; + +static esp_err_t onewire_bus_uart_read_bit(onewire_bus_handle_t bus, uint8_t *rx_bit); +static esp_err_t onewire_bus_uart_write_bit(onewire_bus_handle_t bus, uint8_t tx_bit); +static esp_err_t onewire_bus_uart_read_bytes(onewire_bus_handle_t bus, uint8_t *rx_buf, size_t rx_buf_size); +static esp_err_t onewire_bus_uart_write_bytes(onewire_bus_handle_t bus, const uint8_t *tx_data, uint8_t tx_data_size); +static esp_err_t onewire_bus_uart_reset(onewire_bus_handle_t bus); +static esp_err_t onewire_bus_uart_del(onewire_bus_handle_t bus); + +static esp_err_t onewire_bus_uart_destroy(onewire_bus_uart_obj_t *bus_uart); +static esp_err_t onewire_bus_uart_set_baud_rate(onewire_bus_uart_obj_t *bus_uart, uint32_t baud_rate); +static esp_err_t onewire_bus_uart_exchange_byte(onewire_bus_uart_obj_t *bus_uart, uint8_t tx_data, uint8_t *rx_data); +static esp_err_t onewire_bus_uart_write_bit_nolock(onewire_bus_uart_obj_t *bus_uart, uint8_t tx_bit); +static esp_err_t onewire_bus_uart_read_bit_nolock(onewire_bus_uart_obj_t *bus_uart, uint8_t *rx_bit); + +esp_err_t onewire_new_bus_uart(const onewire_bus_config_t *bus_config, const onewire_bus_uart_config_t *uart_config, onewire_bus_handle_t *ret_bus) +{ + esp_err_t ret = ESP_OK; + onewire_bus_uart_obj_t *bus_uart = NULL; + ESP_RETURN_ON_FALSE(bus_config && uart_config && ret_bus, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + ESP_RETURN_ON_FALSE(GPIO_IS_VALID_OUTPUT_GPIO(bus_config->bus_gpio_num), ESP_ERR_INVALID_ARG, TAG, "invalid GPIO number"); + + bus_uart = calloc(1, sizeof(onewire_bus_uart_obj_t)); + ESP_RETURN_ON_FALSE(bus_uart, ESP_ERR_NO_MEM, TAG, "no mem for onewire_bus_uart_obj_t"); + bus_uart->uart_port_num = UART_NUM_MAX; + bus_uart->data_gpio_num = GPIO_NUM_NC; + + const gpio_config_t io_conf = { + .pin_bit_mask = (1ULL << bus_config->bus_gpio_num), + .mode = GPIO_MODE_INPUT_OUTPUT_OD, + .pull_up_en = bus_config->flags.en_pull_up ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "gpio config failed"); + bus_uart->data_gpio_num = bus_config->bus_gpio_num; + + // Simulate a 1-Wire bus using UART 8N1 mode + // refer to https://www.analog.com/en/resources/technical-articles/using-a-uart-to-implement-a-1wire-bus-master.html for more information + const uart_config_t uart_cfg = { + .baud_rate = ONEWIRE_UART_BAUD_RESET, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + ESP_GOTO_ON_ERROR(uart_param_config(uart_config->uart_port_num, &uart_cfg), err, TAG, "uart param config failed"); + bus_uart->current_baud_rate = uart_cfg.baud_rate; + ESP_GOTO_ON_ERROR(uart_set_pin(uart_config->uart_port_num, bus_config->bus_gpio_num, bus_config->bus_gpio_num, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE), + err, TAG, "uart set pin failed"); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0) + // Set the RX buffer to minimum size because we only need to receive 1 byte at a time + // Disable the TX buffer because we only need to send 1 byte at a time + ESP_GOTO_ON_ERROR(uart_driver_install(uart_config->uart_port_num, UART_HW_FIFO_LEN(uart_config->uart_port_num) + 1, 0, 0, NULL, 0), + err, TAG, "uart driver install failed"); +#else + ESP_GOTO_ON_ERROR(uart_driver_install(uart_config->uart_port_num, UART_FIFO_LEN + 1, 0, 0, NULL, 0), + err, TAG, "uart driver install failed"); +#endif + bus_uart->uart_port_num = uart_config->uart_port_num; + + // Configuration optimized for this scenario + // We only need to receive 1 byte at a time, so set the rx full threshold to 1 for faster response + // Normally, the RX timeout interrupt is not expected to trigger. Setting the timeout to 1 here is simply to ensure a fast response + ESP_GOTO_ON_ERROR(uart_set_rx_full_threshold(uart_config->uart_port_num, 1), err, TAG, "uart set rx full threshold failed"); + ESP_GOTO_ON_ERROR(uart_set_rx_timeout(uart_config->uart_port_num, 1), err, TAG, "uart set rx timeout failed"); + + bus_uart->bus_mutex = xSemaphoreCreateMutex(); + ESP_GOTO_ON_FALSE(bus_uart->bus_mutex, ESP_ERR_NO_MEM, err, TAG, "bus mutex creation failed"); + + bus_uart->base.del = onewire_bus_uart_del; + bus_uart->base.reset = onewire_bus_uart_reset; + bus_uart->base.write_bit = onewire_bus_uart_write_bit; + bus_uart->base.write_bytes = onewire_bus_uart_write_bytes; + bus_uart->base.read_bit = onewire_bus_uart_read_bit; + bus_uart->base.read_bytes = onewire_bus_uart_read_bytes; + *ret_bus = &bus_uart->base; + return ret; + +err: + if (bus_uart) { + onewire_bus_uart_destroy(bus_uart); + } + return ret; +} + +static esp_err_t onewire_bus_uart_destroy(onewire_bus_uart_obj_t *bus_uart) +{ + if (bus_uart->bus_mutex) { + vSemaphoreDelete(bus_uart->bus_mutex); + } + if (bus_uart->uart_port_num != UART_NUM_MAX) { + uart_driver_delete(bus_uart->uart_port_num); + } + if (bus_uart->data_gpio_num != GPIO_NUM_NC) { + gpio_reset_pin(bus_uart->data_gpio_num); + } + free(bus_uart); + return ESP_OK; +} + +static esp_err_t onewire_bus_uart_set_baud_rate(onewire_bus_uart_obj_t *bus_uart, uint32_t baud_rate) +{ + if (bus_uart->current_baud_rate == baud_rate) { + return ESP_OK; + } + esp_err_t ret = uart_set_baudrate(bus_uart->uart_port_num, baud_rate); + if (ret != ESP_OK) { + return ret; + } + bus_uart->current_baud_rate = baud_rate; + return ESP_OK; +} + +/** + * @brief Send and receive one byte over the UART bus. + * + * @note This function is used for: + * - reset pulse and presence detect + * - write or read one bit on the 1-wire bus + */ +static esp_err_t onewire_bus_uart_exchange_byte(onewire_bus_uart_obj_t *bus_uart, uint8_t tx_data, uint8_t *rx_data) +{ + esp_err_t ret = uart_flush_input(bus_uart->uart_port_num); + if (ret != ESP_OK) { + return ret; + } + if (uart_tx_chars(bus_uart->uart_port_num, (const char *)&tx_data, 1) != 1) { + return ESP_FAIL; + } + if (uart_read_bytes(bus_uart->uart_port_num, rx_data, 1, pdMS_TO_TICKS(ONEWIRE_UART_DEFAULT_TIMEOUT_MS)) != 1) { + return ESP_ERR_TIMEOUT; + } + return ESP_OK; +} + +static esp_err_t onewire_bus_uart_write_bit_nolock(onewire_bus_uart_obj_t *bus_uart, uint8_t tx_bit) +{ + uint8_t rx_data = 0; + const uint8_t tx_data = tx_bit ? ONEWIRE_UART_SLOT_TX_WRITE_1 : ONEWIRE_UART_SLOT_TX_WRITE_0; + esp_err_t ret = onewire_bus_uart_exchange_byte(bus_uart, tx_data, &rx_data); + if (ret != ESP_OK) { + return ret; + } + return (rx_data == tx_data) ? ESP_OK : ESP_ERR_INVALID_STATE; // Check if the sent data is corrupted +} + +static esp_err_t onewire_bus_uart_read_bit_nolock(onewire_bus_uart_obj_t *bus_uart, uint8_t *rx_bit) +{ + uint8_t rx_data = 0; + esp_err_t ret = onewire_bus_uart_exchange_byte(bus_uart, ONEWIRE_UART_SLOT_TX_READ, &rx_data); + if (ret != ESP_OK) { + return ret; + } + *rx_bit = (rx_data == ONEWIRE_UART_SLOT_RX_READ_1) ? 1 : 0; + return ESP_OK; +} + +////////////////////////////// implementation of onewire_bus_t functions ////////////////////////////// + +static esp_err_t onewire_bus_uart_del(onewire_bus_handle_t bus) +{ + onewire_bus_uart_obj_t *bus_uart = __containerof(bus, onewire_bus_uart_obj_t, base); + return onewire_bus_uart_destroy(bus_uart); +} + +static esp_err_t onewire_bus_uart_reset(onewire_bus_handle_t bus) +{ + onewire_bus_uart_obj_t *bus_uart = __containerof(bus, onewire_bus_uart_obj_t, base); + esp_err_t ret = ESP_OK; + uint8_t rx_data = 0; + + xSemaphoreTake(bus_uart->bus_mutex, portMAX_DELAY); + ESP_GOTO_ON_ERROR(onewire_bus_uart_set_baud_rate(bus_uart, ONEWIRE_UART_BAUD_RESET), err, TAG, "set reset baudrate failed"); + ESP_GOTO_ON_ERROR(onewire_bus_uart_exchange_byte(bus_uart, ONEWIRE_UART_RESET_TX, &rx_data), err, TAG, "create reset pulse failed"); + ESP_GOTO_ON_FALSE(rx_data != ONEWIRE_UART_RESET_RX_NO_DEVICE, ESP_ERR_NOT_FOUND, err, TAG, "no 1-wire device found"); + +err: + xSemaphoreGive(bus_uart->bus_mutex); + return ret; +} + +static esp_err_t onewire_bus_uart_write_bytes(onewire_bus_handle_t bus, const uint8_t *tx_data, uint8_t tx_data_size) +{ + onewire_bus_uart_obj_t *bus_uart = __containerof(bus, onewire_bus_uart_obj_t, base); + esp_err_t ret = ESP_OK; + + ESP_RETURN_ON_FALSE(tx_data && tx_data_size, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + xSemaphoreTake(bus_uart->bus_mutex, portMAX_DELAY); + ESP_GOTO_ON_ERROR(onewire_bus_uart_set_baud_rate(bus_uart, ONEWIRE_UART_BAUD_SLOT), err, TAG, "set slot baudrate failed"); + + for (uint8_t i = 0; i < tx_data_size; i++) { + uint8_t current = tx_data[i]; + for (int bit = 0; bit < 8; bit++) { + ESP_GOTO_ON_ERROR(onewire_bus_uart_write_bit_nolock(bus_uart, current & 0x01), err, TAG, "write bit failed"); + current >>= 1; + } + } + +err: + xSemaphoreGive(bus_uart->bus_mutex); + return ret; +} + +static esp_err_t onewire_bus_uart_read_bytes(onewire_bus_handle_t bus, uint8_t *rx_buf, size_t rx_buf_size) +{ + onewire_bus_uart_obj_t *bus_uart = __containerof(bus, onewire_bus_uart_obj_t, base); + esp_err_t ret = ESP_OK; + + ESP_RETURN_ON_FALSE(rx_buf && rx_buf_size, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + memset(rx_buf, 0, rx_buf_size); + xSemaphoreTake(bus_uart->bus_mutex, portMAX_DELAY); + ESP_GOTO_ON_ERROR(onewire_bus_uart_set_baud_rate(bus_uart, ONEWIRE_UART_BAUD_SLOT), err, TAG, "set slot baudrate failed"); + + for (size_t i = 0; i < rx_buf_size; i++) { + uint8_t current = 0; + for (int bit = 0; bit < 8; bit++) { + uint8_t rx_bit = 0; + ESP_GOTO_ON_ERROR(onewire_bus_uart_read_bit_nolock(bus_uart, &rx_bit), err, TAG, "read bit failed"); + if (rx_bit) { + current |= (1 << bit); + } + } + rx_buf[i] = current; + } + +err: + xSemaphoreGive(bus_uart->bus_mutex); + return ret; +} + +static esp_err_t onewire_bus_uart_write_bit(onewire_bus_handle_t bus, uint8_t tx_bit) +{ + onewire_bus_uart_obj_t *bus_uart = __containerof(bus, onewire_bus_uart_obj_t, base); + esp_err_t ret = ESP_OK; + + xSemaphoreTake(bus_uart->bus_mutex, portMAX_DELAY); + ESP_GOTO_ON_ERROR(onewire_bus_uart_set_baud_rate(bus_uart, ONEWIRE_UART_BAUD_SLOT), err, TAG, "set slot baudrate failed"); + ESP_GOTO_ON_ERROR(onewire_bus_uart_write_bit_nolock(bus_uart, tx_bit), err, TAG, "write bit failed"); +err: + xSemaphoreGive(bus_uart->bus_mutex); + return ret; +} + +static esp_err_t onewire_bus_uart_read_bit(onewire_bus_handle_t bus, uint8_t *rx_bit) +{ + onewire_bus_uart_obj_t *bus_uart = __containerof(bus, onewire_bus_uart_obj_t, base); + ESP_RETURN_ON_FALSE(rx_bit, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + esp_err_t ret = ESP_OK; + + xSemaphoreTake(bus_uart->bus_mutex, portMAX_DELAY); + ESP_GOTO_ON_ERROR(onewire_bus_uart_set_baud_rate(bus_uart, ONEWIRE_UART_BAUD_SLOT), err, TAG, "set slot baudrate failed"); + ESP_GOTO_ON_ERROR(onewire_bus_uart_read_bit_nolock(bus_uart, rx_bit), err, TAG, "read bit failed"); +err: + xSemaphoreGive(bus_uart->bus_mutex); + return ret; +} + diff --git a/esp32/managed_components/espressif__onewire_bus/src/onewire_crc.c b/esp32/managed_components/espressif__onewire_bus/src/onewire_crc.c new file mode 100644 index 0000000..81595b1 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/src/onewire_crc.c @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "onewire_crc.h" + +#define FAST_CRC 1 // define this to use the fast CRC table + +#if FAST_CRC + +static const uint8_t dalas_crc8_table[] = { + 0, 94, 188, 226, 97, 63, 221, 131, 194, 156, 126, 32, 163, 253, 31, 65, + 157, 195, 33, 127, 252, 162, 64, 30, 95, 1, 227, 189, 62, 96, 130, 220, + 35, 125, 159, 193, 66, 28, 254, 160, 225, 191, 93, 3, 128, 222, 60, 98, + 190, 224, 2, 92, 223, 129, 99, 61, 124, 34, 192, 158, 29, 67, 161, 255, + 70, 24, 250, 164, 39, 121, 155, 197, 132, 218, 56, 102, 229, 187, 89, 7, + 219, 133, 103, 57, 186, 228, 6, 88, 25, 71, 165, 251, 120, 38, 196, 154, + 101, 59, 217, 135, 4, 90, 184, 230, 167, 249, 27, 69, 198, 152, 122, 36, + 248, 166, 68, 26, 153, 199, 37, 123, 58, 100, 134, 216, 91, 5, 231, 185, + 140, 210, 48, 110, 237, 179, 81, 15, 78, 16, 242, 172, 47, 113, 147, 205, + 17, 79, 173, 243, 112, 46, 204, 146, 211, 141, 111, 49, 178, 236, 14, 80, + 175, 241, 19, 77, 206, 144, 114, 44, 109, 51, 209, 143, 12, 82, 176, 238, + 50, 108, 142, 208, 83, 13, 239, 177, 240, 174, 76, 18, 145, 207, 45, 115, + 202, 148, 118, 40, 171, 245, 23, 73, 8, 86, 180, 234, 105, 55, 213, 139, + 87, 9, 235, 181, 54, 104, 138, 212, 149, 203, 41, 119, 244, 170, 72, 22, + 233, 183, 85, 11, 136, 214, 52, 106, 43, 117, 151, 201, 74, 20, 246, 168, + 116, 42, 200, 150, 21, 75, 169, 247, 182, 232, 10, 84, 215, 137, 107, 53 +}; + +uint8_t onewire_crc8(uint8_t init_crc, uint8_t *input, size_t input_size) +{ + uint8_t crc = init_crc; + for (size_t i = 0; i < input_size; i ++) { + crc = dalas_crc8_table[crc ^ input[i]]; + } + return crc; +} + +#else // FAST_CRC + +uint8_t onewire_crc8(uint8_t init_crc, uint8_t *input, size_t input_size) +{ + uint8_t crc = init_crc; + for (size_t i = 0; i < input_size; i++) { + uint8_t byte = input[i]; + for (int j = 0; j < 8; j++) { + uint8_t x = (byte ^ crc) & 0x01; + crc >>= 1; + if (x != 0) { + crc ^= 0x8C; + } + byte >>= 1; + } + } + return crc; +} + +#endif // FAST_CRC diff --git a/esp32/managed_components/espressif__onewire_bus/src/onewire_device.c b/esp32/managed_components/espressif__onewire_bus/src/onewire_device.c new file mode 100644 index 0000000..cdf9d79 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/src/onewire_device.c @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include "esp_check.h" +#include "esp_log.h" +#include "onewire_bus.h" +#include "onewire_device.h" +#include "onewire_crc.h" +#include "onewire_cmd.h" + +static const char *TAG = "1-wire.device"; + +typedef struct onewire_device_iter_t { + onewire_bus_handle_t bus; + uint16_t last_discrepancy; + bool is_last_device; + uint8_t rom_number[sizeof(onewire_device_address_t)]; +} onewire_device_iter_t; + +esp_err_t onewire_new_device_iter(onewire_bus_handle_t bus, onewire_device_iter_handle_t *ret_iter) +{ + ESP_RETURN_ON_FALSE(bus && ret_iter, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + + onewire_device_iter_t *iter = calloc(1, sizeof(onewire_device_iter_t)); + ESP_RETURN_ON_FALSE(iter, ESP_ERR_NO_MEM, TAG, "no mem for device iterator"); + + iter->bus = bus; + *ret_iter = iter; + + return ESP_OK; +} + +esp_err_t onewire_del_device_iter(onewire_device_iter_handle_t iter) +{ + ESP_RETURN_ON_FALSE(iter, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + + free(iter); + + return ESP_OK; +} + +// Search algorithm inspired by https://www.analog.com/en/app-notes/1wire-search-algorithm.html +esp_err_t onewire_device_iter_get_next(onewire_device_iter_handle_t iter, onewire_device_t *dev) +{ + ESP_RETURN_ON_FALSE(iter && dev, ESP_ERR_INVALID_ARG, TAG, "invalid argument"); + // we don't treat iterator ending and ESP_ERR_NOT_FOUND as an error condition, so just print debug message here + if (iter->is_last_device) { + ESP_LOGD(TAG, "1-wire rom search finished"); + return ESP_ERR_NOT_FOUND; + } + onewire_bus_handle_t bus = iter->bus; + esp_err_t reset_result = onewire_bus_reset(bus); + if (reset_result == ESP_ERR_NOT_FOUND) { + ESP_LOGW(TAG, "reset bus failed: no devices found"); + return ESP_ERR_NOT_FOUND; + } + ESP_RETURN_ON_ERROR(reset_result, TAG, "reset bus failed"); + + // send rom search command and start search algorithm + ESP_RETURN_ON_ERROR(onewire_bus_write_bytes(bus, (uint8_t[]) { + ONEWIRE_CMD_SEARCH_NORMAL + }, 1), TAG, "send ONEWIRE_CMD_SEARCH_NORMAL failed"); + + uint8_t last_zero = 0; + for (uint16_t rom_bit_index = 0; rom_bit_index < sizeof(onewire_device_address_t) * 8; rom_bit_index ++) { + uint8_t rom_byte_index = rom_bit_index / 8; + uint8_t rom_bit_mask = 1 << (rom_bit_index % 8); // calculate byte index and bit mask in advance for convenience + + uint8_t rom_bit = 0; + uint8_t rom_bit_complement = 0; + ESP_RETURN_ON_ERROR(onewire_bus_read_bit(bus, &rom_bit), TAG, "read rom_bit error"); // write 1 bit to read from the bus + ESP_RETURN_ON_ERROR(onewire_bus_read_bit(bus, &rom_bit_complement), TAG, "read rom_bit_complement error"); // read a bit and its complement + + // No devices participating in search. + if (rom_bit && rom_bit_complement) { + ESP_LOGE(TAG, "no devices participating in search"); + return ESP_ERR_NOT_FOUND; + } + + uint8_t search_direction; + if (rom_bit != rom_bit_complement) { // There are only 0s or 1s in the bit of the participating ROM numbers. + search_direction = rom_bit; // just go ahead + } else { // There are both 0s and 1s in the current bit position of the participating ROM numbers. This is a discrepancy. + if (rom_bit_index < iter->last_discrepancy) { // current id bit is before the last discrepancy bit + search_direction = (iter->rom_number[rom_byte_index] & rom_bit_mask) ? 0x01 : 0x00; // follow previous way + } else { + search_direction = (rom_bit_index == iter->last_discrepancy) ? 0x01 : 0x00; // search for 0 bit first + } + + if (search_direction == 0) { // record zero's position in last zero + last_zero = rom_bit_index; + } + } + + if (search_direction == 1) { // set corresponding rom bit by search direction + iter->rom_number[rom_byte_index] |= rom_bit_mask; + } else { + iter->rom_number[rom_byte_index] &= ~rom_bit_mask; + } + + // set search direction + ESP_RETURN_ON_ERROR(onewire_bus_write_bit(bus, search_direction), TAG, "write direction bit error"); + } + + // if the search was successful + iter->last_discrepancy = last_zero; + if (iter->last_discrepancy == 0) { // last zero loops back to the first bit + iter->is_last_device = true; + } + + // check crc + ESP_RETURN_ON_FALSE(onewire_crc8(0, iter->rom_number, 7) == iter->rom_number[7], ESP_ERR_INVALID_CRC, TAG, "bad device crc"); + + // save the ROM number as the device address + memcpy(&dev->address, iter->rom_number, sizeof(onewire_device_address_t)); + dev->bus = bus; + ESP_LOGD(TAG, "new 1-Wire device found, address: %016llX", dev->address); + + return ESP_OK; +} diff --git a/esp32/managed_components/espressif__onewire_bus/test_apps/CMakeLists.txt b/esp32/managed_components/espressif__onewire_bus/test_apps/CMakeLists.txt new file mode 100644 index 0000000..61074aa --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/test_apps/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +set(COMPONENTS main) +project(onewire_bus_test) diff --git a/esp32/managed_components/espressif__onewire_bus/test_apps/main/CMakeLists.txt b/esp32/managed_components/espressif__onewire_bus/test_apps/main/CMakeLists.txt new file mode 100644 index 0000000..e176084 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/test_apps/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "onewire_bus_test.c" + INCLUDE_DIRS "." + PRIV_REQUIRES unity) diff --git a/esp32/managed_components/espressif__onewire_bus/test_apps/main/Kconfig.projbuild b/esp32/managed_components/espressif__onewire_bus/test_apps/main/Kconfig.projbuild new file mode 100644 index 0000000..0d7c27e --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/test_apps/main/Kconfig.projbuild @@ -0,0 +1,53 @@ +menu "Example Configuration" + + choice EXAMPLE_ONEWIRE_BACKEND + prompt "1-Wire backend" + default EXAMPLE_ONEWIRE_BACKEND_RMT if SOC_RMT_SUPPORTED + default EXAMPLE_ONEWIRE_BACKEND_UART if SOC_UART_SUPPORTED + help + Select which low-level backend the test app uses. + + config EXAMPLE_ONEWIRE_BACKEND_RMT + bool "RMT backend" + depends on SOC_RMT_SUPPORTED + + config EXAMPLE_ONEWIRE_BACKEND_UART + bool "UART backend" + depends on SOC_UART_SUPPORTED + endchoice + + config EXAMPLE_ONEWIRE_UART_PORT_NUM + int "UART port number" + depends on EXAMPLE_ONEWIRE_BACKEND_UART + default 1 + range 0 2 + help + UART port used by UART backend. + Valid UART port numbers differ across targets. + Note: UART0 is typically used by the console output. + + config EXAMPLE_ONEWIRE_BUS_GPIO + int "1-Wire data GPIO" + default 0 + range 0 63 + help + GPIO number used for the 1-Wire data line. + Valid GPIO numbers depend on the selected target; choose a + data-capable GPIO within this range for your chip. + + config EXAMPLE_ONEWIRE_ENABLE_INTERNAL_PULLUP + bool "Enable internal pull-up" + default y + help + Enable internal pull-up resistor on the GPIO pin used for the 1-Wire data line. + This is useful when no external pull-up resistor is present. + + config EXAMPLE_ONEWIRE_MAX_DEVICES + int "Maximum devices to search" + default 2 + range 1 64 + help + Maximum number of devices to search on the 1-Wire bus. + This test app performs SEARCH ROM to collect device addresses (64-bit ROM IDs). + +endmenu diff --git a/esp32/managed_components/espressif__onewire_bus/test_apps/main/idf_component.yml b/esp32/managed_components/espressif__onewire_bus/test_apps/main/idf_component.yml new file mode 100644 index 0000000..a769d09 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/test_apps/main/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + espressif/onewire_bus: + version: "*" + override_path: "../.." diff --git a/esp32/managed_components/espressif__onewire_bus/test_apps/main/onewire_bus_test.c b/esp32/managed_components/espressif__onewire_bus/test_apps/main/onewire_bus_test.c new file mode 100644 index 0000000..f2d2645 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/test_apps/main/onewire_bus_test.c @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include "esp_log.h" +#include "esp_check.h" +#include "sdkconfig.h" +#include "onewire_bus.h" +#include "onewire_device.h" + +static const char *TAG = "test-app"; + +#if CONFIG_EXAMPLE_ONEWIRE_ENABLE_INTERNAL_PULLUP +#define EXAMPLE_ONEWIRE_ENABLE_INTERNAL_PULLUP 1 +#else +#define EXAMPLE_ONEWIRE_ENABLE_INTERNAL_PULLUP 0 +#endif + +#if CONFIG_EXAMPLE_ONEWIRE_BACKEND_UART +#define EXAMPLE_ONEWIRE_UART_PORT_NUM CONFIG_EXAMPLE_ONEWIRE_UART_PORT_NUM +#endif + +#define EXAMPLE_ONEWIRE_BUS_GPIO CONFIG_EXAMPLE_ONEWIRE_BUS_GPIO +#define EXAMPLE_ONEWIRE_MAX_DEVICES CONFIG_EXAMPLE_ONEWIRE_MAX_DEVICES + +void app_main(void) +{ + // install new 1-wire bus + onewire_bus_handle_t bus; + onewire_bus_config_t bus_config = { + .bus_gpio_num = EXAMPLE_ONEWIRE_BUS_GPIO, + .flags = { + .en_pull_up = EXAMPLE_ONEWIRE_ENABLE_INTERNAL_PULLUP, + } + }; +#if CONFIG_EXAMPLE_ONEWIRE_BACKEND_RMT + onewire_bus_rmt_config_t rmt_config = { + .max_rx_bytes = 10, // 1byte ROM command + 8byte ROM number + 1byte device command + }; + ESP_ERROR_CHECK(onewire_new_bus_rmt(&bus_config, &rmt_config, &bus)); + ESP_LOGI(TAG, "1-Wire bus installed on GPIO%d by RMT backend", EXAMPLE_ONEWIRE_BUS_GPIO); +#elif CONFIG_EXAMPLE_ONEWIRE_BACKEND_UART + onewire_bus_uart_config_t uart_config = { + .uart_port_num = EXAMPLE_ONEWIRE_UART_PORT_NUM, + }; + ESP_ERROR_CHECK(onewire_new_bus_uart(&bus_config, &uart_config, &bus)); + ESP_LOGI(TAG, "1-Wire bus installed on GPIO%d by UART backend (UART%d)", + EXAMPLE_ONEWIRE_BUS_GPIO, EXAMPLE_ONEWIRE_UART_PORT_NUM); +#else +#error "No 1-Wire backend selected in menuconfig" +#endif + + int onewire_device_found = 0; + onewire_device_iter_handle_t iter = NULL; + onewire_device_t next_onewire_device; + esp_err_t search_result = ESP_OK; + + // create 1-wire device iterator, which is used for device search + ESP_ERROR_CHECK(onewire_new_device_iter(bus, &iter)); + ESP_LOGI(TAG, "Device iterator created, start searching..."); + do { + search_result = onewire_device_iter_get_next(iter, &next_onewire_device); + // found a new device + if (search_result == ESP_OK) { + ESP_LOGI(TAG, "Found a new device, address: %016llX", next_onewire_device.address); + onewire_device_found++; + if (onewire_device_found >= EXAMPLE_ONEWIRE_MAX_DEVICES) { + ESP_LOGI(TAG, "Max device number reached, stop searching..."); + break; + } + } + } while (search_result != ESP_ERR_NOT_FOUND); + ESP_ERROR_CHECK(onewire_del_device_iter(iter)); + ESP_LOGI(TAG, "Searching done, %d device(s) found", onewire_device_found); + + // delete the bus + ESP_LOGI(TAG, "Deleting bus..."); + ESP_ERROR_CHECK(onewire_bus_del(bus)); +} diff --git a/esp32/managed_components/espressif__onewire_bus/test_apps/pytest_onewire_bus.py b/esp32/managed_components/espressif__onewire_bus/test_apps/pytest_onewire_bus.py new file mode 100644 index 0000000..1fbe738 --- /dev/null +++ b/esp32/managed_components/espressif__onewire_bus/test_apps/pytest_onewire_bus.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: Unlicense OR CC0-1.0 +import pytest +from pytest_embedded import Dut + +@pytest.mark.generic +@pytest.mark.parametrize('config', ['rmt', 'uart'], indirect=True) +def test_onewire_bus(dut: Dut, config: str) -> None: + if config == 'rmt': + dut.expect_exact('test-app: 1-Wire bus installed on GPIO0 by RMT backend') + elif config == 'uart': + dut.expect_exact('test-app: 1-Wire bus installed on GPIO0 by UART backend (UART1)') + else: + raise ValueError(f'Unknown test config: {config}') + dut.expect_exact('test-app: Device iterator created, start searching') + dut.expect_exact('test-app: Searching done') diff --git a/esp32/partitions.csv b/esp32/partitions.csv new file mode 100644 index 0000000..231b9c4 --- /dev/null +++ b/esp32/partitions.csv @@ -0,0 +1,7 @@ +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0x9000, 0x5000 +otadata, data, ota, 0xe000, 0x2000 +phy_init, data, phy, 0x10000, 0x1000 + +ota_0, app, ota_0, 0x20000, 0x140000 +ota_1, app, ota_1, 0x160000, 0x140000 \ No newline at end of file diff --git a/esp32/version.txt b/esp32/version.txt new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/esp32/version.txt @@ -0,0 +1 @@ +1 \ No newline at end of file