HB, OTA etc

This commit is contained in:
2026-04-22 20:11:55 +03:00
parent 4100931deb
commit cb1014c950
76 changed files with 3157 additions and 232 deletions

BIN
backend/backend.zip Normal file

Binary file not shown.

View File

@@ -20,6 +20,7 @@ dependencies {
implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("org.mariadb.jdbc:mariadb-java-client:3.3.3")
implementation("com.zaxxer:HikariCP:5.1.0")
implementation("io.ktor:ktor-server-compression:2.3.7")
testImplementation(kotlin("test"))
}
@@ -44,3 +45,48 @@ tasks.processResources {
into("db/migration")
}
}
tasks.register("deploy") {
dependsOn("jar")
doLast {
val jarFile = file("build/libs/backend.jar")
if (!jarFile.exists()) {
throw GradleException("Jar not found: ${jarFile.absolutePath}")
}
fun runCommand(vararg cmd: String) {
println("Running: ${cmd.joinToString(" ")}")
val process = ProcessBuilder(*cmd)
.inheritIO()
.start()
val exitCode = process.waitFor()
if (exitCode != 0) {
throw GradleException("Command failed: ${cmd.joinToString(" ")}")
}
}
// upload
runCommand(
"scp",
jarFile.absolutePath,
"home-iot:/tmp/backend.jar"
)
// deploy
runCommand(
"ssh",
"home-iot",
"sudo mv /tmp/backend.jar /opt/iot-backend/app.jar && sudo systemctl restart iot-backend"
)
runCommand("ssh",
"home-iot",
"systemctl status iot-backend --no-pager")
println("Deploy completed")
}
}

View File

@@ -15,42 +15,32 @@ import org.pavloveugene.iot.backend.db.Migration
import org.pavloveugene.iot.backend.db.runMigrations
import java.time.Duration
import org.pavloveugene.iot.backend.routes.*
import org.pavloveugene.iot.backend.services.executeCleanup
import org.pavloveugene.iot.backend.services.runNormalizeLoop
import org.pavloveugene.iot.backend.services.startKtorServer
import kotlin.system.exitProcess
fun main() {
Database.dataSource
fun main(args: Array<String>) {
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)
}

View File

@@ -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<Any?> = emptyList()): Int {
dataSource.connection.use { conn ->
conn.prepareStatement(sql).use { stmt ->
params.forEachIndexed { i, p ->
stmt.setObject(i + 1, p)
}
return stmt.executeUpdate()
}
}
}
fun queryOne(sql: String, params: List<Any?> = emptyList()): Map<String, Any?>? {
dataSource.connection.use { conn ->
conn.prepareStatement(sql).use { stmt ->
params.forEachIndexed { i, p ->
stmt.setObject(i + 1, p)
}
val rs = stmt.executeQuery()
if (!rs.next()) return null
val meta = rs.metaData
val map = mutableMapOf<String, Any?>()
for (i in 1..meta.columnCount) {
map[meta.getColumnLabel(i)] = rs.getObject(i)
}
return map
}
}
}
fun query(sql: String, params: List<Any?> = emptyList()): List<Map<String, Any?>> {
dataSource.connection.use { conn ->
conn.prepareStatement(sql).use { stmt ->
params.forEachIndexed { i, p ->
stmt.setObject(i + 1, p)
}
val rs = stmt.executeQuery()
val meta = rs.metaData
val result = mutableListOf<Map<String, Any?>>()
while (rs.next()) {
val map = mutableMapOf<String, Any?>()
for (i in 1..meta.columnCount) {
map[meta.getColumnLabel(i)] = rs.getObject(i)
}
result.add(map)
}
return result
}
}
}
fun insertAndReturnId(sql: String, params: List<Any?> = emptyList()): Long {
dataSource.connection.use { conn ->
conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS).use { stmt ->
params.forEachIndexed { i, p ->
stmt.setObject(i + 1, p)
}
stmt.executeUpdate()
val rs = stmt.generatedKeys
if (rs.next()) {
return rs.getLong(1)
} else {
error("No generated key")
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
package org.pavloveugene.iot.backend.dto
import kotlinx.serialization.Serializable
@Serializable
data class EventDto (
val type: String
)

View File

@@ -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)
}
}

View File

@@ -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") {
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<MessageDto>()
protocolService.handleMessage(msg)
call.respond(HttpStatusCode.Accepted)
call.respond("ok")
}
}
}

View File

@@ -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")
}
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -1,4 +0,0 @@
package org.pavloveugene.iot.backend.routes
class WsRoutes {
}

View File

