Files
cacto 0eed35289c 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 платформы.
2026-08-13 11:12:29 +05:00

84 lines
2.4 KiB
Go

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