Ajout Android CLI
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
export IOT_USER_TOKEN=demo-user-alice
|
||||||
|
export IOT_BACKEND_URL=http://127.0.0.1:5000
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm") version "2.4.20"
|
||||||
|
application
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass.set("iotmonitor.MainKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(17)
|
||||||
|
}
|
||||||
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = "iot-monitor-android"
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package iotmonitor
|
||||||
|
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logique réseau minimale. Réutilisable telle quelle dans une MainActivity.
|
||||||
|
*
|
||||||
|
* Règles de sécurité (modules 03/04) :
|
||||||
|
* - le secret de device passe par en-tête, jamais loggé ;
|
||||||
|
* - on n'imprime que l'identité et le résultat ;
|
||||||
|
* - les erreurs sont remontées sans détail d'implémentation.
|
||||||
|
*/
|
||||||
|
class BackendClient(
|
||||||
|
private val baseUrl: String,
|
||||||
|
private val http: HttpClient = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(5))
|
||||||
|
.build(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
/** Envoie une mesure en tant que device. Retourne le code de statut HTTP. */
|
||||||
|
fun ingest(deviceId: String, deviceSecret: String, measure: Measure): Int {
|
||||||
|
val body = buildIngestBody(measure)
|
||||||
|
val request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("$baseUrl/api/ingest"))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("X-Device-ID", deviceId)
|
||||||
|
.header("X-Device-Secret", deviceSecret)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = http.send(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
// Le secret n'est jamais imprimé.
|
||||||
|
println("[ingest] device=$deviceId -> HTTP ${response.statusCode()}")
|
||||||
|
return response.statusCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste les mesures d'un device en tant qu'utilisateur.
|
||||||
|
* L'autorisation est décidée par le backend (module 03) : 403 => refus.
|
||||||
|
*/
|
||||||
|
fun listMeasures(userToken: String, deviceId: String): List<Measure> {
|
||||||
|
val request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("$baseUrl/api/measures?device_id=$deviceId"))
|
||||||
|
.header("Authorization", "Bearer $userToken")
|
||||||
|
.GET()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = http.send(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
println("[measures] device=$deviceId -> HTTP ${response.statusCode()}")
|
||||||
|
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
return parseMeasures(response.body())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildIngestBody(measure: Measure): String =
|
||||||
|
"""{"device_id":"${measure.device_id}","timestamp":"${measure.timestamp}", "light_value":${measure.light_value}}"""
|
||||||
|
|
||||||
|
private fun parseMeasures(body: String): List<Measure> {
|
||||||
|
// Parse très simple, sans dépendance JSON : on extrait chaque objet
|
||||||
|
// entre accolades (ordre des champs indifférent), puis les valeurs
|
||||||
|
// par clé. Suffisant pour le format minimal du backend.
|
||||||
|
val objectRegex = Regex("""\{[^{}]*\}""")
|
||||||
|
val fieldRegex = Regex(""""([a-z_]+)":"?([^",\}]+)"?""")
|
||||||
|
|
||||||
|
return objectRegex.findAll(body).mapNotNull { objMatch ->
|
||||||
|
val fields = fieldRegex.findAll(objMatch.value)
|
||||||
|
.associate { m -> m.groupValues[1] to m.groupValues[2] }
|
||||||
|
val deviceId = fields["device_id"] ?: return@mapNotNull null
|
||||||
|
val timestamp = fields["timestamp"] ?: return@mapNotNull null
|
||||||
|
val light = fields["light_value"]?.toLongOrNull() ?: return@mapNotNull null
|
||||||
|
Measure(device_id = deviceId, timestamp = timestamp, light_value = light)
|
||||||
|
}.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package iotmonitor
|
||||||
|
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLI minimale : `ingest` ou `measures`.
|
||||||
|
*
|
||||||
|
* Aucun secret en dur : tout vient de l'environnement (ou renvoie une
|
||||||
|
* erreur lisible). L'état « connecté » est dérivé du code de statut HTTP.
|
||||||
|
*/
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
val command = args.firstOrNull()
|
||||||
|
?: run {
|
||||||
|
println("usage: ingest | measures --device <id>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val baseUrl = System.getenv("IOT_BACKEND_URL")
|
||||||
|
?: run {
|
||||||
|
println("erreur : IOT_BACKEND_URL manquant (ex. http://127.0.0.1:5000)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val client = BackendClient(baseUrl)
|
||||||
|
|
||||||
|
when (command) {
|
||||||
|
"ingest" -> runIngest(client)
|
||||||
|
"measures" -> runMeasures(client, args)
|
||||||
|
else -> println("commande inconnue : $command")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runIngest(client: BackendClient) {
|
||||||
|
val deviceId = System.getenv("IOT_DEVICE_ID")
|
||||||
|
?: run {
|
||||||
|
println("erreur : IOT_DEVICE_ID manquant")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val secret = System.getenv("IOT_DEVICE_SECRET")
|
||||||
|
?: run {
|
||||||
|
println("erreur : IOT_DEVICE_SECRET manquant (ne jamais mettre en dur)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val measure = Measure(
|
||||||
|
device_id = deviceId,
|
||||||
|
timestamp = Instant.now().toString(),
|
||||||
|
light_value = (0..1023).random().toLong(), // valeur brute simulée
|
||||||
|
)
|
||||||
|
|
||||||
|
val status = client.ingest(deviceId, secret, measure)
|
||||||
|
println("état : ${if (status in 200..299) "connecté (accepté)" else "refusé (HTTP $status)"}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runMeasures(client: BackendClient, args: Array<String>) {
|
||||||
|
val deviceId = args.getOrNull(args.indexOf("--device") + 1)
|
||||||
|
?: run {
|
||||||
|
println("usage: measures --device <id>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val token = System.getenv("IOT_USER_TOKEN")
|
||||||
|
?: run {
|
||||||
|
println("erreur : IOT_USER_TOKEN manquant")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val measures = client.listMeasures(token, deviceId)
|
||||||
|
if (measures.isEmpty()) {
|
||||||
|
println("état : aucune mesure retournée ou accès refusé")
|
||||||
|
} else {
|
||||||
|
println("état : connecté — ${measures.size} mesure(s)")
|
||||||
|
measures.forEach { m ->
|
||||||
|
println(" device=${m.device_id} time=${m.timestamp} value=${m.light_value}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package iotmonitor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modèle de données minimal (seulement les champs exposés par l'API,
|
||||||
|
* pas de sur-exposition — module 04).
|
||||||
|
*/
|
||||||
|
data class Measure(
|
||||||
|
val device_id: String,
|
||||||
|
val timestamp: String,
|
||||||
|
val light_value: Long,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user