ESP32 Emulator: веб-морда контроллера гроубокса поверх MQTT
Go-сервис без внешних фронтенд-зависимостей: embedded HTML-страница
с SVG-изображением платы (датчик + вентилятор + свет + помпа) и
Server-Sent Events для live-обновления. Слушает devices/+/commands
и реально отвечает ack'ом, публикует devices/{id}/telemetry —
протокол зеркалит ingest-service/device-control-service основного
репозитория, общего кода между репозиториями нет. Дефолтные
external_id (sensor-1/fan-1/light-1/pump-1) совпадают с
DemoGrowboxSeeder платформы.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Command emulator serves a small web UI that stands in for an ESP32
|
||||
// growbox controller: a sensor slot (publishes temperature/humidity
|
||||
// telemetry) and three actuator slots (fan/light/pump — react live to
|
||||
// commands the platform sends over MQTT). See README.md for the protocol
|
||||
// this speaks and why it exists as a standalone tool.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.cactoz.su/cacto/home_automation_emulator/internal/httpserver"
|
||||
"git.cactoz.su/cacto/home_automation_emulator/internal/mqttclient"
|
||||
"git.cactoz.su/cacto/home_automation_emulator/internal/state"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
|
||||
brokerURL := getEnv("MQTT_BROKER_URL", "tcp://localhost:1883")
|
||||
httpAddr := ":" + getEnv("HTTP_PORT", "8091")
|
||||
|
||||
store := state.NewStore()
|
||||
|
||||
mqttClient, err := mqttclient.Connect(mqttclient.Config{
|
||||
BrokerURL: brokerURL,
|
||||
ClientID: "esp32-emulator",
|
||||
}, logger)
|
||||
if err != nil {
|
||||
logger.Error("mqtt connect failed", "broker", brokerURL, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer mqttClient.Close()
|
||||
|
||||
if err := mqttClient.SubscribeCommands(func(externalID, action string, level float64, hasLevel bool) {
|
||||
reported, ok := store.ApplyCommand(externalID, action, level, hasLevel)
|
||||
if !ok {
|
||||
logger.Warn("command for unknown device ignored", "device_id", externalID, "action", action)
|
||||
return
|
||||
}
|
||||
logger.Info("command applied", "device_id", externalID, "action", action)
|
||||
if err := mqttClient.PublishAck(externalID, reported); err != nil {
|
||||
logger.Error("publish ack failed", "device_id", externalID, "error", err)
|
||||
}
|
||||
}); err != nil {
|
||||
logger.Error("mqtt subscribe failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: httpAddr,
|
||||
Handler: httpserver.New(store, mqttClient, logger),
|
||||
}
|
||||
|
||||
go func() {
|
||||
logger.Info("http server listening", "addr", httpAddr, "mqtt_broker", brokerURL)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
logger.Error("http server failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
<-ctx.Done()
|
||||
|
||||
logger.Info("shutting down")
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(shutdownCtx)
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
Reference in New Issue
Block a user