764 lines
18 KiB
Go
764 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Signal metadata (populated by DISCOVER)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type SignalMeta struct {
|
|
Name string `json:"name"`
|
|
ID uint32 `json:"id"`
|
|
Type string `json:"type"`
|
|
Dimensions uint8 `json:"dimensions"`
|
|
Elements uint32 `json:"elements"`
|
|
Names []string // canonical + alias names mapping to this ID
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Outbound WS message helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type TelemetrySample struct {
|
|
ID uint32 `json:"id"`
|
|
Names []string `json:"names"`
|
|
Ts float64 `json:"ts"`
|
|
Values []float64 `json:"values"`
|
|
}
|
|
|
|
func broadcast(hub *Hub, v any) {
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
return
|
|
}
|
|
hub.broadcast(b)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// MarteClient
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type MarteClient struct {
|
|
hub *Hub
|
|
|
|
mu sync.Mutex
|
|
tcpConn net.Conn
|
|
writer *bufio.Writer
|
|
|
|
cmdMu sync.Mutex // serialise TCP writes
|
|
|
|
sigMu sync.RWMutex
|
|
signals map[uint32]*SignalMeta // id -> meta
|
|
|
|
baseTs uint64
|
|
baseTsSet bool
|
|
basesMu sync.Mutex
|
|
|
|
connected int32 // atomic bool
|
|
|
|
lastWriteMs int64 // atomic; updated on every TCP write, used by keepalive
|
|
|
|
stopCh chan struct{}
|
|
|
|
// accumulates signals across DISCOVER_PART chunks; merged on final DISCOVER
|
|
discoverAcc []discoverSignalJSON
|
|
|
|
// cached last-known ports (updated by SERVICE_INFO)
|
|
host string
|
|
cmdPort int
|
|
udpPort int
|
|
logPort int
|
|
}
|
|
|
|
func newMarteClient(hub *Hub) *MarteClient {
|
|
return &MarteClient{
|
|
hub: hub,
|
|
signals: make(map[uint32]*SignalMeta),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (m *MarteClient) IsConnected() bool {
|
|
return atomic.LoadInt32(&m.connected) == 1
|
|
}
|
|
|
|
// SendCachedState sends the current signal list to a freshly connected browser.
|
|
func (m *MarteClient) SendCachedState(c *WSClient) {
|
|
m.sigMu.RLock()
|
|
defer m.sigMu.RUnlock()
|
|
if len(m.signals) == 0 {
|
|
return
|
|
}
|
|
sigs := make([]*SignalMeta, 0, len(m.signals))
|
|
for _, s := range m.signals {
|
|
sigs = append(sigs, s)
|
|
}
|
|
b, _ := json.Marshal(map[string]any{"type": "signal_cache", "signals": sigs})
|
|
select {
|
|
case c.send <- b:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Connect / Disconnect
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (m *MarteClient) Connect(host string, cmdPort, udpPort, logPort int) {
|
|
m.Disconnect()
|
|
|
|
m.mu.Lock()
|
|
m.host = host
|
|
m.cmdPort = cmdPort
|
|
m.udpPort = udpPort
|
|
m.logPort = logPort
|
|
m.stopCh = make(chan struct{})
|
|
m.mu.Unlock()
|
|
|
|
go m.runTCP(host, cmdPort)
|
|
go m.runUDP(host, udpPort)
|
|
go m.runLog(host, logPort)
|
|
}
|
|
|
|
func (m *MarteClient) Disconnect() {
|
|
m.mu.Lock()
|
|
select {
|
|
case <-m.stopCh:
|
|
// already closed
|
|
default:
|
|
close(m.stopCh)
|
|
}
|
|
if m.tcpConn != nil {
|
|
m.tcpConn.Close()
|
|
m.tcpConn = nil
|
|
}
|
|
m.mu.Unlock()
|
|
|
|
atomic.StoreInt32(&m.connected, 0)
|
|
m.basesMu.Lock()
|
|
m.baseTsSet = false
|
|
m.basesMu.Unlock()
|
|
m.discoverAcc = nil
|
|
}
|
|
|
|
func (m *MarteClient) stopped() bool {
|
|
select {
|
|
case <-m.stopCh:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TCP command channel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (m *MarteClient) runTCP(host string, port int) {
|
|
addr := fmt.Sprintf("%s:%d", host, port)
|
|
for !m.stopped() {
|
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
|
if err != nil {
|
|
time.Sleep(2 * time.Second)
|
|
continue
|
|
}
|
|
conn.(*net.TCPConn).SetNoDelay(true)
|
|
|
|
m.mu.Lock()
|
|
m.tcpConn = conn
|
|
m.writer = bufio.NewWriter(conn)
|
|
m.mu.Unlock()
|
|
|
|
atomic.StoreInt32(&m.connected, 1)
|
|
broadcast(m.hub, map[string]any{"type": "connected"})
|
|
|
|
// Send SERVICE_INFO to auto-discover ports
|
|
m.writeCmd("SERVICE_INFO")
|
|
|
|
go m.runKeepalive()
|
|
|
|
m.readLoop(conn)
|
|
|
|
atomic.StoreInt32(&m.connected, 0)
|
|
broadcast(m.hub, map[string]any{"type": "disconnected"})
|
|
|
|
m.mu.Lock()
|
|
m.tcpConn = nil
|
|
m.writer = nil
|
|
m.mu.Unlock()
|
|
|
|
if !m.stopped() {
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *MarteClient) writeCmd(cmd string) {
|
|
m.cmdMu.Lock()
|
|
defer m.cmdMu.Unlock()
|
|
m.mu.Lock()
|
|
w := m.writer
|
|
m.mu.Unlock()
|
|
if w == nil {
|
|
return
|
|
}
|
|
log.Printf("[→MARTe] %s", cmd)
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
|
"level": "CMD", "message": fmt.Sprintf("→ %s", cmd),
|
|
})
|
|
w.WriteString(cmd + "\n")
|
|
w.Flush()
|
|
atomic.StoreInt64(&m.lastWriteMs, time.Now().UnixMilli())
|
|
}
|
|
|
|
// runKeepalive sends INFO every 20 s when no other command has been written
|
|
// in the last 20 s, keeping the DebugService 30 s idle timer from firing.
|
|
func (m *MarteClient) runKeepalive() {
|
|
ticker := time.NewTicker(20 * time.Second)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-m.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
if !m.IsConnected() {
|
|
continue
|
|
}
|
|
idleMs := time.Now().UnixMilli() - atomic.LoadInt64(&m.lastWriteMs)
|
|
if idleMs >= 20_000 {
|
|
m.writeCmd("INFO")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *MarteClient) SendCommand(cmd string) {
|
|
m.writeCmd(cmd)
|
|
}
|
|
|
|
func (m *MarteClient) readLoop(conn net.Conn) {
|
|
scanner := bufio.NewScanner(conn)
|
|
scanner.Buffer(make([]byte, 8*1024*1024), 8*1024*1024)
|
|
|
|
var jsonAcc strings.Builder
|
|
inJSON := false
|
|
|
|
for scanner.Scan() {
|
|
if m.stopped() {
|
|
return
|
|
}
|
|
line := scanner.Text()
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
|
|
// Detect start of JSON block
|
|
if !inJSON && strings.HasPrefix(trimmed, "{") {
|
|
inJSON = true
|
|
jsonAcc.Reset()
|
|
}
|
|
|
|
if inJSON {
|
|
jsonAcc.WriteString(trimmed)
|
|
tag, done := detectJSONDone(trimmed)
|
|
if done {
|
|
inJSON = false
|
|
raw := jsonAcc.String()
|
|
// Strip trailing sentinel
|
|
idx := strings.Index(raw, "OK "+tag)
|
|
if idx >= 0 {
|
|
raw = strings.TrimSpace(raw[:idx])
|
|
}
|
|
m.handleJSONResponse(tag, raw)
|
|
jsonAcc.Reset()
|
|
}
|
|
} else {
|
|
m.handleTextLine(trimmed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// detectJSONDone returns (tag, true) when the line is exactly "OK <TAG>".
|
|
// DISCOVER_PART must appear before DISCOVER so partial chunks are not mistaken
|
|
// for the final chunk (though with exact matching this is not strictly
|
|
// necessary; kept for clarity).
|
|
func detectJSONDone(line string) (string, bool) {
|
|
tags := []string{
|
|
"DISCOVER_PART", "DISCOVER", "TREE", "INFO", "CONFIG", "STEP_STATUS",
|
|
"VALUE", "MSG", "TRACE", "FORCE", "UNFORCE", "BREAK",
|
|
"PAUSE", "RESUME", "STEP", "MONITOR", "UNMONITOR", "LS",
|
|
}
|
|
for _, t := range tags {
|
|
if line == "OK "+t {
|
|
return t, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (m *MarteClient) handleJSONResponse(tag, data string) {
|
|
log.Printf("[←MARTe] %s %d bytes", tag, len(data))
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "log", "time": time.Now().Format("15:04:05.000"),
|
|
"level": "RESP", "message": fmt.Sprintf("← %s (%d B)", tag, len(data)),
|
|
})
|
|
switch tag {
|
|
case "DISCOVER_PART":
|
|
// Accumulate this chunk; do NOT broadcast — we wait for the final chunk.
|
|
var resp discoverResp
|
|
if err := json.Unmarshal([]byte(data), &resp); err != nil {
|
|
log.Printf("[DISCOVER_PART] parse error: %v", err)
|
|
return
|
|
}
|
|
m.discoverAcc = append(m.discoverAcc, resp.Signals...)
|
|
log.Printf("[DISCOVER_PART] accumulated %d signals (total so far: %d)",
|
|
len(resp.Signals), len(m.discoverAcc))
|
|
return
|
|
|
|
case "DISCOVER":
|
|
// Merge any previously accumulated part-chunks with this final chunk.
|
|
var resp discoverResp
|
|
if err := json.Unmarshal([]byte(data), &resp); err != nil {
|
|
log.Printf("[DISCOVER] parse error: %v", err)
|
|
return
|
|
}
|
|
all := append(m.discoverAcc, resp.Signals...)
|
|
m.discoverAcc = nil
|
|
m.parseDiscoverSignals(all)
|
|
// Re-marshal the merged list so the browser gets a single consistent blob.
|
|
merged, _ := json.Marshal(discoverResp{Signals: all})
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "response", "tag": "DISCOVER", "data": string(merged),
|
|
})
|
|
return
|
|
|
|
case "TREE":
|
|
// Parse server-side and stream pre-digested flat-node batches to the
|
|
// browser so it never has to JSON.parse a multi-MB string.
|
|
go m.sendTreeBatches(data)
|
|
return
|
|
}
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "response",
|
|
"tag": tag,
|
|
"data": data,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TREE streaming
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// TreeNode mirrors the DebugService TREE JSON structure.
|
|
type TreeNode struct {
|
|
Name string `json:"Name"`
|
|
Class string `json:"Class"`
|
|
IsTraceable bool `json:"IsTraceable"`
|
|
IsForcable bool `json:"IsForcable"`
|
|
Elements uint32 `json:"Elements"`
|
|
Children []*TreeNode `json:"Children"`
|
|
}
|
|
|
|
// FlatNode is a pre-digested, compact representation sent to the browser.
|
|
type FlatNode struct {
|
|
Path string `json:"p"`
|
|
Name string `json:"n"`
|
|
Class string `json:"c"`
|
|
Parent string `json:"par"`
|
|
Tr bool `json:"tr,omitempty"`
|
|
Fo bool `json:"fo,omitempty"`
|
|
El uint32 `json:"el,omitempty"`
|
|
HasCh bool `json:"ch,omitempty"`
|
|
}
|
|
|
|
func flattenTree(node *TreeNode, parentPath string, out *[]FlatNode) {
|
|
path := node.Name
|
|
if parentPath != "" {
|
|
path = parentPath + "." + node.Name
|
|
}
|
|
hasCh := len(node.Children) > 0
|
|
*out = append(*out, FlatNode{
|
|
Path: path,
|
|
Name: node.Name,
|
|
Class: node.Class,
|
|
Parent: parentPath,
|
|
Tr: node.IsTraceable,
|
|
Fo: node.IsForcable,
|
|
El: node.Elements,
|
|
HasCh: hasCh,
|
|
})
|
|
for _, c := range node.Children {
|
|
flattenTree(c, path, out)
|
|
}
|
|
}
|
|
|
|
// sendTreeBatches parses the raw TREE JSON and streams compact FlatNode
|
|
// batches to all browsers. Each browser processes one batch per animation
|
|
// frame, keeping the UI responsive during large tree loads.
|
|
func (m *MarteClient) sendTreeBatches(raw string) {
|
|
var root TreeNode
|
|
if err := json.Unmarshal([]byte(raw), &root); err != nil {
|
|
log.Printf("[TREE] parse error: %v — falling back to raw JSON", err)
|
|
broadcast(m.hub, map[string]any{"type": "response", "tag": "TREE", "data": raw})
|
|
return
|
|
}
|
|
|
|
var nodes []FlatNode
|
|
if root.Name == "Root" && len(root.Children) > 0 {
|
|
for _, c := range root.Children {
|
|
flattenTree(c, "", &nodes)
|
|
}
|
|
} else {
|
|
flattenTree(&root, "", &nodes)
|
|
}
|
|
|
|
total := len(nodes)
|
|
log.Printf("[TREE] streaming %d flat nodes to browser", total)
|
|
broadcast(m.hub, map[string]any{"type": "tree_clear", "total": total})
|
|
|
|
const batchSize = 50
|
|
for i := 0; i < total; i += batchSize {
|
|
end := i + batchSize
|
|
if end > total {
|
|
end = total
|
|
}
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "tree_batch",
|
|
"nodes": nodes[i:end],
|
|
"done": end == total,
|
|
})
|
|
}
|
|
}
|
|
|
|
func (m *MarteClient) handleTextLine(line string) {
|
|
if strings.HasPrefix(line, "OK SERVICE_INFO") {
|
|
// OK SERVICE_INFO TCP_CTRL:8110 UDP_STREAM:8111 TCP_LOG:8082 STATE:RUNNING
|
|
parts := strings.Fields(line)
|
|
newUDP, newLog := 0, 0
|
|
for _, p := range parts {
|
|
if strings.HasPrefix(p, "UDP_STREAM:") {
|
|
fmt.Sscanf(p[11:], "%d", &newUDP)
|
|
}
|
|
if strings.HasPrefix(p, "TCP_LOG:") {
|
|
fmt.Sscanf(p[8:], "%d", &newLog)
|
|
}
|
|
}
|
|
// Always forward as a response so the browser can display it.
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "response",
|
|
"tag": "SERVICE_INFO",
|
|
"data": line[len("OK SERVICE_INFO "):],
|
|
})
|
|
if newUDP > 0 || newLog > 0 {
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "service_config",
|
|
"udp_port": newUDP,
|
|
"log_port": newLog,
|
|
})
|
|
// Restart UDP/log workers with updated ports if they differ
|
|
m.mu.Lock()
|
|
host := m.host
|
|
oldUDP := m.udpPort
|
|
oldLog := m.logPort
|
|
if newUDP > 0 {
|
|
m.udpPort = newUDP
|
|
}
|
|
if newLog > 0 {
|
|
m.logPort = newLog
|
|
}
|
|
m.mu.Unlock()
|
|
if newUDP > 0 && newUDP != oldUDP {
|
|
go m.runUDP(host, newUDP)
|
|
}
|
|
if newLog > 0 && newLog != oldLog {
|
|
go m.runLog(host, newLog)
|
|
}
|
|
}
|
|
}
|
|
// Forward all text lines to browsers.
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "text_line",
|
|
"data": line,
|
|
})
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// DISCOVER parsing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type discoverSignalJSON struct {
|
|
Name string `json:"name"`
|
|
ID uint32 `json:"id"`
|
|
Type string `json:"type"`
|
|
Dimensions uint8 `json:"dimensions"`
|
|
Elements uint32 `json:"elements"`
|
|
}
|
|
|
|
type discoverResp struct {
|
|
Signals []discoverSignalJSON `json:"Signals"`
|
|
}
|
|
|
|
func (m *MarteClient) parseDiscoverSignals(sigs []discoverSignalJSON) {
|
|
m.sigMu.Lock()
|
|
defer m.sigMu.Unlock()
|
|
m.signals = make(map[uint32]*SignalMeta, len(sigs))
|
|
for _, s := range sigs {
|
|
el := s.Elements
|
|
if el == 0 {
|
|
el = 1
|
|
}
|
|
// Multiple DISCOVER entries can share the same ID (canonical DataSource
|
|
// path + one GAM alias per broker). Merge them into one SignalMeta so
|
|
// that Names carries every alias. This is required for the telemetry
|
|
// receiver to match whichever name the user traced.
|
|
if existing, ok := m.signals[s.ID]; ok {
|
|
existing.Names = append(existing.Names, s.Name)
|
|
continue
|
|
}
|
|
meta := &SignalMeta{
|
|
Name: s.Name,
|
|
ID: s.ID,
|
|
Type: s.Type,
|
|
Dimensions: s.Dimensions,
|
|
Elements: el,
|
|
Names: []string{s.Name},
|
|
}
|
|
m.signals[s.ID] = meta
|
|
}
|
|
log.Printf("[DISCOVER] registered %d unique signals from %d entries", len(m.signals), len(sigs))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UDP telemetry
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const udpMagic uint32 = 0xDA7A57AD
|
|
|
|
func (m *MarteClient) runUDP(host string, port int) {
|
|
addr := fmt.Sprintf("0.0.0.0:%d", port)
|
|
udpAddr, err := net.ResolveUDPAddr("udp4", addr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
conn, err := net.ListenUDP("udp4", udpAddr)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
conn.SetReadBuffer(10 * 1024 * 1024)
|
|
conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
|
|
|
buf := make([]byte, 65535)
|
|
var lastSeq uint32
|
|
var hasLastSeq bool
|
|
var totalPkts uint64
|
|
var dropped uint64
|
|
|
|
for !m.stopped() {
|
|
conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
|
n, _, err := conn.ReadFromUDP(buf)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if n < 20 {
|
|
continue
|
|
}
|
|
if binary.LittleEndian.Uint32(buf[0:4]) != udpMagic {
|
|
continue
|
|
}
|
|
|
|
seq := binary.LittleEndian.Uint32(buf[4:8])
|
|
if hasLastSeq && seq > lastSeq+1 {
|
|
dropped += uint64(seq - lastSeq - 1)
|
|
}
|
|
lastSeq = seq
|
|
hasLastSeq = true
|
|
|
|
totalPkts++
|
|
if totalPkts%500 == 0 {
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "udp_stats",
|
|
"packets": totalPkts,
|
|
"dropped": dropped,
|
|
})
|
|
}
|
|
|
|
count := binary.LittleEndian.Uint32(buf[16:20])
|
|
offset := 20
|
|
|
|
// Resolve base timestamp once per packet.
|
|
m.basesMu.Lock()
|
|
if !m.baseTsSet && n >= 20+12 {
|
|
m.baseTs = binary.LittleEndian.Uint64(buf[20+4 : 20+12])
|
|
m.baseTsSet = true
|
|
}
|
|
baseTs := m.baseTs
|
|
baseTsSet := m.baseTsSet
|
|
m.basesMu.Unlock()
|
|
|
|
m.sigMu.RLock()
|
|
samples := make([]TelemetrySample, 0, count)
|
|
for i := uint32(0); i < count; i++ {
|
|
if offset+16 > n {
|
|
break
|
|
}
|
|
id := binary.LittleEndian.Uint32(buf[offset : offset+4])
|
|
tsRaw := binary.LittleEndian.Uint64(buf[offset+4 : offset+12])
|
|
size := binary.LittleEndian.Uint32(buf[offset+12 : offset+16])
|
|
offset += 16
|
|
|
|
if offset+int(size) > n {
|
|
break
|
|
}
|
|
dataSlice := buf[offset : offset+int(size)]
|
|
offset += int(size)
|
|
|
|
tsS := 0.0
|
|
if baseTsSet && tsRaw >= baseTs {
|
|
tsS = float64(tsRaw-baseTs) / 1_000_000.0
|
|
}
|
|
|
|
meta, ok := m.signals[id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
elements := meta.Elements
|
|
if elements == 0 {
|
|
elements = 1
|
|
}
|
|
typeSize := uint32(size)
|
|
if elements > 0 {
|
|
typeSize = uint32(size) / elements
|
|
}
|
|
vals := make([]float64, 0, elements)
|
|
t := meta.Type
|
|
|
|
for e := uint32(0); e < elements; e++ {
|
|
off := e * typeSize
|
|
if off+typeSize > uint32(len(dataSlice)) {
|
|
break
|
|
}
|
|
elem := dataSlice[off : off+typeSize]
|
|
vals = append(vals, parseTypedValue(elem, typeSize, t))
|
|
}
|
|
|
|
samples = append(samples, TelemetrySample{
|
|
ID: id,
|
|
Names: meta.Names,
|
|
Ts: tsS,
|
|
Values: vals,
|
|
})
|
|
}
|
|
m.sigMu.RUnlock()
|
|
|
|
if len(samples) > 0 {
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "telemetry",
|
|
"seq": seq,
|
|
"signals": samples,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseTypedValue(data []byte, size uint32, t string) float64 {
|
|
switch size {
|
|
case 1:
|
|
if strings.Contains(t, "u") {
|
|
return float64(data[0])
|
|
}
|
|
return float64(int8(data[0]))
|
|
case 2:
|
|
if len(data) < 2 {
|
|
return 0
|
|
}
|
|
v := binary.LittleEndian.Uint16(data[:2])
|
|
if strings.Contains(t, "u") {
|
|
return float64(v)
|
|
}
|
|
return float64(int16(v))
|
|
case 4:
|
|
if len(data) < 4 {
|
|
return 0
|
|
}
|
|
v := binary.LittleEndian.Uint32(data[:4])
|
|
if strings.Contains(t, "float") || strings.Contains(t, "float32") {
|
|
return float64(math.Float32frombits(v))
|
|
}
|
|
if strings.Contains(t, "u") {
|
|
return float64(v)
|
|
}
|
|
return float64(int32(v))
|
|
case 8:
|
|
if len(data) < 8 {
|
|
return 0
|
|
}
|
|
v := binary.LittleEndian.Uint64(data[:8])
|
|
if strings.Contains(t, "float") || strings.Contains(t, "float64") || strings.Contains(t, "double") {
|
|
return math.Float64frombits(v)
|
|
}
|
|
if strings.Contains(t, "u") {
|
|
return float64(v)
|
|
}
|
|
return float64(int64(v))
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TCP log channel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func (m *MarteClient) runLog(host string, port int) {
|
|
addr := fmt.Sprintf("%s:%d", host, port)
|
|
for !m.stopped() {
|
|
conn, err := net.DialTimeout("tcp", addr, 5*time.Second)
|
|
if err != nil {
|
|
time.Sleep(2 * time.Second)
|
|
continue
|
|
}
|
|
scanner := bufio.NewScanner(conn)
|
|
for scanner.Scan() {
|
|
if m.stopped() {
|
|
conn.Close()
|
|
return
|
|
}
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if strings.HasPrefix(line, "LOG ") {
|
|
rest := line[4:]
|
|
idx := strings.Index(rest, " ")
|
|
if idx < 0 {
|
|
continue
|
|
}
|
|
level := rest[:idx]
|
|
msg := rest[idx+1:]
|
|
broadcast(m.hub, map[string]any{
|
|
"type": "log",
|
|
"time": time.Now().Format("15:04:05.000"),
|
|
"level": level,
|
|
"message": msg,
|
|
})
|
|
}
|
|
}
|
|
conn.Close()
|
|
if !m.stopped() {
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
}
|
|
}
|