@@ -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")
}

View File

@@ -0,0 +1,8 @@
package org.pavloveugene.iot.backend.services
import io.ktor.websocket.WebSocketSession
data class DeviceConnection(
val session: WebSocketSession,
var lastSeen: Long
)

View File

@@ -0,0 +1,18 @@
package org.pavloveugene.iot.backend.services
import io.ktor.websocket.WebSocketSession
import java.util.concurrent.ConcurrentHashMap
object DeviceConnections {
private val map = ConcurrentHashMap<UInt, DeviceConnection>()
fun register(deviceId: UInt, connection: DeviceConnection) {
map[deviceId] = connection
}
fun unregister(deviceId: UInt) {
map.remove(deviceId)
}
fun get(deviceId: UInt): DeviceConnection? = map[deviceId]
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()

BIN
contract/ingest.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "contract/ingest/command.schema.json",
"type": "object",
"required": ["t"],
"additionalProperties": false,
"properties": {
"t": {
"type": "string",
"enum": ["fw"]
}
},
"allOf": [
{
"if": {
"properties": { "t": { "const": "fw" } }
},
"then": {
"required": ["u", "s"],
"properties": {
"u": { "type": "string", "format": "uri" },
"s": { "type": "string", "pattern": "^[a-f0-9]{64}$" }
}
}
}
]
}

View File

@@ -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",
"required": ["type"],
"properties": {
"type": {
"const": "event"
},
"payload": {
"type": "object",
"required": ["name"],
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "Event identifier, e.g. device.overheat"
"enum": ["hb"]
}
},
"severity": {
"type": "string",
"enum": ["debug", "info", "warn", "error", "fatal"]
},
"message": {
"type": "string"
},
"data": {
"type": "object",
"additionalProperties": true
}
}
}
}
}
]
"additionalProperties": false
}

View File

@@ -0,0 +1,20 @@
CREATE TABLE telemetry_data (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
device_id BIGINT NOT NULL,
ts BIGINT NOT NULL,
metric VARCHAR(32) NOT NULL,
source VARCHAR(64) NOT NULL,
value DOUBLE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_device_metric_ts (device_id, metric, ts),
INDEX idx_ts (ts),
FOREIGN KEY (device_id) REFERENCES devices(id)
);
CREATE INDEX idx_processed ON telemetry(processed);

View File

@@ -0,0 +1,2 @@
CREATE INDEX idx_telemetry_processed_id
ON telemetry (processed, id);

View File

@@ -0,0 +1,16 @@
alter TABLE telemetry
add defective boolean default false,
add defective_reason varchar(64);
alter TABLE telemetry_data ADD
unit varchar (16);
create table units (
unit varchar(16) not null,
target_unit varchar(16) not null,
multiplier double not null,
primary key (unit)
);
insert into units (unit, target_unit, multiplier)
values ('c_x100', 'c', 0.01);

View File

@@ -0,0 +1,24 @@
create table firmware (
id int not null auto_increment,
device_id bigint not null,
version int not null,
path varchar(4096) not null,
sha256 char(64),
size int,
uploaded_at timestamp not null default current_timestamp,
primary key (id),
foreign key (device_id) references devices(id),
unique(device_id, version)
);
create table firmware_updates (
id int not null auto_increment,
firmware_id int not null,
status varchar(32) not null, -- pending / sent / applied / failed
requested_at timestamp not null default current_timestamp,
applied_at timestamp null,
primary key (id),
foreign key (firmware_id) references firmware(id)
);

View File

@@ -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)

View File

@@ -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

BIN
esp32/esp32.zip Normal file

Binary file not shown.

View File

@@ -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")

85
esp32/main/ds18b20.cpp Normal file
View File

