Initial Ktor backend skeleton with ProtocolMessage DTO, REST and WebSocket
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.pavloveugene.iot.backend.config
|
||||
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.websocket.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Duration
|
||||
|
||||
fun Application.configureSerialization() {
|
||||
install(ContentNegotiation) {
|
||||
json(
|
||||
Json {
|
||||
prettyPrint = true
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Application.configureWebSockets() {
|
||||
install(WebSockets) {
|
||||
pingPeriod = Duration.ofSeconds(30)
|
||||
timeout = Duration.ofSeconds(15)
|
||||
maxFrameSize = Long.MAX_VALUE
|
||||
masking = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.pavloveugene.iot.backend.dto
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ProtocolMessage(
|
||||
val v: Int,
|
||||
val id: String,
|
||||
val type: String,
|
||||
val ts: Long,
|
||||
val deviceId: String,
|
||||
val payload: JsonObject
|
||||
)
|
||||
@@ -0,0 +1,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
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.pavloveugene.iot.backend.routes
|
||||
|
||||
class ApiRoutes {
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.pavloveugene.iot.backend.routes
|
||||
|
||||
class WsRoutes {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.pavloveugene.iot.backend.services
|
||||
|
||||
class DeviceManager {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.pavloveugene.iot.backend.utils
|
||||
|
||||
class JsonUtils {
|
||||
}
|
||||
16
backend/src/main/resources/application.conf
Normal file
16
backend/src/main/resources/application.conf
Normal file
@@ -0,0 +1,16 @@
|
||||
ktor {
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
}
|
||||
|
||||
app {
|
||||
name = "iot-backend"
|
||||
}
|
||||
|
||||
api {
|
||||
prefix = "/api/v1"
|
||||
}
|
||||
|
||||
ws {
|
||||
path = "/ws"
|
||||
}
|
||||
12
backend/src/main/resources/application.yaml
Normal file
12
backend/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
ktor:
|
||||
host: 0.0.0.0
|
||||
port: 8080
|
||||
|
||||
app:
|
||||
name: iot-backend
|
||||
|
||||
api:
|
||||
prefix: /api/v1
|
||||
|
||||
ws:
|
||||
path: /ws
|
||||
4
backend/src/main/resources/logback.xml
Normal file
4
backend/src/main/resources/logback.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<document>
|
||||
|
||||
</document>
|
||||
Reference in New Issue
Block a user