Initial Ktor backend skeleton with ProtocolMessage DTO, REST and WebSocket

This commit is contained in:
2026-04-01 18:23:41 +03:00
parent dcf3d30295
commit b0a48e94bd
29 changed files with 638 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
package org.pavloveugene.iot.backend
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import org.pavloveugene.iot.backend.config.AppConfig
import org.pavloveugene.iot.backend.config.configureSerialization
import org.pavloveugene.iot.backend.config.configureWebSockets
import java.time.Duration
import org.pavloveugene.iot.backend.routes.*
fun main() {
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()
}
}.start(wait = true)
}

View File

@@ -0,0 +1,15 @@
package org.pavloveugene.iot.backend.config
import com.typesafe.config.ConfigFactory
object AppConfig {
private val config = ConfigFactory.load()
val serverHost: String = config.getString("ktor.host")
val serverPort: Int = config.getInt("ktor.port")
val appName: String = config.getString("app.name")
val apiPrefix: String = config.getString("api.prefix")
val wsPath: String = config.getString("ws.path")
}

View File

@@ -0,0 +1,29 @@
package org.pavloveugene.iot.backend.config
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.websocket.*
import kotlinx.serialization.json.Json
import java.time.Duration
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(
Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
}
)
}
}
fun Application.configureWebSockets() {
install(WebSockets) {
pingPeriod = Duration.ofSeconds(30)
timeout = Duration.ofSeconds(15)
maxFrameSize = Long.MAX_VALUE
masking = false
}
}

View File

@@ -0,0 +1,15 @@
package org.pavloveugene.iot.backend.dto
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable
data class BaseMessageDto(
val v: Int,
val id: String,
val type: String,
val ts: Long,
val deviceId: String,
val payload: JsonObject
)

View File

@@ -0,0 +1,15 @@
package org.pavloveugene.iot.backend.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.Serializable
@Serializable
data class ProtocolMessage(
val v: Int,
val id: String,
val type: String,
val ts: Long,
val deviceId: String,
val payload: JsonObject
)

View File

@@ -0,0 +1,10 @@
package org.pavloveugene.iot.backend.dto
import kotlinx.serialization.Serializable
@Serializable
data class TelemetryPayloadDto(
val voltage: Double? = null,
val current: Double? = null,
val power: Double? = null
)

View File

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

View File

@@ -0,0 +1,24 @@
package org.pavloveugene.iot.backend.routes
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
import org.pavloveugene.iot.backend.config.AppConfig
import org.pavloveugene.iot.backend.dto.ProtocolMessage
fun Route.protocolRoutes() {
route(AppConfig.apiPrefix+"/protocol") {
get("/health") {
call.respond(HttpStatusCode.OK, mapOf("status" to "ok"))
}
post("/message") {
val message = call.receive<ProtocolMessage>()
// TODO: обработка сообщения
println("Received message: $message")
call.respond(HttpStatusCode.Accepted, mapOf("received" to message.id))
}
}
}

View File

@@ -0,0 +1,48 @@
package org.pavloveugene.iot.backend.routes
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 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 java.time.Duration
fun Route.protocolWebSocket() {
val json = Json { prettyPrint = true }
webSocket(AppConfig.wsPath) {
send("Connected to IoT backend WebSocket")
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val text = frame.readText()
try {
val message = json.decodeFromString<ProtocolMessage>(text)
println("Received WS message: $message")
// Эхо обратно
val response = ProtocolMessage(
v = message.v,
id = message.id,
type = "response",
ts = System.currentTimeMillis(),
deviceId = message.deviceId,
payload = JsonObject(mapOf("status" to Json.encodeToJsonElement("ok")))
)
send(json.encodeToString(response))
} catch (e: Exception) {
send("Invalid message format: ${e.message}")
}
}
else -> {}
}
}
}
}

View File

@@ -0,0 +1,15 @@
package org.pavloveugene.iot.backend.routes
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureRouting() {
routing {
get("/api/v1/health") {
call.respond(mapOf(
"status" to "ok"
))
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,16 @@
ktor {
host = "0.0.0.0"
port = 8080
}
app {
name = "iot-backend"
}
api {
prefix = "/api/v1"
}
ws {
path = "/ws"
}

View File

@@ -0,0 +1,12 @@
ktor:
host: 0.0.0.0
port: 8080
app:
name: iot-backend
api:
prefix: /api/v1
ws:
path: /ws

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<document>
</document>