updated and tested till end of lesson 4
This commit is contained in:
+39
-58
@@ -2,16 +2,16 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/eclipse/paho.golang/autopaho"
|
||||
"github.com/eclipse/paho.golang/paho"
|
||||
"example.com/mqtt5-learning/internal/config"
|
||||
"example.com/mqtt5-learning/internal/mqtt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -21,71 +21,52 @@ func main() {
|
||||
}
|
||||
|
||||
func run() error {
|
||||
cfg, err := config.Load(os.Args[1:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
user := os.Getenv("TEST_MQTT5_MQTT_USERNAME")
|
||||
pass := os.Getenv("TEST_MQTT5_MQTT_PASSWORD")
|
||||
base := "lab/v1/devices/" + cfg.MQTT.DeviceID
|
||||
inbox := make(chan mqtt.Message, 16)
|
||||
|
||||
if user == "" || pass == "" {
|
||||
return fmt.Errorf("TEST_MQTT5_MQTT_USERNAME and TEST_MQTT5_MQTT_PASSWORD environment variables not set")
|
||||
}
|
||||
|
||||
broker, err := url.Parse("mqtt://127.0.0.1:18883")
|
||||
manager, err := mqtt.Start(ctx, cfg, []string{base + "/telemetry", base + "/responses"}, inbox)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse broker url: %w", err)
|
||||
return fmt.Errorf("failed to start MQTT client: %w", err)
|
||||
}
|
||||
defer mqtt.Stop(manager)
|
||||
|
||||
cfg := autopaho.ClientConfig{
|
||||
ServerUrls: []*url.URL{broker},
|
||||
KeepAlive: 20,
|
||||
CleanStartOnInitialConnection: true,
|
||||
SessionExpiryInterval: 0,
|
||||
ConnectUsername: user,
|
||||
ConnectPassword: []byte(pass),
|
||||
OnConnectionUp: func(_ *autopaho.ConnectionManager, ack *paho.Connack) {
|
||||
log.Printf("connected: session_present=%t reason=0x%02x", ack.SessionPresent, ack.ReasonCode)
|
||||
},
|
||||
OnConnectionDown: func() bool {
|
||||
log.Print("connection down; AutoPaho will retry")
|
||||
return true
|
||||
},
|
||||
OnConnectError: func(err error) {
|
||||
log.Printf("connect attempt failed: %v", err)
|
||||
},
|
||||
ClientID: "lab-backend",
|
||||
OnClientError: func(err error) {
|
||||
log.Printf("client error: %v", err)
|
||||
},
|
||||
OnServerDisconnect: func(packet *paho.Disconnect) {
|
||||
log.Printf("server disconnected: reason=0x%02x", packet.ReasonCode)
|
||||
},
|
||||
}
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
manager, err := autopaho.NewConnection(ctx, cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create connection manager: %w", err)
|
||||
}
|
||||
runID := time.Now().UnixNano()
|
||||
sequence := 0
|
||||
|
||||
defer func() {
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := manager.Disconnect(shutdownCtx); err != nil {
|
||||
log.Printf("shutdown did not finish within deadline: %v", err)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Print("shutting down")
|
||||
return nil
|
||||
case msg := <-inbox:
|
||||
log.Printf("received message on topic %s retained=%t payload=%s", msg.Topic, msg.Retain, string(msg.Payload))
|
||||
|
||||
case <-ticker.C:
|
||||
sequence++
|
||||
payload, err := json.Marshal(struct {
|
||||
ID string `json:"id"`
|
||||
Command string `json:"command"`
|
||||
}{
|
||||
ID: fmt.Sprintf("ping-%d-%d", runID, sequence), Command: "ping",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
if err := mqtt.Publish(ctx, manager, base+"/commands", payload); err != nil {
|
||||
log.Printf("command publication failed or outcome uncertain: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
startup, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
err = manager.AwaitConnection(startup)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to broker: %w", err)
|
||||
}
|
||||
|
||||
log.Print("connected to broker; pres Ctrl+C to stop")
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"example.com/mqtt5-learning/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg, err := config.Load(os.Args[1:])
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Printf("client_id=%s device_id=%s username=%s timeout=%s password=[redacted]",
|
||||
cfg.MQTT.ClientID, cfg.MQTT.DeviceID, cfg.MQTT.Username, cfg.ConnectTimeout())
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"example.com/mqtt5-learning/internal/config"
|
||||
mqtt "example.com/mqtt5-learning/internal/mqtt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Convert a returned application error into a visible nonzero process exit.
|
||||
if err := run(); err != nil {
|
||||
log.Print(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
// Load settings and bind cancellation to Ctrl+C or the container stop signal.
|
||||
cfg, err := config.Load(os.Args[1:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
// Build this device's topic namespace and bounded receive queue.
|
||||
base := "lab/v1/devices/" + cfg.MQTT.DeviceID
|
||||
inbox := make(chan mqtt.Message, 16)
|
||||
manager, err := mqtt.Start(ctx, cfg, []string{base + "/commands"}, inbox)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mqtt.Stop(manager)
|
||||
// Heartbeats are periodic; command handling remains active between ticks.
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
// Handle cancellation, telemetry publication, and inbound commands together.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
payload, err := json.Marshal(struct {
|
||||
DeviceID string `json:"device_id"`
|
||||
Time string `json:"time"`
|
||||
}{cfg.MQTT.DeviceID, time.Now().UTC().Format(time.RFC3339)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mqtt.Publish(ctx, manager, base+"/telemetry", payload); err != nil {
|
||||
log.Printf("heartbeat: %v", err)
|
||||
}
|
||||
case message := <-inbox:
|
||||
// Decode into a local command value only after rejecting retained messages.
|
||||
var command struct {
|
||||
ID string `json:"id"`
|
||||
Command string `json:"command"`
|
||||
}
|
||||
if message.Retain {
|
||||
log.Print("ignoring retained command")
|
||||
continue
|
||||
}
|
||||
if err := json.Unmarshal(message.Payload, &command); err != nil || command.ID == "" || command.Command != "ping" {
|
||||
log.Print("ignoring malformed or unsupported command")
|
||||
continue
|
||||
}
|
||||
// Echo the command ID so the backend can associate this pong with its ping.
|
||||
response, err := json.Marshal(struct {
|
||||
ID string `json:"id"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Status string `json:"status"`
|
||||
}{command.ID, cfg.MQTT.DeviceID, "pong"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mqtt.Publish(ctx, manager, base+"/responses", response); err != nil {
|
||||
log.Printf("response: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user