191 lines
5.6 KiB
Go
191 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/coder/websocket/wsjson"
|
|
|
|
"realtime-gateway/internal/model"
|
|
)
|
|
|
|
func main() {
|
|
url := flag.String("url", "ws://127.0.0.1:8080/ws", "gateway websocket url")
|
|
token := flag.String("token", "dev-producer-token", "producer token")
|
|
channelID := flag.String("channel-id", "", "channel id to join before publish")
|
|
deviceID := flag.String("device-id", "child-001", "target device id")
|
|
groupID := flag.String("group-id", "", "target group id")
|
|
sourceID := flag.String("source-id", "mock-producer-001", "source id")
|
|
mode := flag.String("mode", "mock", "source mode")
|
|
topic := flag.String("topic", "telemetry.location", "publish topic")
|
|
lat := flag.Float64("lat", 31.2304, "start latitude")
|
|
lng := flag.Float64("lng", 121.4737, "start longitude")
|
|
speed := flag.Float64("speed", 1.2, "speed value")
|
|
accuracy := flag.Float64("accuracy", 6, "accuracy value")
|
|
bpm := flag.Int("bpm", 120, "heart rate value when topic is telemetry.heart_rate")
|
|
confidence := flag.Float64("confidence", 0, "heart rate confidence when topic is telemetry.heart_rate")
|
|
interval := flag.Duration("interval", time.Second, "publish interval")
|
|
count := flag.Int("count", 0, "number of messages to send, 0 means unlimited")
|
|
flag.Parse()
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
|
|
conn, _, err := websocket.Dial(ctx, *url, nil)
|
|
if err != nil {
|
|
log.Fatalf("dial gateway: %v", err)
|
|
}
|
|
defer conn.Close(websocket.StatusNormalClosure, "producer closed")
|
|
|
|
var welcome model.ServerMessage
|
|
if err := wsjson.Read(ctx, conn, &welcome); err != nil {
|
|
log.Fatalf("read welcome: %v", err)
|
|
}
|
|
log.Printf("connected: session=%s", welcome.SessionID)
|
|
|
|
authReq := model.ClientMessage{
|
|
Type: "authenticate",
|
|
Role: model.RoleProducer,
|
|
Token: *token,
|
|
}
|
|
if *channelID != "" {
|
|
authReq.Type = "join_channel"
|
|
authReq.ChannelID = *channelID
|
|
}
|
|
if err := wsjson.Write(ctx, conn, authReq); err != nil {
|
|
log.Fatalf("send authenticate: %v", err)
|
|
}
|
|
|
|
var authResp model.ServerMessage
|
|
if err := wsjson.Read(ctx, conn, &authResp); err != nil {
|
|
log.Fatalf("read authenticate response: %v", err)
|
|
}
|
|
if authResp.Type == "error" {
|
|
log.Fatalf("authenticate failed: %s", authResp.Error)
|
|
}
|
|
log.Printf("authenticated: session=%s", authResp.SessionID)
|
|
|
|
ticker := time.NewTicker(*interval)
|
|
defer ticker.Stop()
|
|
|
|
sent := 0
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
log.Println("producer stopping")
|
|
return
|
|
case <-ticker.C:
|
|
envelope := model.Envelope{
|
|
SchemaVersion: 1,
|
|
MessageID: messageID(sent + 1),
|
|
Timestamp: time.Now().UnixMilli(),
|
|
Topic: *topic,
|
|
Source: model.Source{
|
|
Kind: model.RoleProducer,
|
|
ID: *sourceID,
|
|
Mode: *mode,
|
|
},
|
|
Target: model.Target{
|
|
DeviceID: *deviceID,
|
|
GroupID: *groupID,
|
|
},
|
|
Payload: payloadForTopic(*topic, *lat, *lng, *speed, *accuracy, *bpm, *confidence, sent),
|
|
}
|
|
|
|
req := model.ClientMessage{
|
|
Type: "publish",
|
|
Envelope: &envelope,
|
|
}
|
|
if err := wsjson.Write(ctx, conn, req); err != nil {
|
|
log.Fatalf("publish failed: %v", err)
|
|
}
|
|
|
|
var resp model.ServerMessage
|
|
if err := wsjson.Read(ctx, conn, &resp); err != nil {
|
|
log.Fatalf("read publish response: %v", err)
|
|
}
|
|
if resp.Type == "error" {
|
|
log.Fatalf("publish rejected: %s", resp.Error)
|
|
}
|
|
|
|
sent++
|
|
log.Printf("published #%d %s", sent, describePublished(*topic, *deviceID, *lat, *lng, *speed, *accuracy, *bpm, *confidence, sent))
|
|
if *count > 0 && sent >= *count {
|
|
log.Println("producer completed")
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func payloadForTopic(topic string, baseLat, baseLng, speed, accuracy float64, bpm int, confidence float64, step int) []byte {
|
|
if topic == "telemetry.heart_rate" {
|
|
return heartRatePayload(bpm, confidence)
|
|
}
|
|
return locationPayload(baseLat, baseLng, speed, accuracy, step)
|
|
}
|
|
|
|
func locationPayload(baseLat, baseLng, speed, accuracy float64, step int) []byte {
|
|
lat := currentLat(baseLat, step)
|
|
lng := currentLng(baseLng, step)
|
|
return []byte(
|
|
`{"lat":` + formatFloat(lat) +
|
|
`,"lng":` + formatFloat(lng) +
|
|
`,"speed":` + formatFloat(speed) +
|
|
`,"bearing":90` +
|
|
`,"accuracy":` + formatFloat(accuracy) +
|
|
`,"coordSystem":"GCJ02"}`,
|
|
)
|
|
}
|
|
|
|
func heartRatePayload(bpm int, confidence float64) []byte {
|
|
if confidence > 0 {
|
|
return []byte(
|
|
`{"bpm":` + strconv.Itoa(maxInt(1, bpm)) +
|
|
`,"confidence":` + formatFloat(confidence) +
|
|
`}`,
|
|
)
|
|
}
|
|
return []byte(`{"bpm":` + strconv.Itoa(maxInt(1, bpm)) + `}`)
|
|
}
|
|
|
|
func currentLat(base float64, step int) float64 {
|
|
return base + float64(step)*0.00002
|
|
}
|
|
|
|
func currentLng(base float64, step int) float64 {
|
|
return base + float64(step)*0.00003
|
|
}
|
|
|
|
func messageID(step int) string {
|
|
return "msg-" + strconv.Itoa(step)
|
|
}
|
|
|
|
func formatFloat(v float64) string {
|
|
return strconv.FormatFloat(v, 'f', 6, 64)
|
|
}
|
|
|
|
func describePublished(topic string, deviceID string, lat, lng, speed, accuracy float64, bpm int, confidence float64, step int) string {
|
|
if topic == "telemetry.heart_rate" {
|
|
if confidence > 0 {
|
|
return "device=" + deviceID + " topic=" + topic + " bpm=" + strconv.Itoa(maxInt(1, bpm)) + " confidence=" + formatFloat(confidence)
|
|
}
|
|
return "device=" + deviceID + " topic=" + topic + " bpm=" + strconv.Itoa(maxInt(1, bpm))
|
|
}
|
|
return "device=" + deviceID + " topic=" + topic + " lat=" + formatFloat(currentLat(lat, step)) + " lng=" + formatFloat(currentLng(lng, step)) + " speed=" + formatFloat(speed) + " accuracy=" + formatFloat(accuracy)
|
|
}
|
|
|
|
func maxInt(left int, right int) int {
|
|
if left > right {
|
|
return left
|
|
}
|
|
return right
|
|
}
|