@@ -0,0 +1,85 @@
#include "ds18b20.h"
extern "C" {
#include "onewire_bus.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
}
static const char *TAG = "ds18b20";
static onewire_bus_handle_t bus;
uint8_t ds_crc8(const uint8_t *data, int len)
{
uint8_t crc = 0;
for (int i = 0; i < len; i++) {
uint8_t inbyte = data[i];
for (int j = 0; j < 8; j++) {
uint8_t mix = (crc ^ inbyte) & 0x01;
crc >>= 1;
if (mix) crc ^= 0x8C;
inbyte >>= 1;
}
}
return crc;
}
void ds18b20_init(gpio_num_t pin)
{
onewire_bus_config_t bus_config = {};
bus_config.bus_gpio_num = pin;
onewire_bus_rmt_config_t rmt_config = {};
rmt_config.max_rx_bytes = 10;
ESP_ERROR_CHECK(onewire_new_bus_rmt(&bus_config, &rmt_config, &bus));
ESP_LOGI(TAG, "OneWire bus initialized");
}
float ds18b20_read()
{
uint8_t data[9];
uint8_t buf[4] = {0x4E, 0, 0, 0x3F}; // 10-bit (0x3F)
// reset
ESP_ERROR_CHECK(onewire_bus_reset(bus));
// SKIP ROM (один датчик)
uint8_t cmd = 0xCC;
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
//ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, buf, 4));
// CONVERT T
cmd = 0x44;
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
vTaskDelay(pdMS_TO_TICKS(750));
// reset снова
ESP_ERROR_CHECK(onewire_bus_reset(bus));
// SKIP ROM
cmd = 0xCC;
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
// READ SCRATCHPAD
cmd = 0xBE;
ESP_ERROR_CHECK(onewire_bus_write_bytes(bus, &cmd, 1));
ESP_ERROR_CHECK(onewire_bus_read_bytes(bus, data, 9));
ESP_LOGI(TAG,
"RAW: %02X %02X %02X %02X %02X %02X %02X %02X | CRC %02X",
data[0], data[1], data[2], data[3],
data[4], data[5], data[6], data[7], data[8]);
uint8_t crc = ds_crc8(data, 8);
if (crc != data[8]) {
ESP_LOGW(TAG, "CRC error");
}
int16_t raw = (data[1] << 8) | data[0];
return raw / 16.0f;
}

6
esp32/main/ds18b20.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
#include "driver/gpio.h"
void ds18b20_init(gpio_num_t pin);
float ds18b20_read();

18
esp32/main/fw_command.cpp Normal file
View File

@@ -0,0 +1,18 @@
//
// Created by eugene on 22.04.2026.
//
#include "fw_command.h"
#include "json_utils.h"
#include <cstring>
bool parse_fw_command(const char *msg, fw_cmd_t *out) {
if (!strstr(msg, "\"t\":\"c\"")) return false;
if (!strstr(msg, "\"t\":\"fw\"")) return false;
if (!json_get_string(msg, "\"u\"", out->url, sizeof(out->url))) return false;
if (!json_get_string(msg, "\"s\"", out->sha256, sizeof(out->sha256))) return false;
out->is_fw = true;
return true;
}

9
esp32/main/fw_command.h Normal file
View File

@@ -0,0 +1,9 @@
#pragma once
typedef struct {
bool is_fw;
char url[256];
char sha256[65]; // 64 + \0
} fw_cmd_t;
bool parse_fw_command(const char *msg, fw_cmd_t *out);

20
esp32/main/gpio_init.cpp Normal file
View File

@@ -0,0 +1,20 @@
#include "gpio_init.h"
#include "hal/gpio_types.h"
#include "driver/gpio.h"
#define PIN_OUT GPIO_NUM_22
void init_gpio()
{
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << PIN_OUT),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);
gpio_set_level(PIN_OUT, 1); // HIGH ≈ 3.3В
}

3
esp32/main/gpio_init.h Normal file
View File

@@ -0,0 +1,3 @@
#pragma once
void init_gpio();

View File

@@ -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 <time.h>
}
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);
}

View File

@@ -0,0 +1,3 @@
#pragma once
void heartbeat_task_start(void);

View File

@@ -15,3 +15,4 @@ dependencies:
# # All dependencies of `main` are public by default.
# public: true
espressif/esp_websocket_client: '*'
espressif/onewire_bus: '*'

30
esp32/main/json_utils.cpp Normal file
View File

@@ -0,0 +1,30 @@
#include <cstring>
#include <cstddef>
#include "json_utils.h"
bool json_get_string(const char *json, const char *key, char *out, size_t out_size) {
const char *k = strstr(json, key);
if (!k) return false;
const char *start = strchr(k, ':');
if (!start) return false;
start++;
// найти начало строки (первую кавычку)
while (*start && *start != '\"') start++;
if (!*start) return false;
start++; // после "
const char *end = strchr(start, '\"');
if (!end) return false;
size_t len = end - start;
if (len >= out_size) return false;
memcpy(out, start, len);
out[len] = '\0';
return true;
}

4
esp32/main/json_utils.h Normal file
View File

@@ -0,0 +1,4 @@
#pragma once
#include <cstddef>
bool json_get_string(const char *json, const char *key, char *out, size_t out_size);

View File

@@ -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();
}

View File

@@ -1,10 +1,15 @@
#include "protocol.h"
#include "ws.h"
#include <stdio.h>
#include <stdarg.h>
#include <inttypes.h>
#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,13 +35,14 @@ 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,
(unsigned long)protocol_next_id(),
(long long)ts,
(unsigned long)device_id
);
@@ -52,11 +58,12 @@ int build_telemetry(
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],
values[i*2 + 0], // delta time
values[i*2 + 1],
(i < count - 1) ? "," : ""
);
if (pos < 0) return -1;
@@ -82,7 +89,8 @@ __attribute__((deprecated))
const char* unit,
const char* source,
int64_t m_ts_ms
) {
)
{
int pos = 0;
// header
@@ -105,17 +113,20 @@ __attribute__((deprecated))
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,7 +149,8 @@ int build_event(
const char* name,
const char* severity,
const char* message
) {
)
{
int pos = 0;
// header
@@ -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;
}
@@ -173,3 +187,29 @@ int build_event(
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();
}

View File

@@ -2,6 +2,8 @@
#include <stdint.h>
#include <stddef.h>
#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

View File

@@ -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++) {

View File

@@ -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);
void ringbuf_copy(const ringbuf_t *rb, sample_t *out);

View File

@@ -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);
vTaskDelay(pdMS_TO_TICKS(100)); // 10 Гц
const TickType_t period = pdMS_TO_TICKS(6000);
TickType_t last_wake = xTaskGetTickCount();
while (1) {
int64_t now_ms = esp_timer_get_time() / 1000;
float temp = ds18b20_read();
ringbuf_push(&rb, now_ms, (int)(temp * 100));
vTaskDelayUntil(&last_wake, period);// 6 секунд
}
}
void sampler_task_start() {
ringbuf_init(&rb);
adc_reader_init();
ds18b20_init(GPIO_NUM_27); // новый драйвер
xTaskCreate(
sampler_task,
"sampler",
2048,
4096,
NULL,
5,
NULL

View File

@@ -5,54 +5,88 @@
#include <stdio.h>
#include <time.h>
#include "ws.h"
extern "C" {
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
}
static uint32_t 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,
last_send_ts,
1, // device_id (пока захардкожен)
"v", // metric
"adc35", // source
"raw", // unit
data,
RINGBUF_SIZE
"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 {
}
else
{
printf("%s\n", buf); // fallback
}
} else {
}
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",

View File

@@ -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(); // Установка времени

View File

@@ -3,6 +3,7 @@
#include "esp_log.h"
#include <string.h>
#include "sdkconfig.h"
#include "fw_command.h"
static const char* TAG = "WS";
@@ -14,8 +15,8 @@ static void ws_event_handler(void *handler_args,
int32_t event_id,
void* event_data)
{
switch (event_id) {
switch (event_id)
{
case WEBSOCKET_EVENT_CONNECTED:
connected = true;
ESP_LOGI(TAG, "Connected");
@@ -31,9 +32,34 @@ static void ws_event_handler(void *handler_args,
ESP_LOGE(TAG, "Error");
break;
case WEBSOCKET_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);
}
}

View File

@@ -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

View File

@@ -0,0 +1 @@
d709015ba466095259228521cf1bad9c0cdaaa42a92ea5d9c88ec6c28ae89e9b

View File

@@ -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

View File

@@ -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"}]}

View File

@@ -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})

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -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)

View File

@@ -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

View File

@@ -0,0 +1,96 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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

View File

@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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

View File

@@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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

View File

@@ -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

View File

@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#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

View File

@@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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

View File

@@ -0,0 +1,43 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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

View File

@@ -0,0 +1,97 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#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

View File

@@ -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);
}

View File

@@ -0,0 +1,521 @@
/*
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#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(&copy_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;
}

View File

@@ -0,0 +1,298 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdlib.h>
#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;
}

View File

@@ -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

View File

@@ -0,0 +1,124 @@
/*
* SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <string.h>
#include <stdbool.h>
#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;
}

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
idf_component_register(SRCS "onewire_bus_test.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity)

View File

@@ -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

View File

@@ -0,0 +1,4 @@
dependencies:
espressif/onewire_bus:
version: "*"
override_path: "../.."

View File

@@ -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));
}

View File

@@ -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')

7
esp32/partitions.csv Normal file
View File

@@ -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
1 # Name Type SubType Offset Size
2 nvs data nvs 0x9000 0x5000
3 otadata data ota 0xe000 0x2000
4 phy_init data phy 0x10000 0x1000
5 ota_0 app ota_0 0x20000 0x140000
6 ota_1 app ota_1 0x160000 0x140000

1
esp32/version.txt Normal file
View File

@@ -0,0 +1 @@
1