FIrs
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
module udpstreamer-webui
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gorilla/websocket v1.5.1
|
||||
|
||||
require golang.org/x/net v0.17.0 // indirect
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
@@ -0,0 +1,383 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ─── WebSocket client ─────────────────────────────────────────────────────────
|
||||
|
||||
type wsClient struct {
|
||||
hub *Hub
|
||||
conn *websocket.Conn
|
||||
send chan []byte
|
||||
}
|
||||
|
||||
func (c *wsClient) writePump() {
|
||||
pingTicker := time.NewTicker(30 * time.Second)
|
||||
defer func() {
|
||||
pingTicker.Stop()
|
||||
c.conn.Close()
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case msg, ok := <-c.send:
|
||||
if !ok {
|
||||
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
|
||||
return
|
||||
}
|
||||
if err := c.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||
return
|
||||
}
|
||||
case <-pingTicker.C:
|
||||
if err := c.conn.WriteControl(websocket.PingMessage, []byte{},
|
||||
time.Now().Add(10*time.Second)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *wsClient) readPump() {
|
||||
defer func() {
|
||||
c.hub.unregister <- c
|
||||
c.conn.Close()
|
||||
}()
|
||||
c.conn.SetReadLimit(64 * 1024)
|
||||
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
c.conn.SetPongHandler(func(string) error {
|
||||
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
return nil
|
||||
})
|
||||
for {
|
||||
_, msg, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
// Handle client messages (ping, setWindow, etc.) – currently just log.
|
||||
var env map[string]interface{}
|
||||
if json.Unmarshal(msg, &env) == nil {
|
||||
if t, ok := env["type"].(string); ok {
|
||||
switch t {
|
||||
case "ping":
|
||||
resp, _ := json.Marshal(map[string]string{"type": "pong"})
|
||||
select {
|
||||
case c.send <- resp:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Hub ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 64 * 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// Hub is the central data broker between the UDP client and WebSocket clients.
|
||||
type Hub struct {
|
||||
mu sync.RWMutex
|
||||
signals []SignalInfo
|
||||
configJS []byte // cached JSON config message
|
||||
|
||||
clients map[*wsClient]bool
|
||||
register chan *wsClient
|
||||
unregister chan *wsClient
|
||||
broadcastCh chan []byte // all sends go through Run() to avoid races on c.send
|
||||
|
||||
dataCh chan DataSample // incoming samples from UDP goroutine
|
||||
}
|
||||
|
||||
// NewHub creates an initialised Hub.
|
||||
func NewHub() *Hub {
|
||||
return &Hub{
|
||||
clients: make(map[*wsClient]bool),
|
||||
register: make(chan *wsClient, 8),
|
||||
unregister: make(chan *wsClient, 8),
|
||||
broadcastCh: make(chan []byte, 64),
|
||||
dataCh: make(chan DataSample, 256),
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateConfig stores a new signal config and broadcasts it to all WS clients.
|
||||
func (h *Hub) UpdateConfig(sigs []SignalInfo) {
|
||||
msg, err := json.Marshal(map[string]interface{}{
|
||||
"type": "config",
|
||||
"signals": sigs,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("hub: marshal config: %v", err)
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.signals = sigs
|
||||
h.configJS = msg
|
||||
h.mu.Unlock()
|
||||
|
||||
h.broadcast(msg)
|
||||
}
|
||||
|
||||
// PushData enqueues a data sample for broadcasting to WebSocket clients.
|
||||
func (h *Hub) PushData(s DataSample) {
|
||||
select {
|
||||
case h.dataCh <- s:
|
||||
default:
|
||||
// Drop if buffer full to avoid blocking the UDP goroutine.
|
||||
}
|
||||
}
|
||||
|
||||
// broadcast enqueues a message for delivery to all WebSocket clients.
|
||||
// All actual sends happen inside Run() to avoid concurrent access to c.send.
|
||||
func (h *Hub) broadcast(msg []byte) {
|
||||
select {
|
||||
case h.broadcastCh <- msg:
|
||||
default:
|
||||
// Drop if the broadcast queue is full.
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWebSocket upgrades an HTTP request to a WebSocket connection.
|
||||
func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("ws upgrade: %v", err)
|
||||
return
|
||||
}
|
||||
c := &wsClient{
|
||||
hub: h,
|
||||
conn: conn,
|
||||
send: make(chan []byte, 64),
|
||||
}
|
||||
h.register <- c
|
||||
|
||||
go c.writePump()
|
||||
go c.readPump()
|
||||
}
|
||||
|
||||
// Run is the hub's main goroutine. It must be started with go hub.Run().
|
||||
func (h *Hub) Run() {
|
||||
// Batch data at ≤30 Hz.
|
||||
ticker := time.NewTicker(time.Second / 30)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Accumulate samples between ticks.
|
||||
pending := make([]DataSample, 0, 64)
|
||||
|
||||
for {
|
||||
select {
|
||||
case c := <-h.register:
|
||||
h.mu.Lock()
|
||||
h.clients[c] = true
|
||||
cfg := h.configJS
|
||||
h.mu.Unlock()
|
||||
// Send current config immediately if we have one.
|
||||
if cfg != nil {
|
||||
select {
|
||||
case c.send <- cfg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
case c := <-h.unregister:
|
||||
h.mu.Lock()
|
||||
if _, ok := h.clients[c]; ok {
|
||||
delete(h.clients, c)
|
||||
close(c.send)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
case msg := <-h.broadcastCh:
|
||||
h.mu.RLock()
|
||||
for c := range h.clients {
|
||||
select {
|
||||
case c.send <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
h.mu.RUnlock()
|
||||
|
||||
case s := <-h.dataCh:
|
||||
pending = append(pending, s)
|
||||
|
||||
case <-ticker.C:
|
||||
if len(pending) == 0 {
|
||||
continue
|
||||
}
|
||||
h.mu.RLock()
|
||||
sigs := h.signals
|
||||
noClients := len(h.clients) == 0
|
||||
h.mu.RUnlock()
|
||||
|
||||
if noClients || len(sigs) == 0 {
|
||||
pending = pending[:0]
|
||||
continue
|
||||
}
|
||||
|
||||
msg := h.buildDataMessage(pending, sigs)
|
||||
pending = pending[:0]
|
||||
if msg != nil {
|
||||
h.broadcast(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// maxBatchPoints is the maximum number of points sent per signal per 30 Hz display tick.
|
||||
// For temporal-array signals (e.g. 1 MSps packed as 1000 samples/packet), the expanded
|
||||
// sample stream is decimated to this limit before transmission to WebSocket clients.
|
||||
const maxBatchPoints = 2000
|
||||
|
||||
// sigData carries one signal's worth of time+value pairs in a single batch message.
|
||||
type sigData struct {
|
||||
T []float64 `json:"t"` // unix seconds
|
||||
V []float64 `json:"v"` // physical values
|
||||
}
|
||||
|
||||
// dataMsg is the JSON envelope for batched data sent to WebSocket clients.
|
||||
// Each signal carries its own time axis so that temporal arrays (packed sample
|
||||
// bursts with a known sampling rate) and scalar signals can coexist cleanly.
|
||||
type dataMsg struct {
|
||||
Type string `json:"type"`
|
||||
Signals map[string]sigData `json:"signals"`
|
||||
}
|
||||
|
||||
// buildDataMessage merges a batch of DataSamples into one JSON message.
|
||||
//
|
||||
// Three cases are handled:
|
||||
//
|
||||
// 1. Temporal array (NumElements > 1, TimeMode != PacketTime):
|
||||
// The N samples in each packet represent a contiguous time burst at SamplingRate.
|
||||
// Wall arrival time is treated as the timestamp of the last sample; earlier
|
||||
// samples are reconstructed as t[k] = wallT - (N-1-k)/SamplingRate.
|
||||
// The full expanded stream is decimated to maxBatchPoints if needed.
|
||||
//
|
||||
// 2. Scalar signal (NumElements == 1):
|
||||
// One {t, v} pair per packet – wall arrival time used as timestamp.
|
||||
//
|
||||
// 3. Spatial / PacketTime array (NumElements > 1, TimeMode == 0):
|
||||
// Each element is tracked as a separate stream keyed "sig[i]", with wall
|
||||
// arrival time as the shared timestamp.
|
||||
func (h *Hub) buildDataMessage(batch []DataSample, sigs []SignalInfo) []byte {
|
||||
if len(batch) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make(map[string]sigData, len(sigs)*2)
|
||||
|
||||
for _, sig := range sigs {
|
||||
n := sig.NumElements()
|
||||
isTemporal := n > 1 && sig.TimeMode != 0
|
||||
|
||||
switch {
|
||||
case isTemporal:
|
||||
// Expand each packet's N samples into individual time-stamped points.
|
||||
allT := make([]float64, 0, len(batch)*n)
|
||||
allV := make([]float64, 0, len(batch)*n)
|
||||
|
||||
dt := 0.0
|
||||
if sig.SamplingRate > 0 {
|
||||
dt = 1.0 / sig.SamplingRate
|
||||
}
|
||||
|
||||
for _, s := range batch {
|
||||
vals, ok := s.Values[sig.Name]
|
||||
if !ok || len(vals) < n {
|
||||
continue
|
||||
}
|
||||
// wallT ≈ arrival time of the packet ≈ timestamp of the last sample.
|
||||
wallT := float64(s.WallTime.UnixNano()) / 1e9
|
||||
for k := 0; k < n; k++ {
|
||||
allT = append(allT, wallT-float64(n-1-k)*dt)
|
||||
allV = append(allV, vals[k])
|
||||
}
|
||||
}
|
||||
|
||||
// Decimate to maxBatchPoints if necessary.
|
||||
step := 1
|
||||
if len(allT) > maxBatchPoints {
|
||||
step = len(allT) / maxBatchPoints
|
||||
if step < 1 {
|
||||
step = 1
|
||||
}
|
||||
}
|
||||
decimT := make([]float64, 0, len(allT)/step+1)
|
||||
decimV := make([]float64, 0, len(allV)/step+1)
|
||||
for i := 0; i < len(allT); i += step {
|
||||
decimT = append(decimT, allT[i])
|
||||
decimV = append(decimV, allV[i])
|
||||
}
|
||||
out[sig.Name] = sigData{T: decimT, V: decimV}
|
||||
|
||||
case n == 1:
|
||||
// Scalar signal: one sample per packet.
|
||||
ts := make([]float64, 0, len(batch))
|
||||
vs := make([]float64, 0, len(batch))
|
||||
for _, s := range batch {
|
||||
vals, ok := s.Values[sig.Name]
|
||||
if !ok || len(vals) < 1 {
|
||||
continue
|
||||
}
|
||||
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
|
||||
vs = append(vs, vals[0])
|
||||
}
|
||||
out[sig.Name] = sigData{T: ts, V: vs}
|
||||
|
||||
default:
|
||||
// Spatial / PacketTime array: one stream per element, keyed "sig[i]".
|
||||
for i := 0; i < n; i++ {
|
||||
key := arrayKey(sig.Name, i)
|
||||
ts := make([]float64, 0, len(batch))
|
||||
vs := make([]float64, 0, len(batch))
|
||||
for _, s := range batch {
|
||||
vals, ok := s.Values[sig.Name]
|
||||
if !ok || len(vals) <= i {
|
||||
continue
|
||||
}
|
||||
ts = append(ts, float64(s.WallTime.UnixNano())/1e9)
|
||||
vs = append(vs, vals[i])
|
||||
}
|
||||
out[key] = sigData{T: ts, V: vs}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result, err := json.Marshal(dataMsg{
|
||||
Type: "data",
|
||||
Signals: out,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("hub: marshal data: %v", err)
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// arrayKey returns the buffer key for element i of an array signal.
|
||||
func arrayKey(name string, i int) string {
|
||||
return name + "[" + itoa(i) + "]"
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
buf := [20]byte{}
|
||||
pos := len(buf)
|
||||
for n > 0 {
|
||||
pos--
|
||||
buf[pos] = byte('0' + n%10)
|
||||
n /= 10
|
||||
}
|
||||
return string(buf[pos:])
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed static/index.html
|
||||
var staticFiles embed.FS
|
||||
|
||||
func main() {
|
||||
streamerAddr := flag.String("streamer", "127.0.0.1:44500", "MARTe2 UDP streamer address (host:port)")
|
||||
listenAddr := flag.String("listen", ":8080", "HTTP listen address")
|
||||
clientPort := flag.Int("clientport", 44900, "Local UDP port to bind for streamer data")
|
||||
flag.Parse()
|
||||
|
||||
hub := NewHub()
|
||||
go hub.Run()
|
||||
|
||||
udpClient := NewUDPClient(*streamerAddr, *clientPort, hub)
|
||||
go udpClient.Run()
|
||||
|
||||
// Serve the embedded index.html at /.
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data, err := staticFiles.ReadFile("static/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, "index.html not found", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write(data)
|
||||
})
|
||||
|
||||
http.HandleFunc("/ws", hub.HandleWebSocket)
|
||||
|
||||
log.Printf("WebUI listening on %s (streamer=%s, local UDP port=%d)",
|
||||
*listenAddr, *streamerAddr, *clientPort)
|
||||
if err := http.ListenAndServe(*listenAddr, nil); err != nil {
|
||||
log.Fatalf("http: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
MagicUDPS uint32 = 0x53504455 // 'UDPS' little-endian
|
||||
|
||||
PktData uint8 = 0
|
||||
PktConfig uint8 = 1
|
||||
PktACK uint8 = 2
|
||||
PktConnect uint8 = 3
|
||||
PktDisconnect uint8 = 4
|
||||
|
||||
HeaderSize = 17
|
||||
SigDescSize = 136
|
||||
NoTimeSignal = uint32(0xFFFFFFFF)
|
||||
|
||||
QuantNone uint8 = 0
|
||||
QuantUint8 uint8 = 1
|
||||
QuantInt8 uint8 = 2
|
||||
QuantUint16 uint8 = 3
|
||||
QuantInt16 uint8 = 4
|
||||
)
|
||||
|
||||
// ─── Packet header (17 bytes, little-endian, packed) ─────────────────────────
|
||||
|
||||
type PacketHeader struct {
|
||||
Magic uint32
|
||||
Type uint8
|
||||
Counter uint32
|
||||
FragmentIdx uint16
|
||||
TotalFragments uint16
|
||||
PayloadBytes uint32
|
||||
}
|
||||
|
||||
// ParseHeader decodes exactly HeaderSize bytes into a PacketHeader.
|
||||
func ParseHeader(b []byte) (PacketHeader, error) {
|
||||
if len(b) < HeaderSize {
|
||||
return PacketHeader{}, fmt.Errorf("header too short: %d bytes", len(b))
|
||||
}
|
||||
var h PacketHeader
|
||||
r := bytes.NewReader(b[:HeaderSize])
|
||||
if err := binary.Read(r, binary.LittleEndian, &h); err != nil {
|
||||
return PacketHeader{}, err
|
||||
}
|
||||
if h.Magic != MagicUDPS {
|
||||
return PacketHeader{}, fmt.Errorf("bad magic: 0x%08X", h.Magic)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// buildHeader serialises a PacketHeader to a 17-byte slice.
|
||||
func buildHeader(h PacketHeader) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
_ = binary.Write(buf, binary.LittleEndian, h)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// BuildConnectPacket returns a 17-byte CONNECT datagram.
|
||||
func BuildConnectPacket() []byte {
|
||||
return buildHeader(PacketHeader{
|
||||
Magic: MagicUDPS,
|
||||
Type: PktConnect,
|
||||
Counter: 0,
|
||||
FragmentIdx: 0,
|
||||
TotalFragments: 1,
|
||||
PayloadBytes: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// BuildDisconnectPacket returns a 17-byte DISCONNECT datagram.
|
||||
func BuildDisconnectPacket() []byte {
|
||||
return buildHeader(PacketHeader{
|
||||
Magic: MagicUDPS,
|
||||
Type: PktDisconnect,
|
||||
Counter: 0,
|
||||
FragmentIdx: 0,
|
||||
TotalFragments: 1,
|
||||
PayloadBytes: 0,
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Signal descriptor (136 bytes) ───────────────────────────────────────────
|
||||
|
||||
// SignalInfo holds the parsed metadata for one signal.
|
||||
type SignalInfo struct {
|
||||
Name string `json:"name"`
|
||||
TypeCode uint8 `json:"typeCode"`
|
||||
QuantType uint8 `json:"quantType"`
|
||||
NumDimensions uint8 `json:"numDimensions"`
|
||||
NumRows uint32 `json:"numRows"`
|
||||
NumCols uint32 `json:"numCols"`
|
||||
RangeMin float64 `json:"rangeMin"`
|
||||
RangeMax float64 `json:"rangeMax"`
|
||||
TimeMode uint8 `json:"timeMode"`
|
||||
SamplingRate float64 `json:"samplingRate"`
|
||||
TimeSignalIdx uint32 `json:"timeSignalIdx"`
|
||||
Unit string `json:"unit"`
|
||||
}
|
||||
|
||||
// NumElements returns the total number of scalar values in one sample of this signal.
|
||||
func (s SignalInfo) NumElements() int {
|
||||
r := int(s.NumRows)
|
||||
c := int(s.NumCols)
|
||||
if r == 0 {
|
||||
r = 1
|
||||
}
|
||||
if c == 0 {
|
||||
c = 1
|
||||
}
|
||||
return r * c
|
||||
}
|
||||
|
||||
// rawTypeSize returns the byte size for one element of the raw (unquantised) type.
|
||||
func rawTypeSize(typeCode uint8) int {
|
||||
switch typeCode {
|
||||
case 0, 1: // uint8, int8
|
||||
return 1
|
||||
case 2, 3: // uint16, int16
|
||||
return 2
|
||||
case 4, 5: // uint32, int32
|
||||
return 4
|
||||
case 6, 7: // uint64, int64
|
||||
return 8
|
||||
case 8: // float32
|
||||
return 4
|
||||
case 9: // float64
|
||||
return 8
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// quantSize returns the byte size of one quantised element.
|
||||
func quantSize(qt uint8) int {
|
||||
switch qt {
|
||||
case QuantUint8, QuantInt8:
|
||||
return 1
|
||||
case QuantUint16, QuantInt16:
|
||||
return 2
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// readRawElement reads one element at offset and converts it to float64.
|
||||
func readRawElement(b []byte, offset int, typeCode uint8) float64 {
|
||||
switch typeCode {
|
||||
case 0:
|
||||
return float64(b[offset])
|
||||
case 1:
|
||||
return float64(int8(b[offset]))
|
||||
case 2:
|
||||
return float64(binary.LittleEndian.Uint16(b[offset:]))
|
||||
case 3:
|
||||
return float64(int16(binary.LittleEndian.Uint16(b[offset:])))
|
||||
case 4:
|
||||
return float64(binary.LittleEndian.Uint32(b[offset:]))
|
||||
case 5:
|
||||
return float64(int32(binary.LittleEndian.Uint32(b[offset:])))
|
||||
case 6:
|
||||
return float64(binary.LittleEndian.Uint64(b[offset:]))
|
||||
case 7:
|
||||
return float64(int64(binary.LittleEndian.Uint64(b[offset:])))
|
||||
case 8:
|
||||
bits := binary.LittleEndian.Uint32(b[offset:])
|
||||
return float64(math.Float32frombits(bits))
|
||||
case 9:
|
||||
bits := binary.LittleEndian.Uint64(b[offset:])
|
||||
return math.Float64frombits(bits)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// dequantise converts a raw quantised integer to a physical float64.
|
||||
func dequantise(qt uint8, raw uint16, rangeMin, rangeMax float64) float64 {
|
||||
span := rangeMax - rangeMin
|
||||
switch qt {
|
||||
case QuantUint8:
|
||||
return rangeMin + (float64(uint8(raw))/255.0)*span
|
||||
case QuantInt8:
|
||||
return rangeMin + (float64(int8(raw)+127)/254.0)*span
|
||||
case QuantUint16:
|
||||
return rangeMin + (float64(raw)/65535.0)*span
|
||||
case QuantInt16:
|
||||
return rangeMin + (float64(int16(raw)+32767)/65534.0)*span
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// nullTermString converts a zero-padded byte slice to a Go string.
|
||||
func nullTermString(b []byte) string {
|
||||
n := bytes.IndexByte(b, 0)
|
||||
if n < 0 {
|
||||
return string(b)
|
||||
}
|
||||
return string(b[:n])
|
||||
}
|
||||
|
||||
// ─── CONFIG payload parser ────────────────────────────────────────────────────
|
||||
|
||||
// ParseConfig decodes a fully-reassembled CONFIG payload.
|
||||
func ParseConfig(payload []byte) ([]SignalInfo, error) {
|
||||
if len(payload) < 4 {
|
||||
return nil, fmt.Errorf("config payload too short")
|
||||
}
|
||||
numSigs := binary.LittleEndian.Uint32(payload[0:4])
|
||||
offset := 4
|
||||
sigs := make([]SignalInfo, 0, numSigs)
|
||||
for i := uint32(0); i < numSigs; i++ {
|
||||
if offset+SigDescSize > len(payload) {
|
||||
return nil, fmt.Errorf("config payload truncated at signal %d", i)
|
||||
}
|
||||
raw := payload[offset : offset+SigDescSize]
|
||||
si := SignalInfo{
|
||||
Name: nullTermString(raw[0:64]),
|
||||
TypeCode: raw[64],
|
||||
QuantType: raw[65],
|
||||
NumDimensions: raw[66],
|
||||
NumRows: binary.LittleEndian.Uint32(raw[67:71]),
|
||||
NumCols: binary.LittleEndian.Uint32(raw[71:75]),
|
||||
RangeMin: math.Float64frombits(binary.LittleEndian.Uint64(raw[75:83])),
|
||||
RangeMax: math.Float64frombits(binary.LittleEndian.Uint64(raw[83:91])),
|
||||
TimeMode: raw[91],
|
||||
SamplingRate: math.Float64frombits(binary.LittleEndian.Uint64(raw[92:100])),
|
||||
TimeSignalIdx: binary.LittleEndian.Uint32(raw[100:104]),
|
||||
Unit: nullTermString(raw[104:136]),
|
||||
}
|
||||
sigs = append(sigs, si)
|
||||
offset += SigDescSize
|
||||
}
|
||||
return sigs, nil
|
||||
}
|
||||
|
||||
// ─── DATA payload parser ──────────────────────────────────────────────────────
|
||||
|
||||
// DataSample holds the decoded values from one DATA packet.
|
||||
type DataSample struct {
|
||||
HRTTimestamp uint64
|
||||
WallTime time.Time // wall-clock time at UDP arrival; used as x-axis
|
||||
Values map[string][]float64 // key = signal name, value = []float64 with NumElements entries
|
||||
}
|
||||
|
||||
// ParseData decodes a fully-reassembled DATA payload using the provided signal config.
|
||||
// arrivalTime is the wall-clock time at which the packet was received.
|
||||
func ParseData(payload []byte, sigs []SignalInfo, arrivalTime time.Time) (DataSample, error) {
|
||||
if len(payload) < 8 {
|
||||
return DataSample{}, fmt.Errorf("data payload too short")
|
||||
}
|
||||
hrt := binary.LittleEndian.Uint64(payload[0:8])
|
||||
offset := 8
|
||||
|
||||
vals := make(map[string][]float64, len(sigs))
|
||||
for _, sig := range sigs {
|
||||
n := sig.NumElements()
|
||||
elems := make([]float64, n)
|
||||
|
||||
if sig.QuantType == QuantNone {
|
||||
sz := rawTypeSize(sig.TypeCode)
|
||||
needed := n * sz
|
||||
if offset+needed > len(payload) {
|
||||
return DataSample{}, fmt.Errorf("data payload truncated for signal %q", sig.Name)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
elems[i] = readRawElement(payload, offset+i*sz, sig.TypeCode)
|
||||
}
|
||||
offset += needed
|
||||
} else {
|
||||
sz := quantSize(sig.QuantType)
|
||||
needed := n * sz
|
||||
if offset+needed > len(payload) {
|
||||
return DataSample{}, fmt.Errorf("data payload truncated (quant) for signal %q", sig.Name)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
var raw uint16
|
||||
if sz == 1 {
|
||||
raw = uint16(payload[offset+i])
|
||||
} else {
|
||||
raw = binary.LittleEndian.Uint16(payload[offset+i*2:])
|
||||
}
|
||||
elems[i] = dequantise(sig.QuantType, raw, sig.RangeMin, sig.RangeMax)
|
||||
}
|
||||
offset += needed
|
||||
}
|
||||
vals[sig.Name] = elems
|
||||
}
|
||||
return DataSample{HRTTimestamp: hrt, WallTime: arrivalTime, Values: vals}, nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// fragmentSet holds the received fragments for one (counter, type) pair.
|
||||
type fragmentSet struct {
|
||||
total int
|
||||
received int
|
||||
fragments [][]byte // indexed by fragmentIdx
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
// Reassembler reassembles fragmented UDP payloads.
|
||||
// Key: uint64(counter)<<8 | uint64(pktType)
|
||||
type Reassembler struct {
|
||||
mu sync.Mutex
|
||||
sets map[uint64]*fragmentSet
|
||||
expiry time.Duration // how long to keep incomplete sets
|
||||
}
|
||||
|
||||
// NewReassembler creates a Reassembler that expires stale fragment sets after ttl.
|
||||
func NewReassembler(ttl time.Duration) *Reassembler {
|
||||
r := &Reassembler{
|
||||
sets: make(map[uint64]*fragmentSet),
|
||||
expiry: ttl,
|
||||
}
|
||||
go r.gcLoop()
|
||||
return r
|
||||
}
|
||||
|
||||
func reassemblyKey(counter uint32, pktType uint8) uint64 {
|
||||
return uint64(counter)<<8 | uint64(pktType)
|
||||
}
|
||||
|
||||
// AddFragment registers one fragment. Returns the reassembled payload and true
|
||||
// when all fragments for this (counter, type) have been received, otherwise
|
||||
// returns nil, false.
|
||||
func (r *Reassembler) AddFragment(h PacketHeader, payload []byte) ([]byte, bool) {
|
||||
key := reassemblyKey(h.Counter, h.Type)
|
||||
total := int(h.TotalFragments)
|
||||
idx := int(h.FragmentIdx)
|
||||
|
||||
// Fast path: single-fragment packet.
|
||||
if total == 1 && idx == 0 {
|
||||
return payload, true
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
fs, ok := r.sets[key]
|
||||
if !ok {
|
||||
fs = &fragmentSet{
|
||||
total: total,
|
||||
fragments: make([][]byte, total),
|
||||
}
|
||||
r.sets[key] = fs
|
||||
}
|
||||
|
||||
if idx >= len(fs.fragments) {
|
||||
// Stale or corrupt – discard.
|
||||
return nil, false
|
||||
}
|
||||
if fs.fragments[idx] == nil {
|
||||
fs.fragments[idx] = payload
|
||||
fs.received++
|
||||
}
|
||||
fs.lastSeen = time.Now()
|
||||
|
||||
if fs.received < fs.total {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// All fragments received – concatenate in order.
|
||||
delete(r.sets, key)
|
||||
|
||||
total_len := 0
|
||||
for _, f := range fs.fragments {
|
||||
total_len += len(f)
|
||||
}
|
||||
out := make([]byte, 0, total_len)
|
||||
for _, f := range fs.fragments {
|
||||
out = append(out, f...)
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
// gcLoop periodically removes fragment sets that have been incomplete for too long.
|
||||
func (r *Reassembler) gcLoop() {
|
||||
ticker := time.NewTicker(r.expiry / 2)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
r.mu.Lock()
|
||||
now := time.Now()
|
||||
for k, fs := range r.sets {
|
||||
if now.Sub(fs.lastSeen) > r.expiry {
|
||||
delete(r.sets, k)
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
silenceTimeout = 5 * time.Second
|
||||
reconnectDelay = 2 * time.Second
|
||||
readBufSize = 65536
|
||||
)
|
||||
|
||||
// UDPClient manages the UDP connection to the MARTe2 streamer.
|
||||
type UDPClient struct {
|
||||
serverAddr string
|
||||
localPort int
|
||||
hub *Hub
|
||||
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// NewUDPClient creates a UDPClient. Call Run() in a goroutine.
|
||||
func NewUDPClient(serverAddr string, localPort int, hub *Hub) *UDPClient {
|
||||
return &UDPClient{
|
||||
serverAddr: serverAddr,
|
||||
localPort: localPort,
|
||||
hub: hub,
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Stop asks the client to disconnect and exit.
|
||||
func (u *UDPClient) Stop() {
|
||||
close(u.stopCh)
|
||||
}
|
||||
|
||||
// Run is the main loop; it reconnects automatically if the server goes silent.
|
||||
func (u *UDPClient) Run() {
|
||||
for {
|
||||
select {
|
||||
case <-u.stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
log.Printf("udp: connecting to %s (local port %d)", u.serverAddr, u.localPort)
|
||||
err := u.runSession()
|
||||
if err != nil {
|
||||
log.Printf("udp: session ended: %v", err)
|
||||
}
|
||||
|
||||
// Check stop before sleeping.
|
||||
select {
|
||||
case <-u.stopCh:
|
||||
return
|
||||
case <-time.After(reconnectDelay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runSession opens a UDP socket, sends CONNECT, reads data until the server
|
||||
// goes silent or an error occurs.
|
||||
func (u *UDPClient) runSession() error {
|
||||
localAddr := &net.UDPAddr{Port: u.localPort}
|
||||
conn, err := net.ListenUDP("udp4", localAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
serverAddr, err := net.ResolveUDPAddr("udp4", u.serverAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send CONNECT.
|
||||
if _, err := conn.WriteToUDP(BuildConnectPacket(), serverAddr); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("udp: sent CONNECT")
|
||||
|
||||
reassembler := NewReassembler(2 * time.Second)
|
||||
buf := make([]byte, readBufSize)
|
||||
var currentSigs []SignalInfo
|
||||
|
||||
for {
|
||||
// Silence timeout.
|
||||
conn.SetReadDeadline(time.Now().Add(silenceTimeout))
|
||||
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
arrivalTime := time.Now()
|
||||
if err != nil {
|
||||
// Send DISCONNECT best-effort before returning.
|
||||
conn.WriteToUDP(BuildDisconnectPacket(), serverAddr)
|
||||
return err
|
||||
}
|
||||
|
||||
if n < HeaderSize {
|
||||
log.Printf("udp: short datagram (%d bytes), skipping", n)
|
||||
continue
|
||||
}
|
||||
|
||||
hdr, err := ParseHeader(buf[:n])
|
||||
if err != nil {
|
||||
log.Printf("udp: parse header: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
payload := make([]byte, n-HeaderSize)
|
||||
copy(payload, buf[HeaderSize:n])
|
||||
|
||||
complete, ok := reassembler.AddFragment(hdr, payload)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch hdr.Type {
|
||||
case PktConfig:
|
||||
sigs, err := ParseConfig(complete)
|
||||
if err != nil {
|
||||
log.Printf("udp: parse config: %v", err)
|
||||
continue
|
||||
}
|
||||
currentSigs = sigs
|
||||
log.Printf("udp: received CONFIG (%d signals)", len(sigs))
|
||||
u.hub.UpdateConfig(sigs)
|
||||
|
||||
case PktData:
|
||||
if len(currentSigs) == 0 {
|
||||
// We haven't received a CONFIG yet – ignore.
|
||||
continue
|
||||
}
|
||||
sample, err := ParseData(complete, currentSigs, arrivalTime)
|
||||
if err != nil {
|
||||
log.Printf("udp: parse data: %v", err)
|
||||
continue
|
||||
}
|
||||
u.hub.PushData(sample)
|
||||
|
||||
case PktACK:
|
||||
log.Printf("udp: received ACK (counter=%d)", hdr.Counter)
|
||||
|
||||
case PktDisconnect:
|
||||
log.Printf("udp: server sent DISCONNECT")
|
||||
return nil
|
||||
|
||||
default:
|
||||
log.Printf("udp: unknown packet type %d", hdr.Type)
|
||||
}
|
||||
|
||||
// Check stop request.
|
||||
select {
|
||||
case <-u.stopCh:
|
||||
conn.WriteToUDP(BuildDisconnectPacket(), serverAddr)
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
# UDPStreamer Wire Protocol
|
||||
|
||||
This document specifies the binary protocol used between UDPStreamer (server) and any
|
||||
compatible client (the included Go WebUI, a Python script, etc.).
|
||||
|
||||
All multi-byte integers are **little-endian**.
|
||||
|
||||
---
|
||||
|
||||
## Packet Header (17 bytes, packed)
|
||||
|
||||
Every datagram begins with a 17-byte header:
|
||||
|
||||
```
|
||||
Offset Size Type Field
|
||||
────── ──── ────── ────────────────────────────────────────────────────
|
||||
0 4 uint32 magic = 0x53504455 ('UDPS' LE)
|
||||
4 1 uint8 type see Packet Types below
|
||||
5 4 uint32 counter per-update sequence number
|
||||
(same across all fragments of one update)
|
||||
9 2 uint16 fragmentIdx 0-based index of this fragment
|
||||
11 2 uint16 totalFragments number of fragments for this update
|
||||
13 4 uint32 payloadBytes bytes of payload following this header
|
||||
```
|
||||
|
||||
**Total header size:** 17 bytes
|
||||
**Magic:** `0x55 0x44 0x50 0x53` (`UDPS`)
|
||||
|
||||
---
|
||||
|
||||
## Packet Types
|
||||
|
||||
| Value | Direction | Name | Description |
|
||||
|-------|-----------|------|-------------|
|
||||
| 0 | Server → Client | DATA | Signal data (may be fragmented) |
|
||||
| 1 | Server → Client | CONFIG | Signal metadata sent on connect |
|
||||
| 2 | Client → Server | ACK | Acknowledge a data counter (reserved) |
|
||||
| 3 | Client → Server | CONNECT | Request a session |
|
||||
| 4 | Client → Server | DISCONNECT | End the session |
|
||||
|
||||
---
|
||||
|
||||
## Session Flow
|
||||
|
||||
```
|
||||
Client Server
|
||||
────── ──────
|
||||
CONNECT (type=3) →
|
||||
← CONFIG (type=1)
|
||||
← DATA (type=0) ┐
|
||||
← DATA (type=0) │ repeated every RT cycle
|
||||
← DATA (type=0) ┘
|
||||
DISCONNECT (type=4) →
|
||||
```
|
||||
|
||||
1. Client sends a 17-byte CONNECT packet (`payloadBytes = 0`).
|
||||
2. Server responds immediately with one or more CONFIG fragments describing all signals.
|
||||
3. Server sends DATA fragments on every `Synchronise()` call while a client is connected.
|
||||
4. Client sends DISCONNECT to terminate cleanly. A new CONNECT replaces an existing session.
|
||||
|
||||
---
|
||||
|
||||
## CONFIG Payload
|
||||
|
||||
The CONFIG payload is sent as one or more fragmented packets (`type = 1`).
|
||||
After reassembly the layout is:
|
||||
|
||||
```
|
||||
Offset Size Type Field
|
||||
────── ──── ─────── ────────────────────────────────────
|
||||
0 4 uint32 numSignals
|
||||
── for each signal (136 bytes) ──────────────────────────────
|
||||
0 64 char[64] name null-terminated
|
||||
64 1 uint8 typeCode see Type Codes
|
||||
65 1 uint8 quantType see Quantization Types
|
||||
66 1 uint8 numDimensions 0 = scalar, 1 = 1-D array, 2 = matrix
|
||||
67 4 uint32 numRows 0 or 1 for scalar/1-D
|
||||
71 4 uint32 numCols number of elements along fastest axis
|
||||
75 8 float64 rangeMin
|
||||
83 8 float64 rangeMax
|
||||
91 1 uint8 timeMode see Time Modes
|
||||
92 8 float64 samplingRate Hz (0 if PacketTime)
|
||||
100 4 uint32 timeSignalIdx index of the time-reference signal;
|
||||
0xFFFFFFFF = PacketTime (no reference)
|
||||
104 32 char[32] unit null-terminated physical unit string
|
||||
── (total per signal: 136 bytes) ────────────────────────────
|
||||
```
|
||||
|
||||
### Type Codes
|
||||
|
||||
| Code | C type | Bytes/element |
|
||||
|------|--------|---------------|
|
||||
| 0 | uint8 | 1 |
|
||||
| 1 | int8 | 1 |
|
||||
| 2 | uint16 | 2 |
|
||||
| 3 | int16 | 2 |
|
||||
| 4 | uint32 | 4 |
|
||||
| 5 | int32 | 4 |
|
||||
| 6 | uint64 | 8 |
|
||||
| 7 | int64 | 8 |
|
||||
| 8 | float32 | 4 |
|
||||
| 9 | float64 | 8 |
|
||||
|
||||
### Quantization Type Codes (wire side)
|
||||
|
||||
| Code | Wire type | Description |
|
||||
|------|-----------|-------------|
|
||||
| 0 | — | No quantization; raw type as above |
|
||||
| 1 | uint8 | Linear map `[rangeMin, rangeMax]` → `[0, 255]` |
|
||||
| 2 | int8 | Linear map `[rangeMin, rangeMax]` → `[-127, 127]` |
|
||||
| 3 | uint16 | Linear map `[rangeMin, rangeMax]` → `[0, 65535]` |
|
||||
| 4 | int16 | Linear map `[rangeMin, rangeMax]` → `[-32767, 32767]` |
|
||||
|
||||
### Time Mode Codes
|
||||
|
||||
| Code | Name | Meaning |
|
||||
|------|------|---------|
|
||||
| 0 | PacketTime | HRT timestamp at `Synchronise()` — see DATA payload |
|
||||
| 1 | FullArray | `timeSignalIdx` signal has same `numElements`; element `[k]` time = `timeSignal[k]` |
|
||||
| 2 | FirstSample | `timeSignalIdx` is scalar; `t[k] = t[0] + k / samplingRate` |
|
||||
| 3 | LastSample | `timeSignalIdx` is scalar; `t[k] = t[N-1] - (N-1-k) / samplingRate` |
|
||||
|
||||
---
|
||||
|
||||
## DATA Payload
|
||||
|
||||
After reassembly, the DATA payload layout is:
|
||||
|
||||
```
|
||||
Offset Size Type Field
|
||||
────── ──── ────── ────────────────────────────────────────────────────
|
||||
0 8 uint64 hrtTimestamp hardware reference timer count at Synchronise()
|
||||
── for each signal (in config order) ────────────────────────────────────
|
||||
varies N×sz — signal data N = numRows×numCols, sz = element size
|
||||
(wire size if quantized, raw size otherwise)
|
||||
```
|
||||
|
||||
Signal data for quantized signals uses the wire element size (see Quantization Type Codes),
|
||||
not the original MARTe2 type size.
|
||||
|
||||
### Dequantization
|
||||
|
||||
To recover physical values from quantized integers:
|
||||
|
||||
```
|
||||
// uint16 → float
|
||||
span = rangeMax - rangeMin
|
||||
physical = rangeMin + (wire_uint16 / 65535.0) × span
|
||||
|
||||
// int16 → float
|
||||
physical = rangeMin + ((wire_int16 + 32767) / 65534.0) × span
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fragmentation
|
||||
|
||||
When a payload exceeds `MaxPayloadSize` bytes, it is split into fragments:
|
||||
|
||||
```
|
||||
chunkSize = MaxPayloadSize - 17 // usable bytes per datagram
|
||||
numFragments = ceil(payloadSize / chunkSize)
|
||||
```
|
||||
|
||||
Fragment `i` carries bytes `[i × chunkSize .. min((i+1) × chunkSize, payloadSize))`.
|
||||
All fragments share the same `counter`; `fragmentIdx` and `totalFragments` allow
|
||||
the client to reassemble them in any order.
|
||||
|
||||
**Example:** `MaxPayloadSize = 1400`, payload = 8016 B
|
||||
→ `chunkSize = 1383`, `numFragments = ceil(8016/1383) = 6`
|
||||
|
||||
---
|
||||
|
||||
## Minimal Python Client Example
|
||||
|
||||
```python
|
||||
import socket, struct, time
|
||||
|
||||
MAGIC = 0x53504455
|
||||
HDR_FMT = '<IBHHI' # magic, type, counter, fragIdx, totalFrags, payloadBytes
|
||||
HDR_SIZE = 17
|
||||
|
||||
def build_connect():
|
||||
return struct.pack(HDR_FMT, MAGIC, 3, 0, 0, 1, 0)
|
||||
|
||||
def parse_header(data):
|
||||
return struct.unpack_from(HDR_FMT, data)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(('', 44900))
|
||||
sock.sendto(build_connect(), ('127.0.0.1', 44500))
|
||||
sock.settimeout(5.0)
|
||||
|
||||
fragments = {}
|
||||
while True:
|
||||
data, _ = sock.recvfrom(65536)
|
||||
magic, ptype, counter, frag_idx, total_frags, payload_bytes = parse_header(data)
|
||||
payload = data[HDR_SIZE:]
|
||||
|
||||
if ptype == 1: # CONFIG
|
||||
print(f"CONFIG fragment {frag_idx+1}/{total_frags}")
|
||||
elif ptype == 0: # DATA
|
||||
fragments.setdefault(counter, {})[frag_idx] = payload
|
||||
if len(fragments[counter]) == total_frags:
|
||||
full = b''.join(fragments.pop(counter)[i] for i in range(total_frags))
|
||||
hrt = struct.unpack_from('<Q', full)[0]
|
||||
print(f"DATA counter={counter} hrt={hrt} payload={len(full)}B")
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
# SineArrayGAM
|
||||
|
||||
`SineArrayGAM` is a helper GAM bundled with the UDPStreamer library. It fills a `float32`
|
||||
output array with a continuous sinusoidal waveform, maintaining phase continuity across
|
||||
RT cycles. It is intended for testing and demonstrating the UDPStreamer's packed-burst
|
||||
signal capability.
|
||||
|
||||
> **Library:** The class is compiled into `UDPStreamer.so` alongside the UDPStreamer
|
||||
> DataSource. A `SineArrayGAM.so → UDPStreamer.so` symlink is required for MARTe2's
|
||||
> auto-loader (created automatically by `Test/MARTeApp/run.sh`).
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```
|
||||
+Ch1GAM = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0 // Signal frequency [Hz] (default: 1.0)
|
||||
Amplitude = 1.0 // Peak amplitude (default: 1.0)
|
||||
Offset = 0.0 // DC offset (default: 0.0)
|
||||
Phase = 0.0 // Initial phase [radians] (default: 0.0)
|
||||
SamplingRate = 1000000.0 // Sample rate [Hz] (default: 1000000.0)
|
||||
// Must match the SamplingRate declared in the
|
||||
// UDPStreamer signal config.
|
||||
|
||||
OutputSignals = {
|
||||
Ch1 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000 // N samples per RT cycle
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `Frequency` | float64 | 1.0 | Signal frequency in Hz |
|
||||
| `Amplitude` | float64 | 1.0 | Peak amplitude (half peak-to-peak) |
|
||||
| `Offset` | float64 | 0.0 | DC offset added to every sample |
|
||||
| `Phase` | float64 | 0.0 | Phase shift in radians |
|
||||
| `SamplingRate` | float64 | 1 000 000.0 | Sample rate in Hz; must be > 0 |
|
||||
|
||||
### Output signal
|
||||
|
||||
Exactly **one** output signal of type **`float32`** is required. The signal must have
|
||||
`NumberOfDimensions = 1` and `NumberOfElements = N` where N ≥ 1.
|
||||
|
||||
---
|
||||
|
||||
## Waveform formula
|
||||
|
||||
Each `Execute()` call fills elements `[0 .. N-1]` using:
|
||||
|
||||
```
|
||||
v[k] = Amplitude × sin(2π × Frequency × (offset_total + k) / SamplingRate + Phase) + Offset
|
||||
```
|
||||
|
||||
where `offset_total` is a cumulative sample counter that increments by `N` after each call.
|
||||
This guarantees a gapless, phase-continuous waveform across RT cycles.
|
||||
|
||||
---
|
||||
|
||||
## Bandwidth and fragmentation
|
||||
|
||||
For a 1 MSps burst at 10 kHz RT rate with 1000 samples per cycle:
|
||||
|
||||
```
|
||||
N = SamplingRate / RT_rate = 1 000 000 / 1000 = 1000 samples per cycle
|
||||
Wire bytes = 1000 × 4 (float32) = 4000 B per channel per cycle
|
||||
At 10 kHz: 4000 × 10 000 = 40 MB/s raw (UDP, before quantization)
|
||||
```
|
||||
|
||||
Using `QuantizedType = uint16` halves the wire bandwidth to 20 MB/s.
|
||||
With `MaxPayloadSize = 1400` the 4 000-byte payload spans 3 UDP datagrams
|
||||
(`ceil(4008 / 1383) = 3`).
|
||||
|
||||
---
|
||||
|
||||
## Example: two-channel quadrature pair
|
||||
|
||||
```
|
||||
+Ch1GAM = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0; Amplitude = 1.0; Phase = 0.0; SamplingRate = 10000000.0
|
||||
OutputSignals = {
|
||||
Ch1 = { DataSource = DDB; Type = float32; NumberOfDimensions = 1; NumberOfElements = 1000 }
|
||||
}
|
||||
}
|
||||
|
||||
+Ch2GAM = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0; Amplitude = 1.0; Phase = 1.5708; SamplingRate = 10000000.0
|
||||
OutputSignals = {
|
||||
Ch2 = { DataSource = DDB; Type = float32; NumberOfDimensions = 1; NumberOfElements = 1000 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This produces two signals 90° out of phase, useful for verifying IQ-style signal chains.
|
||||
@@ -0,0 +1,359 @@
|
||||
# Tutorial: Streaming Signals with UDPStreamer
|
||||
|
||||
This tutorial walks you through:
|
||||
|
||||
1. Setting up the environment
|
||||
2. Running the demo application
|
||||
3. Visualising signals in the browser
|
||||
4. Adding UDPStreamer to your own MARTe2 application
|
||||
5. Streaming high-frequency packed signals
|
||||
|
||||
**Prerequisites:** MARTe2 and MARTe2-components must already be built.
|
||||
See the [MARTe2 installation guide](https://vcis.f4e.europa.eu/marte2-docs/) if needed.
|
||||
|
||||
---
|
||||
|
||||
## 1. Environment Setup
|
||||
|
||||
Edit `marte_env.sh` in the repository root to point at your MARTe2 installations:
|
||||
|
||||
```bash
|
||||
# marte_env.sh (key variables)
|
||||
export MARTe2_DIR="$HOME/workspace/MARTe2"
|
||||
export MARTe2_Components_DIR="$HOME/workspace/MARTe2-components"
|
||||
```
|
||||
|
||||
Then source it in your shell:
|
||||
|
||||
```bash
|
||||
cd /path/to/MARTe_IO_components
|
||||
source marte_env.sh
|
||||
```
|
||||
|
||||
Verify the environment is correct:
|
||||
|
||||
```bash
|
||||
echo $MARTe2_DIR
|
||||
ls $MARTe2_DIR/Build/x86-linux/App/MARTeApp.ex # should exist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Running the Demo Application
|
||||
|
||||
The demo is in `Test/MARTeApp/`. It runs a 10 kHz MARTe2 application that streams:
|
||||
|
||||
- **Counter** and **Time** — scalar counters from the Linux timer
|
||||
- **Sine1** — 1 Hz sine wave (float32, amplitude 10, quantized to uint16 on wire)
|
||||
- **Sine2** — 0.3 Hz sine wave (float32, amplitude 5, raw float32 on wire)
|
||||
- **Ch1**, **Ch2** — 1 kHz sine bursts packed as 1000 samples/packet (10 MSps)
|
||||
|
||||
Start everything with one command:
|
||||
|
||||
```bash
|
||||
cd Test/MARTeApp
|
||||
./run.sh --webui
|
||||
```
|
||||
|
||||
The script will:
|
||||
1. Build the UDPStreamer shared library.
|
||||
2. Build the Go WebUI binary (first run only).
|
||||
3. Start the WebUI relay on `http://localhost:8080`.
|
||||
4. Launch the MARTe2 application.
|
||||
|
||||
Press `Ctrl+C` to stop both processes.
|
||||
|
||||
---
|
||||
|
||||
## 3. Visualising Signals in the Browser
|
||||
|
||||
Open `http://localhost:8080` in any modern browser.
|
||||
|
||||
### Add your first plot
|
||||
|
||||
1. Click **+ Add Plot** in the toolbar.
|
||||
2. A blank plot panel appears with a "Drop signals here" hint.
|
||||
|
||||
### Plot a signal
|
||||
|
||||
1. In the left sidebar find **Sine1** (listed as `Sine1 · f32`).
|
||||
2. Click and drag it onto the plot panel.
|
||||
3. The sine wave appears immediately.
|
||||
|
||||
### Overlay multiple signals
|
||||
|
||||
Drag **Sine2** onto the same plot — it is added as a second trace.
|
||||
|
||||
### Adjust the time window
|
||||
|
||||
Use the **Window** dropdown in the top bar to change the rolling display window
|
||||
(1 s, 5 s, 10 s, 30 s, 60 s).
|
||||
|
||||
### Plot layout
|
||||
|
||||
Use the layout buttons (`1×1`, `2×1`, `2×2`, …) to split the screen into multiple
|
||||
plot panels. Each panel is independent — drag different signals onto each.
|
||||
|
||||
### High-frequency signals
|
||||
|
||||
Drag **Ch1** (shown as `Ch1 · [1000] f32`) onto a plot. Each UDP packet carries
|
||||
1000 samples at 10 MSps; the WebUI reconstructs per-sample timestamps and displays
|
||||
the continuous waveform.
|
||||
|
||||
### Export data
|
||||
|
||||
Click **⬇** on any plot to download the visible window as a CSV file.
|
||||
|
||||
---
|
||||
|
||||
## 4. Adding UDPStreamer to Your Own Application
|
||||
|
||||
### Step 1 — Declare the DataSource
|
||||
|
||||
Add UDPStreamer to the `+Data` section of your MARTe2 configuration:
|
||||
|
||||
```
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB
|
||||
|
||||
+DDB = { Class = GAMDataSource }
|
||||
|
||||
+Streamer = {
|
||||
Class = UDPStreamer
|
||||
Port = 44500
|
||||
MaxPayloadSize = 1400
|
||||
|
||||
Signals = {
|
||||
Voltage = {
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
RangeMin = -10.0
|
||||
RangeMax = 10.0
|
||||
QuantizedType = uint16 // 16-bit quantized on wire
|
||||
}
|
||||
Current = {
|
||||
Type = float32
|
||||
Unit = "A"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+Timings = { Class = TimingDataSource }
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 — Route signals with IOGAM
|
||||
|
||||
Use IOGAM to copy signals from your inter-GAM DDB into the Streamer:
|
||||
|
||||
```
|
||||
+StreamerGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Voltage = { DataSource = DDB; Type = float32 }
|
||||
Current = { DataSource = DDB; Type = float32 }
|
||||
}
|
||||
OutputSignals = {
|
||||
Voltage = { DataSource = Streamer; Type = float32 }
|
||||
Current = { DataSource = Streamer; Type = float32 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add `StreamerGAM` at the **end** of the thread's `Functions` list so it runs after
|
||||
your control GAMs have written their outputs.
|
||||
|
||||
### Step 3 — Add the library to LD_LIBRARY_PATH
|
||||
|
||||
In your run script, add the UDPStreamer build directory:
|
||||
|
||||
```bash
|
||||
export LD_LIBRARY_PATH="/path/to/Build/x86-linux/Components/DataSources/UDPStreamer:$LD_LIBRARY_PATH"
|
||||
```
|
||||
|
||||
### Step 4 — Start the WebUI and connect
|
||||
|
||||
```bash
|
||||
# From the Client/WebUI directory:
|
||||
./udpstreamer-webui --streamer 127.0.0.1:44500 --listen :8080 --clientport 44900
|
||||
```
|
||||
|
||||
Open `http://localhost:8080`, drag your signals onto a plot, and you're done.
|
||||
|
||||
---
|
||||
|
||||
## 5. Streaming High-Frequency Packed Signals
|
||||
|
||||
This section shows how to stream 1000 samples per RT cycle at 1 MSps.
|
||||
|
||||
### Overview
|
||||
|
||||
At 1 kHz RT rate with 1000 samples per cycle the effective sample rate is 1 MSps.
|
||||
Each UDP packet carries a burst of 1000 samples; the client reconstructs timestamps
|
||||
using the anchor timestamp and `SamplingRate`.
|
||||
|
||||
### Step 1 — Generate burst data with SineArrayGAM
|
||||
|
||||
`SineArrayGAM` (bundled in `UDPStreamer.so`) produces a continuous float32 array:
|
||||
|
||||
```
|
||||
+Ch1GAM = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0 // 1 kHz signal
|
||||
Amplitude = 1.0
|
||||
Phase = 0.0
|
||||
SamplingRate = 1000000.0 // must match Streamer config below
|
||||
OutputSignals = {
|
||||
Ch1 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2 — Add a time reference signal
|
||||
|
||||
Add a scalar time signal that will anchor the first sample's timestamp:
|
||||
|
||||
```
|
||||
+TimerGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Time = { DataSource = Timer; Type = uint32; Frequency = 1000 }
|
||||
}
|
||||
OutputSignals = {
|
||||
Time = { DataSource = DDB; Type = uint32 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3 — Configure UDPStreamer for packed signals
|
||||
|
||||
```
|
||||
+Streamer = {
|
||||
Class = UDPStreamer
|
||||
Port = 44500
|
||||
MaxPayloadSize = 1400
|
||||
|
||||
Signals = {
|
||||
Time = { Type = uint32; Unit = "us" } // time reference (scalar)
|
||||
|
||||
Ch1 = {
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
Unit = "V"
|
||||
TimeMode = FirstSample // Time = timestamp of first sample
|
||||
TimeSignal = Time
|
||||
SamplingRate = 1000000.0 // Hz
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4 — Wire everything in the thread
|
||||
|
||||
```
|
||||
+Thread1 = {
|
||||
Class = RealTimeThread
|
||||
Functions = { TimerGAM Ch1GAM StreamerGAM }
|
||||
}
|
||||
```
|
||||
|
||||
Where `StreamerGAM` is the IOGAM that copies `Time` and `Ch1` from DDB to Streamer.
|
||||
|
||||
### Step 5 — Fragmentation note
|
||||
|
||||
A single 1000-element float32 channel plus a uint32 time signal produces:
|
||||
|
||||
```
|
||||
payload = 8 B (HRT) + 4 B (Time/uint32) + 4000 B (float32×1000) = 4012 B
|
||||
```
|
||||
|
||||
With `MaxPayloadSize = 1400`:
|
||||
|
||||
```
|
||||
fragments = ceil(4012 / 1383) = 3 datagrams per cycle
|
||||
```
|
||||
|
||||
At 1 kHz that is 3000 UDP datagrams/second per channel — well within typical LAN capacity.
|
||||
|
||||
### Step 6 — Create the SineArrayGAM symlink
|
||||
|
||||
MARTe2 tries to `dlopen("SineArrayGAM.so")` the first time it encounters the class.
|
||||
Create the symlink in your build directory:
|
||||
|
||||
```bash
|
||||
UDPSTREAMER_LIB=/path/to/Build/x86-linux/Components/DataSources/UDPStreamer
|
||||
ln -sf "${UDPSTREAMER_LIB}/UDPStreamer.so" "${UDPSTREAMER_LIB}/SineArrayGAM.so"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Writing a Custom UDP Client
|
||||
|
||||
A minimal Python client that receives and prints signal data:
|
||||
|
||||
```python
|
||||
import socket, struct
|
||||
|
||||
MAGIC = 0x53504455
|
||||
HDR = struct.Struct('<IBHHI') # 17 bytes: magic, type, counter, fragIdx, total, payloadBytes
|
||||
SERVER = ('127.0.0.1', 44500)
|
||||
MY_PORT = 44900
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.bind(('', MY_PORT))
|
||||
sock.settimeout(5.0)
|
||||
|
||||
# Send CONNECT
|
||||
sock.sendto(HDR.pack(MAGIC, 3, 0, 0, 1, 0), SERVER)
|
||||
print("CONNECT sent")
|
||||
|
||||
reassembly = {}
|
||||
|
||||
while True:
|
||||
data, _ = sock.recvfrom(65536)
|
||||
if len(data) < 17:
|
||||
continue
|
||||
magic, ptype, counter, frag_idx, total_frags, payload_bytes = HDR.unpack_from(data)
|
||||
if magic != MAGIC:
|
||||
continue
|
||||
payload = data[17:17 + payload_bytes]
|
||||
|
||||
# Accumulate fragments
|
||||
bucket = reassembly.setdefault((ptype, counter), {})
|
||||
bucket[frag_idx] = payload
|
||||
if len(bucket) < total_frags:
|
||||
continue
|
||||
full = b''.join(bucket[i] for i in range(total_frags))
|
||||
del reassembly[(ptype, counter)]
|
||||
|
||||
if ptype == 1: # CONFIG
|
||||
num_sigs = struct.unpack_from('<I', full)[0]
|
||||
print(f"CONFIG: {num_sigs} signals")
|
||||
elif ptype == 0: # DATA
|
||||
hrt = struct.unpack_from('<Q', full)[0]
|
||||
print(f"DATA counter={counter} hrt={hrt} payload={len(full)}B")
|
||||
```
|
||||
|
||||
For a full-featured client with CONFIG parsing and dequantization see
|
||||
[`Client/WebUI/protocol.go`](../Client/WebUI/protocol.go) (Go)
|
||||
and the [Protocol reference](Protocol.md).
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `Failed dlopen(): UDPStreamer.so` | Library not in `LD_LIBRARY_PATH` | Add build dir to `LD_LIBRARY_PATH` |
|
||||
| `Failed dlopen(): WaveformSin.so` | Symlink missing | Run `run.sh`; it creates the symlinks automatically |
|
||||
| `Failed dlopen(): SineArrayGAM.so` | Symlink missing | Create `SineArrayGAM.so → UDPStreamer.so` in the build dir |
|
||||
| WebUI shows "No data for Xs" | UDPStreamer not running / wrong port | Check port numbers; check MARTe2 logs |
|
||||
| Plots only show ~167 ms of HF data | Browser buffer too small | Use `TEMPORAL_CAP` in JS (already 600 000 in current WebUI) |
|
||||
| Fragmentation error / missing data | MTU too small | Reduce `MaxPayloadSize` to 1200 or smaller |
|
||||
@@ -0,0 +1,200 @@
|
||||
# UDPStreamer DataSource
|
||||
|
||||
`UDPStreamer` is a MARTe2 output DataSource that streams signals from a real-time application
|
||||
to a single connected UDP client. It is fully asynchronous from the RT thread: the RT cycle
|
||||
only performs a fast spinlock + memcpy, while all network I/O runs on a dedicated background
|
||||
thread.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Zero-copy RT path** — `Synchronise()` only locks, copies signal memory, and posts a semaphore.
|
||||
- **Single-client model** — one client at a time; a new CONNECT replaces the previous session.
|
||||
- **Packet fragmentation** — large payloads are split into ≤ `MaxPayloadSize`-byte datagrams,
|
||||
each with a header carrying fragment index and total count so the client can reassemble them.
|
||||
- **Signal quantization** — `float32`/`float64` signals can be linearly quantized to
|
||||
`uint8`, `int8`, `uint16`, or `int16` on the wire, reducing bandwidth significantly.
|
||||
- **Temporal arrays** — signals with `NumberOfElements > 1` can carry per-sample time
|
||||
metadata via `TimeMode` and `TimeSignal`, enabling high-frequency burst transmission
|
||||
(e.g. 1 000 samples per RT cycle at 1 MSps).
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```
|
||||
+Streamer = {
|
||||
Class = UDPStreamer
|
||||
|
||||
// Network
|
||||
Port = 44500 // UDP port the server listens on (default: 44500)
|
||||
MaxPayloadSize = 1400 // Maximum bytes per UDP datagram (default: 1400)
|
||||
// Must be > 17 (header size). Tune for MTU.
|
||||
|
||||
// Background thread (optional)
|
||||
CPUMask = 0x2 // CPU affinity mask for the network thread
|
||||
StackSize = 1048576 // Stack size in bytes (default: 1 MiB)
|
||||
|
||||
Signals = {
|
||||
// ── Scalar signal ────────────────────────────────────────────────────
|
||||
Time = {
|
||||
Type = uint32
|
||||
Unit = "us" // Optional: physical unit string (informational)
|
||||
}
|
||||
|
||||
// ── Float signal with quantization ───────────────────────────────────
|
||||
Pressure = {
|
||||
Type = float32
|
||||
Unit = "Pa"
|
||||
RangeMin = 0.0 // Required when QuantizedType is set
|
||||
RangeMax = 1000000.0 // Required when QuantizedType is set
|
||||
QuantizedType = uint16 // none | uint8 | int8 | uint16 | int16
|
||||
}
|
||||
|
||||
// ── Temporal array (packed burst) ────────────────────────────────────
|
||||
Channel1 = {
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000 // N samples per RT cycle
|
||||
Unit = "V"
|
||||
TimeMode = FirstSample // see Time Modes below
|
||||
TimeSignal = Time // name of a scalar signal in this DataSource
|
||||
SamplingRate = 1000000.0 // Hz — used by client to reconstruct timestamps
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Top-level Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `Port` | uint16 | 44500 | UDP server port |
|
||||
| `MaxPayloadSize` | uint32 | 1400 | Max payload bytes per UDP datagram (min 18) |
|
||||
| `CPUMask` | uint32 | 0 (any) | Background thread CPU affinity |
|
||||
| `StackSize` | uint32 | 1 048 576 | Background thread stack size in bytes |
|
||||
|
||||
### Per-signal Parameters
|
||||
|
||||
| Parameter | Type | Default | Applies to |
|
||||
|-----------|------|---------|------------|
|
||||
| `Unit` | string | `""` | Any type — informational, forwarded to client in CONFIG |
|
||||
| `RangeMin` | float64 | 0.0 | float32/float64 with `QuantizedType` |
|
||||
| `RangeMax` | float64 | 1.0 | float32/float64 with `QuantizedType` |
|
||||
| `QuantizedType` | string | `none` | float32/float64 only |
|
||||
| `TimeMode` | string | `PacketTime` | Signals with `NumberOfElements > 1` |
|
||||
| `TimeSignal` | string | — | Required when `TimeMode` ≠ `PacketTime` |
|
||||
| `SamplingRate` | float64 | 0.0 | Required when `TimeMode` = `FirstSample` or `LastSample` |
|
||||
|
||||
### Quantization Types
|
||||
|
||||
| Value | Wire type | Bit depth | Notes |
|
||||
|-------|-----------|-----------|-------|
|
||||
| `none` | same as source | — | Raw copy, no quantization |
|
||||
| `uint8` | uint8 | 8-bit | Maps `[RangeMin, RangeMax]` → `[0, 255]` |
|
||||
| `int8` | int8 | 8-bit | Maps `[RangeMin, RangeMax]` → `[-127, 127]` |
|
||||
| `uint16` | uint16 | 16-bit | Maps `[RangeMin, RangeMax]` → `[0, 65 535]` |
|
||||
| `int16` | int16 | 16-bit | Maps `[RangeMin, RangeMax]` → `[-32 767, 32 767]` |
|
||||
|
||||
Quantization formula (unsigned, e.g. uint16):
|
||||
|
||||
```
|
||||
normalized = clamp((value - RangeMin) / (RangeMax - RangeMin), 0.0, 1.0)
|
||||
wire_value = (uint16)(normalized × 65535)
|
||||
```
|
||||
|
||||
### Time Modes
|
||||
|
||||
| Value | Meaning | Requirements |
|
||||
|-------|---------|--------------|
|
||||
| `PacketTime` | The HRT counter captured at `Synchronise()` time is used as the packet timestamp. No per-signal time metadata. | — |
|
||||
| `FullArray` | `TimeSignal` carries one timestamp per element (same `NumberOfElements`). | `TimeSignal` must have the same `NumberOfElements`. |
|
||||
| `FirstSample` | `TimeSignal` is a scalar giving the timestamp of element `[0]`. Elements `[1..N-1]` are inferred at `1/SamplingRate` intervals. | Scalar `TimeSignal`; `SamplingRate > 0`. |
|
||||
| `LastSample` | Same as `FirstSample` but `TimeSignal` is the timestamp of element `[N-1]`. | Scalar `TimeSignal`; `SamplingRate > 0`. |
|
||||
|
||||
---
|
||||
|
||||
## Broker
|
||||
|
||||
UDPStreamer uses `MemoryMapSynchronisedOutputBroker` for output signals. This broker is
|
||||
called automatically by the MARTe2 scheduler after all GAMs in the thread have executed.
|
||||
|
||||
> **Note:** Input signals are not supported — `GetBrokerName()` returns `""` for
|
||||
> `InputSignals`.
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
PrepareNextState() ← opens UDP server socket, starts background thread
|
||||
↓
|
||||
[RT thread, each cycle]
|
||||
GAMs execute ← write into Streamer signal memory via broker
|
||||
Synchronise() ← spinlock + memcpy to readyBuffer + post dataSem
|
||||
↓
|
||||
[Background thread]
|
||||
Poll serverSocket ← receive CONNECT / DISCONNECT / ACK
|
||||
Wait dataSem ← woken by Synchronise()
|
||||
QuantizeAndSerialize() ← build wire payload
|
||||
SendFragmented() ← send DATA fragments to client
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Notes
|
||||
|
||||
- The RT path (`Synchronise()`) performs only: `FastLock()` + `memcpy` + `FastUnLock()` + `EventSem.Post()`.
|
||||
No socket calls, no heap allocation.
|
||||
- `readyBuffer` and `wireBuffer` are allocated once in `AllocateMemory()`.
|
||||
- If no client is connected the background thread skips serialisation entirely.
|
||||
- Packet loss is tolerated silently. ACK tracking is reserved for future use.
|
||||
|
||||
---
|
||||
|
||||
## Example: minimal scalar streaming
|
||||
|
||||
```
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
|
||||
+Streamer = {
|
||||
Class = UDPStreamer
|
||||
Port = 44500
|
||||
Signals = {
|
||||
Counter = { Type = uint32 }
|
||||
Voltage = { Type = float32; Unit = "V"; RangeMin = -10.0; RangeMax = 10.0; QuantizedType = uint16 }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example: high-frequency burst
|
||||
|
||||
```
|
||||
+Streamer = {
|
||||
Class = UDPStreamer
|
||||
Port = 44500
|
||||
MaxPayloadSize = 1400
|
||||
|
||||
Signals = {
|
||||
T0 = { Type = uint32; Unit = "us" }
|
||||
|
||||
Ch1 = {
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000 // 1000 samples per RT cycle
|
||||
Unit = "V"
|
||||
TimeMode = FirstSample
|
||||
TimeSignal = T0
|
||||
SamplingRate = 1000000.0 // 1 MSps
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With `MaxPayloadSize = 1400`, a single 1000-element float32 signal produces:
|
||||
|
||||
```
|
||||
payload = 8 B (HRT timestamp) + 4 B (T0/uint32) + 4000 B (float32×1000) = 4012 B
|
||||
fragments = ceil(4012 / 1383) = 3
|
||||
```
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
# WebUI Client
|
||||
|
||||
The WebUI is a Go binary that acts as a bridge between UDPStreamer and any web browser.
|
||||
It receives UDP packets from UDPStreamer, reassembles fragmented data, and re-publishes
|
||||
decoded signal values over a WebSocket to the browser. The browser renders live plots
|
||||
using [Plotly.js](https://plotly.com/javascript/).
|
||||
|
||||
```
|
||||
MARTe2 RT app
|
||||
│ UDP (binary protocol)
|
||||
▼
|
||||
udpstreamer-webui (Go)
|
||||
│ WebSocket (JSON)
|
||||
▼
|
||||
Browser (index.html + Plotly.js)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cd Client/WebUI
|
||||
go build -o udpstreamer-webui ./...
|
||||
```
|
||||
|
||||
Requires Go ≥ 1.21. The only external dependency is `gorilla/websocket`
|
||||
(declared in `go.mod`; fetched automatically by `go build`).
|
||||
|
||||
---
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
./udpstreamer-webui \
|
||||
--streamer 127.0.0.1:44500 \ # address:port of the UDPStreamer server
|
||||
--listen :8080 \ # HTTP / WebSocket listen address
|
||||
--clientport 44900 # local UDP port for receiving from streamer
|
||||
```
|
||||
|
||||
Open `http://localhost:8080` in any modern browser.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--streamer` | `127.0.0.1:44500` | UDP address of the UDPStreamer DataSource |
|
||||
| `--listen` | `:8080` | HTTP server bind address |
|
||||
| `--clientport` | `44900` | Local UDP port for receiving data |
|
||||
|
||||
---
|
||||
|
||||
## Browser UI
|
||||
|
||||
### Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ MARTe2 UDP Streamer ● Streaming Window: [5 s▾] ⏸ │ ← top bar
|
||||
├──────────┬──────────────────────────────────────────────┤
|
||||
│ Signals │ Plots [1×1][2×1][2×2]… [+Add] │
|
||||
│──────────│──────────────────────────────────────────────│
|
||||
│ Counter │ ┌─────────────────┐ ┌──────────────────┐ │
|
||||
│ Time │ │ Plot 1 │ │ Plot 2 │ │
|
||||
│ Sine1 │ │ (drop signals) │ │ (drop signals) │ │
|
||||
│ Sine2 │ │ │ │ │ │
|
||||
│ Ch1[1000]│ └─────────────────┘ └──────────────────┘ │
|
||||
│ Ch2[1000]│ │
|
||||
└──────────┴──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Signal Sidebar
|
||||
|
||||
Signals received in the CONFIG packet are listed in the sidebar:
|
||||
|
||||
- **Scalar signals** — appear as single draggable items (e.g. `Sine1 · f32`).
|
||||
- **Temporal arrays** — high-frequency burst signals with `TimeMode ≠ PacketTime`.
|
||||
Displayed as a single draggable item showing element count: `Ch1 · [1000] f32`.
|
||||
- **Spatial arrays** — `TimeMode = PacketTime` arrays are shown as an expandable
|
||||
group; individual elements (`Ch1[0]`, `Ch1[1]`, …) can be dragged independently.
|
||||
|
||||
### Adding Plots
|
||||
|
||||
1. Click **+ Add Plot** in the toolbar.
|
||||
2. Drag a signal from the sidebar onto a plot panel.
|
||||
3. Multiple signals can be overlaid on the same plot.
|
||||
4. Click the **×** badge to remove a trace.
|
||||
|
||||
### Plot Controls
|
||||
|
||||
| Control | Action |
|
||||
|---------|--------|
|
||||
| **⏸ / ▶** (per plot) | Pause / resume that plot; paused plots allow zoom and pan |
|
||||
| **⬇** (per plot) | Export all visible traces to CSV |
|
||||
| **🗑** (per plot) | Delete the plot |
|
||||
| **⏸ Pause All** (top bar) | Pause all plots simultaneously |
|
||||
| **Window** (top bar) | Adjust the rolling time window (1 s – 60 s) |
|
||||
| Layout buttons | Switch between 1×1, 2×1, 1×2, 2×2, 3×1, … grid layouts |
|
||||
| Sidebar **←** | Collapse the signal list to maximise plot area |
|
||||
|
||||
### Status LED
|
||||
|
||||
| Colour | Meaning |
|
||||
|--------|---------|
|
||||
| Red (solid) | No WebSocket connection to browser |
|
||||
| Orange (pulsing) | WebSocket connected but no data received in > 1 s |
|
||||
| Green (pulsing) | Data is being received normally |
|
||||
|
||||
---
|
||||
|
||||
## Architecture (Go side)
|
||||
|
||||
### Goroutines
|
||||
|
||||
```
|
||||
main()
|
||||
├── hub.Run() ← event loop: register/unregister WS clients, batch data at 30 Hz
|
||||
├── udpClient.Run() ← reconnects on silence; parses UDP packets; feeds hub.dataCh
|
||||
└── http.ListenAndServe()
|
||||
├── GET / ← serves embedded index.html
|
||||
└── GET /ws ← upgrades to WebSocket; launches per-client read/write pumps
|
||||
```
|
||||
|
||||
### WebSocket Message Format
|
||||
|
||||
All messages are JSON. Two types are sent from server to browser:
|
||||
|
||||
#### `config` message
|
||||
|
||||
Sent immediately when a browser client connects (if a CONFIG has been received from
|
||||
UDPStreamer) and whenever UDPStreamer sends a new CONFIG packet.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "config",
|
||||
"signals": [
|
||||
{
|
||||
"name": "Sine1",
|
||||
"typeCode": 8,
|
||||
"quantType": 3,
|
||||
"numDimensions": 0,
|
||||
"numRows": 1,
|
||||
"numCols": 1,
|
||||
"rangeMin": -10.0,
|
||||
"rangeMax": 10.0,
|
||||
"timeMode": 0,
|
||||
"samplingRate": 0.0,
|
||||
"timeSignalIdx": 4294967295,
|
||||
"unit": "V"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `data` message
|
||||
|
||||
Sent at ≤ 30 Hz, batching all UDP packets received since the last tick.
|
||||
Each signal carries its own time axis to support mixed scalar + temporal-array signals.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "data",
|
||||
"signals": {
|
||||
"Sine1": { "t": [1747123456.001, 1747123456.002], "v": [3.14, 2.71] },
|
||||
"Counter": { "t": [1747123456.001, 1747123456.002], "v": [12340, 12341] },
|
||||
"Ch1": { "t": [1747123456.000, 1747123456.0001, ...], "v": [0.12, 0.13, ...] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `t` — Unix timestamp in seconds (float64) for each sample.
|
||||
- `v` — physical value (after dequantization if applicable).
|
||||
- For **temporal arrays**, `t` and `v` have `N × batchSize` entries
|
||||
(up to 2000 per 30 Hz tick after server-side decimation).
|
||||
- For **spatial arrays** the keys are `"Ch1[0]"`, `"Ch1[1]"`, etc.
|
||||
|
||||
---
|
||||
|
||||
## Reconnection Behaviour
|
||||
|
||||
- **UDP reconnect:** The Go client reconnects to UDPStreamer automatically after 5 s
|
||||
of silence. This handles MARTe2 restarts transparently.
|
||||
- **WebSocket keepalive:** The server sends a WebSocket ping every 30 s. The browser
|
||||
auto-responds; if no pong is received within 10 s the connection is closed and the
|
||||
browser reconnects with exponential backoff (starting at 1 s, capped at 30 s).
|
||||
- **Buffer preservation:** Browser-side signal buffers are only reset when the signal
|
||||
layout changes (name, type, or dimensions differ). A reconnect with the same CONFIG
|
||||
keeps existing data visible in the plots.
|
||||
@@ -0,0 +1,26 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
export TARGET=x86-linux
|
||||
|
||||
include Makefile.inc
|
||||
@@ -0,0 +1,81 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
# $Id: Makefile.inc 3 2012-01-15 16:26:07Z aneto $
|
||||
#
|
||||
#############################################################
|
||||
#Subprojects w.r.t. the main directory only (important to allow setting SPBM as an export variable).
|
||||
#If SPB is directly exported as an environment variable it will also be evaluated as part of the subprojects SPB, thus
|
||||
#potentially overriding its value
|
||||
#Main target subprojects. May be overridden by shell definition.
|
||||
SPBM?=Source/Components/DataSources.x
|
||||
# Source/Components/GAMs.x \
|
||||
|
||||
|
||||
|
||||
#Testing of the main target subprojects. May be overridden by shell definition.
|
||||
# SPBMT?=Test/Components/DataSources.x \
|
||||
# Test/Components/GAMs.x \
|
||||
# Test/Components/Utils.x \
|
||||
# Test/Components/Interfaces.x \
|
||||
# Test/GTest.x
|
||||
SPBMT?= Test/Components/DataSources.x \
|
||||
Test/GTest.x
|
||||
# Test/Components/GAMs.x \
|
||||
# Test/Components/Utils.x \
|
||||
# Test/Components/Interfaces.x \
|
||||
|
||||
|
||||
#This really has to be defined locally.
|
||||
SUBPROJMAIN=$(SPBM:%.x=%.spb)
|
||||
SUBPROJMAINTEST=$(SPBMT:%.x=%.spb)
|
||||
SUBPROJMAINCLEAN=$(SPBM:%.x=%.spc)
|
||||
SUBPROJMAINTESTCLEAN=$(SPBMT:%.x=%.spc)
|
||||
|
||||
GAMPROJ=$(GAM:%.x=%.spb)
|
||||
|
||||
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
|
||||
|
||||
ROOT_DIR=.
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
|
||||
|
||||
|
||||
all: $(OBJS) core test
|
||||
echo $(OBJS)
|
||||
|
||||
core: $(SUBPROJMAIN) check-marte
|
||||
echo $(SUBPROJMAIN)
|
||||
|
||||
test: $(SUBPROJMAINTEST) check-marte
|
||||
echo $(SUBPROJMAINTEST)
|
||||
|
||||
clean:: $(SUBPROJMAINCLEAN) $(SUBPROJMAINTESTCLEAN) clean_wipe_old
|
||||
#clean:: $(SUBPROJMAINCLEAN) $(SUBPROJMAINTESTCLEAN) clean_wipe_old
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
|
||||
|
||||
check-marte:
|
||||
ifndef MARTe2_DIR
|
||||
$(error MARTe2_DIR is undefined)
|
||||
endif
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
# $Id: Makefile.inc 3 2012-01-15 16:26:07Z aneto $
|
||||
#
|
||||
#############################################################
|
||||
TARGET=msc
|
||||
|
||||
include Makefile.inc
|
||||
|
||||
LIBRARIES0 = kernel32.lib wsock32.lib largeint.lib user32.lib advapi32.lib ws2_32.lib
|
||||
LIBRARIES += $(LIBRARIES0) $(BLCOMPONENTS)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 EFDA | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ("Fusion for Energy")
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
# $Id: Makefile.gcc 3 2015-01-15 16:26:07Z aneto $
|
||||
#
|
||||
#############################################################
|
||||
|
||||
TARGET=rpmbuild
|
||||
ROOT_DIR=.
|
||||
|
||||
FOLDERSX?=Source.x Makefile.x86-linux.x Makefile.inc.x
|
||||
|
||||
MARTe2_MAKEDEFAULT_DIR?=$(MARTe2_DIR)/MakeDefaults
|
||||
|
||||
#If not set try to get the project version from a git tag (in the format vx.y, i.e. assuming it starts with v)
|
||||
M2_RPM_VERSION?=$(shell git tag | sort -V | tail -1 | cut -c2-)
|
||||
M2_RPM_NAME?=MARTe2_Components
|
||||
M2_RPM_ID?=marte2-components
|
||||
M2_RPM_ID_TEST=$(M2_RPM_ID)-test
|
||||
M2_RPM_BUILD_OPTIONS?=core
|
||||
M2_RPM_REQUIRES?=marte2-core
|
||||
M2_RPM_CONFLICTS?=$(M2_RPM_ID_TEST)
|
||||
M2_RPM_SPEC_FILE_PATH=$(MARTe2_DIR)/Resources/RPM/
|
||||
|
||||
include $(MARTe2_MAKEDEFAULT_DIR)/MakeStdLibDefs.$(TARGET)
|
||||
|
||||
all: $(OBJS) $(BUILD_DIR)/$(M2_RPM_ID)-$(M2_RPM_VERSION).rpm check-env
|
||||
echo $(OBJS)
|
||||
|
||||
test:
|
||||
make -f Makefile.$(TARGET) M2_RPM_ID=$(M2_RPM_ID_TEST) FOLDERSX='$(FOLDERSX) Test.x' M2_RPM_REQUIRES=marte2-core-test M2_RPM_BUILD_OPTIONS=all M2_RPM_CONFLICTS=$(M2_RPM_ID)
|
||||
|
||||
check-env:
|
||||
ifndef MARTe2_DIR
|
||||
$(error MARTe2_DIR is undefined)
|
||||
endif
|
||||
|
||||
include $(MARTe2_MAKEDEFAULT_DIR)/MakeStdLibRules.$(TARGET)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# MARTe IO Components
|
||||
|
||||
A collection of I/O components for the [MARTe2](https://vcis.f4e.europa.eu/marte2-docs/) real-time framework,
|
||||
together with a Go-based web visualisation client.
|
||||
|
||||
## Components
|
||||
|
||||
| Component | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| [UDPStreamer](Docs/UDPStreamer.md) | DataSource | Streams MARTe2 signals to a single UDP client in real time |
|
||||
| [SineArrayGAM](Docs/SineArrayGAM.md) | GAM | Generates continuous sinusoidal burst arrays (testing / demo) |
|
||||
|
||||
## Client Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| [WebUI](Docs/WebUI.md) | Browser-based real-time plotter — connects to UDPStreamer via UDP, exposes data over WebSocket |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Configure environment (edit paths to point at your MARTe2 builds)
|
||||
source marte_env.sh
|
||||
|
||||
# 2. Build everything and run the demo application with the browser UI
|
||||
cd Test/MARTeApp
|
||||
./run.sh --webui
|
||||
|
||||
# 3. Open http://localhost:8080 in a browser
|
||||
# Drag signals from the sidebar onto a plot panel to visualise them.
|
||||
```
|
||||
|
||||
See the [Tutorial](Docs/Tutorial.md) for a step-by-step walkthrough.
|
||||
|
||||
## Building
|
||||
|
||||
### UDPStreamer library
|
||||
|
||||
```bash
|
||||
source marte_env.sh
|
||||
make -f Makefile.gcc TARGET=x86-linux
|
||||
```
|
||||
|
||||
### Unit tests
|
||||
|
||||
```bash
|
||||
source marte_env.sh
|
||||
make -f Makefile.gcc TARGET=x86-linux test
|
||||
./Build/x86-linux/Tests/GTest/MainGTest.ex --gtest_filter="UDPStreamer*"
|
||||
```
|
||||
|
||||
### WebUI client
|
||||
|
||||
```bash
|
||||
cd Client/WebUI
|
||||
go build -o udpstreamer-webui ./...
|
||||
```
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
.
|
||||
├── Source/Components/DataSources/UDPStreamer/ # UDPStreamer DataSource + SineArrayGAM
|
||||
├── Test/
|
||||
│ ├── Components/DataSources/UDPStreamer/ # GTest unit tests
|
||||
│ ├── GTest/ # GTest main entry point
|
||||
│ └── MARTeApp/ # Demo MARTe2 application (cfg + run.sh)
|
||||
├── Client/WebUI/ # Go WebSocket relay + browser UI
|
||||
├── Docs/ # Reference documentation and tutorial
|
||||
├── marte_env.sh # Environment setup (MARTe2_DIR, LD_LIBRARY_PATH)
|
||||
└── Makefile.gcc / Makefile.inc # Top-level build system
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Dependency | Notes |
|
||||
|------------|-------|
|
||||
| MARTe2 | Must be built; path configured via `MARTe2_DIR` in `marte_env.sh` |
|
||||
| MARTe2-components | IOGAM, LinuxTimer, WaveformGAM must be built |
|
||||
| GCC ≥ 7 | C++98 mode (`-std=c++98`) |
|
||||
| Go ≥ 1.21 | For the WebUI client only |
|
||||
|
||||
## Documentation
|
||||
|
||||
- [UDPStreamer DataSource reference](Docs/UDPStreamer.md)
|
||||
- [SineArrayGAM reference](Docs/SineArrayGAM.md)
|
||||
- [WebUI client reference](Docs/WebUI.md)
|
||||
- [Wire protocol specification](Docs/Protocol.md)
|
||||
- [Tutorial: streaming your first signals](Docs/Tutorial.md)
|
||||
|
||||
## License
|
||||
|
||||
EUPL v1.1
|
||||
@@ -0,0 +1 @@
|
||||
include Makefile.inc
|
||||
@@ -0,0 +1,38 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
OBJSX=
|
||||
|
||||
SPB = UDPStreamer.x
|
||||
|
||||
PACKAGE=Components
|
||||
ROOT_DIR=../../..
|
||||
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
|
||||
|
||||
all: $(OBJS) $(SUBPROJ)
|
||||
echo $(OBJS)
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
|
||||
@@ -0,0 +1,25 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
include Makefile.inc
|
||||
@@ -0,0 +1,56 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
OBJSX = UDPStreamer.x \
|
||||
SineArrayGAM.x
|
||||
|
||||
PACKAGE=Components/DataSources
|
||||
ROOT_DIR=../../../../
|
||||
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
|
||||
|
||||
INCLUDES += -I.
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams
|
||||
|
||||
all: $(OBJS) \
|
||||
$(BUILD_DIR)/UDPStreamer$(LIBEXT) \
|
||||
$(BUILD_DIR)/UDPStreamer$(DLLEXT)
|
||||
echo $(OBJS)
|
||||
|
||||
include depends.$(TARGET)
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
|
||||
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* @file SineArrayGAM.cpp
|
||||
* @brief Source file for class SineArrayGAM
|
||||
* @date 15/05/2026
|
||||
* @author Martino Ferrari
|
||||
*
|
||||
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||||
* the Development of Fusion Energy ('Fusion for Energy').
|
||||
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||||
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||||
* You may not use this work except in compliance with the Licence.
|
||||
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||||
*
|
||||
* @warning Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the Licence is distributed on an "AS IS"
|
||||
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the Licence permissions and limitations under the Licence.
|
||||
*/
|
||||
|
||||
#define DLL_API
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Standard header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include <cmath>
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Project header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include "AdvancedErrorManagement.h"
|
||||
#include "SineArrayGAM.h"
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Method definitions */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
namespace MARTe {
|
||||
|
||||
SineArrayGAM::SineArrayGAM() :
|
||||
GAM(),
|
||||
frequency(1.0),
|
||||
amplitude(1.0),
|
||||
offset(0.0),
|
||||
phase(0.0),
|
||||
samplingRate(1000000.0),
|
||||
nElements(0u),
|
||||
sampleOffset(0ull),
|
||||
outputBuf(NULL_PTR(float32 *)) {
|
||||
}
|
||||
|
||||
SineArrayGAM::~SineArrayGAM() {
|
||||
}
|
||||
|
||||
bool SineArrayGAM::Initialise(StructuredDataI &data) {
|
||||
bool ok = GAM::Initialise(data);
|
||||
if (ok) {
|
||||
if (!data.Read("Frequency", frequency)) {
|
||||
frequency = 1.0;
|
||||
}
|
||||
if (!data.Read("Amplitude", amplitude)) {
|
||||
amplitude = 1.0;
|
||||
}
|
||||
if (!data.Read("Offset", offset)) {
|
||||
offset = 0.0;
|
||||
}
|
||||
if (!data.Read("Phase", phase)) {
|
||||
phase = 0.0;
|
||||
}
|
||||
if (!data.Read("SamplingRate", samplingRate)) {
|
||||
samplingRate = 1000000.0;
|
||||
}
|
||||
if (samplingRate <= 0.0) {
|
||||
REPORT_ERROR(ErrorManagement::InitialisationError,
|
||||
"SineArrayGAM: SamplingRate must be greater than zero");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool SineArrayGAM::Setup() {
|
||||
bool ok = (GetNumberOfOutputSignals() == 1u);
|
||||
if (!ok) {
|
||||
REPORT_ERROR(ErrorManagement::InitialisationError,
|
||||
"SineArrayGAM: exactly one output signal is required");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32 sz = 0u;
|
||||
ok = GetSignalByteSize(OutputSignals, 0u, sz);
|
||||
if (ok) {
|
||||
nElements = sz / static_cast<uint32>(sizeof(float32));
|
||||
outputBuf = reinterpret_cast<float32 *>(GetOutputSignalMemory(0u));
|
||||
ok = (outputBuf != NULL_PTR(float32 *)) && (nElements > 0u);
|
||||
if (!ok) {
|
||||
REPORT_ERROR(ErrorManagement::InitialisationError,
|
||||
"SineArrayGAM: failed to resolve output signal memory");
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool SineArrayGAM::Execute() {
|
||||
static const float64 TWO_PI = 6.28318530717958647692;
|
||||
const float64 twoPiF = TWO_PI * frequency;
|
||||
const float64 invSr = 1.0 / samplingRate;
|
||||
|
||||
for (uint32 i = 0u; i < nElements; i++) {
|
||||
float64 t = static_cast<float64>(sampleOffset + static_cast<uint64>(i)) * invSr;
|
||||
outputBuf[i] = static_cast<float32>(amplitude * std::sin(twoPiF * t + phase) + offset);
|
||||
}
|
||||
sampleOffset += static_cast<uint64>(nElements);
|
||||
return true;
|
||||
}
|
||||
|
||||
CLASS_REGISTER(SineArrayGAM, "1.0")
|
||||
|
||||
} /* namespace MARTe */
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @file SineArrayGAM.h
|
||||
* @brief GAM that fills a float32 array with a continuous sinusoidal waveform.
|
||||
* @date 15/05/2026
|
||||
* @author Martino Ferrari
|
||||
*
|
||||
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||||
* the Development of Fusion Energy ('Fusion for Energy').
|
||||
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||||
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||||
* You may not use this work except in compliance with the Licence.
|
||||
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||||
*
|
||||
* @warning Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the Licence is distributed on an "AS IS"
|
||||
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the Licence permissions and limitations under the Licence.
|
||||
*
|
||||
* @details Each Execute() call fills one float32 output array with N samples of:
|
||||
* v[k] = Amplitude * sin(2π * Frequency * (sampleOffset + k) / SamplingRate + Phase) + Offset
|
||||
*
|
||||
* The sampleOffset accumulates across calls so the waveform phase is continuous.
|
||||
*
|
||||
* Configuration:
|
||||
* <pre>
|
||||
* +MyGAM = {
|
||||
* Class = SineArrayGAM
|
||||
* Frequency = 1000.0 // Signal frequency in Hz (default 1.0)
|
||||
* Amplitude = 1.0 // Signal amplitude (default 1.0)
|
||||
* Offset = 0.0 // DC offset (default 0.0)
|
||||
* Phase = 0.0 // Phase in radians (default 0.0)
|
||||
* SamplingRate = 1000000.0 // Sample rate in Hz; must match UDPStreamer signal (default 1000000.0)
|
||||
* OutputSignals = {
|
||||
* Ch1 = { DataSource = DDB; Type = float32; NumberOfElements = 1000 }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Exactly one output signal of type float32 is required.
|
||||
*/
|
||||
|
||||
#ifndef SINEARRAYGAM_H_
|
||||
#define SINEARRAYGAM_H_
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Standard header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Project header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include "CompilerTypes.h"
|
||||
#include "GAM.h"
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Class declaration */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
namespace MARTe {
|
||||
|
||||
class SineArrayGAM : public GAM {
|
||||
public:
|
||||
CLASS_REGISTER_DECLARATION()
|
||||
|
||||
/**
|
||||
* @brief Constructor. Sets safe defaults.
|
||||
*/
|
||||
SineArrayGAM();
|
||||
|
||||
/**
|
||||
* @brief Destructor.
|
||||
*/
|
||||
virtual ~SineArrayGAM();
|
||||
|
||||
/**
|
||||
* @brief Reads Frequency, Amplitude, Offset, Phase, SamplingRate from config.
|
||||
*/
|
||||
virtual bool Initialise(StructuredDataI &data);
|
||||
|
||||
/**
|
||||
* @brief Resolves the output signal pointer and element count.
|
||||
* @return true if exactly one float32 output signal is present.
|
||||
*/
|
||||
virtual bool Setup();
|
||||
|
||||
/**
|
||||
* @brief Fills the output array with the next N sinusoidal samples.
|
||||
* @return true always.
|
||||
*/
|
||||
virtual bool Execute();
|
||||
|
||||
private:
|
||||
float64 frequency; /**< Signal frequency [Hz] */
|
||||
float64 amplitude; /**< Signal amplitude */
|
||||
float64 offset; /**< DC offset */
|
||||
float64 phase; /**< Phase offset [radians] */
|
||||
float64 samplingRate; /**< Sample rate [Hz] */
|
||||
uint32 nElements; /**< Number of output elements per cycle */
|
||||
uint64 sampleOffset; /**< Cumulative sample count for continuous phase */
|
||||
float32 *outputBuf; /**< Pointer to the output signal memory */
|
||||
};
|
||||
|
||||
} /* namespace MARTe */
|
||||
|
||||
#endif /* SINEARRAYGAM_H_ */
|
||||
@@ -0,0 +1,995 @@
|
||||
/**
|
||||
* @file UDPStreamer.cpp
|
||||
* @brief Source file for class UDPStreamer
|
||||
* @date 13/05/2026
|
||||
* @author Martino Ferrari
|
||||
*
|
||||
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||||
* the Development of Fusion Energy ('Fusion for Energy').
|
||||
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||||
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||||
* You may not use this work except in compliance with the Licence.
|
||||
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||||
*
|
||||
* @warning Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the Licence is distributed on an "AS IS"
|
||||
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the Licence permissions and limitations under the Licence.
|
||||
*
|
||||
* @details This source file contains the definition of all the methods for
|
||||
* the class UDPStreamer (public, protected, and private). Be aware that some
|
||||
* methods, such as those inline could be defined on the header file, instead.
|
||||
*/
|
||||
|
||||
#define DLL_API
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Standard header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include <sys/select.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Project header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include "AdvancedErrorManagement.h"
|
||||
#include "EmbeddedThreadI.h"
|
||||
#include "GlobalObjectsDatabase.h"
|
||||
#include "HighResolutionTimer.h"
|
||||
#include "MemoryMapSynchronisedOutputBroker.h"
|
||||
#include "MemoryOperationsHelper.h"
|
||||
#include "Sleep.h"
|
||||
#include "Threads.h"
|
||||
#include "UDPStreamer.h"
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Static definitions */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
namespace MARTe {
|
||||
|
||||
/** Default port used when none is specified. */
|
||||
static const uint16 UDPS_DEFAULT_PORT = 44500u;
|
||||
|
||||
/** Default max payload per UDP datagram (bytes). */
|
||||
static const uint32 UDPS_DEFAULT_MAX_PAYLOAD = 1400u;
|
||||
|
||||
/** Minimum MaxPayloadSize: header + at least 1 byte of payload. */
|
||||
static const uint32 UDPS_MIN_PAYLOAD = static_cast<uint32>(sizeof(UDPSPacketHeader)) + 1u;
|
||||
|
||||
/** Server socket receive timeout in milliseconds. */
|
||||
static const uint32 UDPS_RECV_TIMEOUT_MS = 5u;
|
||||
|
||||
/** EventSem wait timeout in milliseconds for the data loop. */
|
||||
static const uint32 UDPS_DATA_WAIT_MS = 10u;
|
||||
|
||||
/** Sentinel: no time-signal reference; use the packet-level timestamp. */
|
||||
static const uint32 UDPS_NO_TIME_SIGNAL = 0xFFFFFFFFu;
|
||||
|
||||
/** Max signal name length in CONFIG packet (including null terminator). */
|
||||
static const uint32 UDPS_MAX_SIGNAL_NAME = 64u;
|
||||
|
||||
/** Max unit string length in CONFIG packet (including null terminator). */
|
||||
static const uint32 UDPS_MAX_UNIT_LEN = 32u;
|
||||
|
||||
/** Size in bytes of one signal descriptor in the CONFIG payload. */
|
||||
static const uint32 UDPS_SIGNAL_DESC_SIZE =
|
||||
UDPS_MAX_SIGNAL_NAME /* name */
|
||||
+ 1u /* typeCode */
|
||||
+ 1u /* quantType */
|
||||
+ 1u /* numDimensions*/
|
||||
+ 4u /* numRows */
|
||||
+ 4u /* numCols */
|
||||
+ 8u /* rangeMin */
|
||||
+ 8u /* rangeMax */
|
||||
+ 1u /* timeMode */
|
||||
+ 8u /* samplingRate */
|
||||
+ 4u /* timeSignalIdx*/
|
||||
+ UDPS_MAX_UNIT_LEN; /* unit */
|
||||
|
||||
/** Bytes prepended to each DATA payload for the HRT packet timestamp. */
|
||||
static const uint32 UDPS_TIMESTAMP_BYTES = 8u;
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Method definitions */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
UDPStreamer::UDPStreamer() :
|
||||
MemoryDataSourceI(),
|
||||
EmbeddedServiceMethodBinderI(),
|
||||
executor(*this) {
|
||||
port = UDPS_DEFAULT_PORT;
|
||||
maxPayloadSize = UDPS_DEFAULT_MAX_PAYLOAD;
|
||||
cpuMask = 0xFFFFFFFFu;
|
||||
stackSize = THREADS_DEFAULT_STACKSIZE;
|
||||
numSigs = 0u;
|
||||
signalInfos = NULL_PTR(UDPStreamerSignalInfo *);
|
||||
readyBuffer = NULL_PTR(uint8 *);
|
||||
scratchBuffer = NULL_PTR(uint8 *);
|
||||
wireBuffer = NULL_PTR(uint8 *);
|
||||
totalSrcBytes = 0u;
|
||||
totalWireBytes = 0u;
|
||||
syncTimestamp = 0u;
|
||||
clientConnected = false;
|
||||
packetCounter = 0u;
|
||||
|
||||
if (!dataSem.Create()) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not create EventSem.");
|
||||
}
|
||||
bufMutex.Create(false);
|
||||
}
|
||||
|
||||
/*lint -e{1551} Destructor must guarantee thread and socket cleanup. */
|
||||
UDPStreamer::~UDPStreamer() {
|
||||
/* Unblock the background thread's dataSem wait so it can exit */
|
||||
(void) dataSem.Post();
|
||||
|
||||
if (executor.GetStatus() != EmbeddedThreadI::OffState) {
|
||||
if (!executor.Stop()) {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"First Stop() attempt failed; retrying.");
|
||||
if (!executor.Stop()) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not stop background thread.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (serverSocket.IsValid()) {
|
||||
(void) serverSocket.Close();
|
||||
}
|
||||
if (clientSocket.IsValid()) {
|
||||
(void) clientSocket.Close();
|
||||
}
|
||||
|
||||
if (signalInfos != NULL_PTR(UDPStreamerSignalInfo *)) {
|
||||
delete[] signalInfos;
|
||||
signalInfos = NULL_PTR(UDPStreamerSignalInfo *);
|
||||
}
|
||||
|
||||
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||
if (readyBuffer != NULL_PTR(uint8 *)) {
|
||||
heap->Free(reinterpret_cast<void *&>(readyBuffer));
|
||||
}
|
||||
if (scratchBuffer != NULL_PTR(uint8 *)) {
|
||||
heap->Free(reinterpret_cast<void *&>(scratchBuffer));
|
||||
}
|
||||
if (wireBuffer != NULL_PTR(uint8 *)) {
|
||||
heap->Free(reinterpret_cast<void *&>(wireBuffer));
|
||||
}
|
||||
|
||||
(void) dataSem.Close();
|
||||
}
|
||||
|
||||
bool UDPStreamer::Initialise(StructuredDataI &data) {
|
||||
bool ok = MemoryDataSourceI::Initialise(data);
|
||||
|
||||
if (ok) {
|
||||
if (!data.Read("Port", port)) {
|
||||
port = UDPS_DEFAULT_PORT;
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"Port not specified; using default %u.",
|
||||
static_cast<uint32>(port));
|
||||
}
|
||||
if (port <= 1024u) {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Port %u is in the privileged range (<= 1024).",
|
||||
static_cast<uint32>(port));
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
if (!data.Read("MaxPayloadSize", maxPayloadSize)) {
|
||||
maxPayloadSize = UDPS_DEFAULT_MAX_PAYLOAD;
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"MaxPayloadSize not specified; using default %u.",
|
||||
maxPayloadSize);
|
||||
}
|
||||
if (maxPayloadSize < UDPS_MIN_PAYLOAD) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"MaxPayloadSize %u is too small (minimum %u).",
|
||||
maxPayloadSize, UDPS_MIN_PAYLOAD);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
uint32 cpuMaskIn = 0xFFFFFFFFu;
|
||||
if (!data.Read("CPUMask", cpuMaskIn)) {
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"CPUMask not specified; using 0xFFFFFFFF.");
|
||||
}
|
||||
cpuMask = cpuMaskIn;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
if (!data.Read("StackSize", stackSize)) {
|
||||
stackSize = THREADS_DEFAULT_STACKSIZE;
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"StackSize not specified; using MARTe2 default %u.",
|
||||
stackSize);
|
||||
}
|
||||
if (stackSize == 0u) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError, "StackSize must be > 0.");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool UDPStreamer::SetConfiguredDatabase(StructuredDataI &data) {
|
||||
bool ok = MemoryDataSourceI::SetConfiguredDatabase(data);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
numSigs = GetNumberOfSignals();
|
||||
if (numSigs == 0u) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"At least one signal must be defined.");
|
||||
return false;
|
||||
}
|
||||
|
||||
signalInfos = new UDPStreamerSignalInfo[numSigs];
|
||||
/* Local array to hold time-signal names (resolved to indices in pass 3) */
|
||||
StreamString *timeSignalNames = new StreamString[numSigs];
|
||||
|
||||
/* --- Pass 1: populate from the MARTe2 framework APIs --- */
|
||||
totalSrcBytes = 0u;
|
||||
totalWireBytes = UDPS_TIMESTAMP_BYTES;
|
||||
|
||||
for (uint32 i = 0u; i < numSigs && ok; i++) {
|
||||
StreamString sigName;
|
||||
ok = GetSignalName(i, sigName);
|
||||
if (!ok) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not get name for signal %u.", i);
|
||||
break;
|
||||
}
|
||||
signalInfos[i].name = sigName;
|
||||
signalInfos[i].type = GetSignalType(i);
|
||||
signalInfos[i].numDimensions = 0u;
|
||||
signalInfos[i].numElements = 1u;
|
||||
signalInfos[i].numRows = 1u;
|
||||
signalInfos[i].numCols = 1u;
|
||||
signalInfos[i].quantType = UDPStreamerQuantNone;
|
||||
signalInfos[i].rangeMin = 0.0;
|
||||
signalInfos[i].rangeMax = 1.0;
|
||||
signalInfos[i].timeMode = UDPStreamerTimePacket;
|
||||
signalInfos[i].samplingRate = 0.0;
|
||||
signalInfos[i].timeSignalIdx = UDPS_NO_TIME_SIGNAL;
|
||||
signalInfos[i].unit = "";
|
||||
signalInfos[i].srcByteSize = 0u;
|
||||
signalInfos[i].wireByteSize = 0u;
|
||||
signalInfos[i].bufferOffset = 0u;
|
||||
timeSignalNames[i] = "";
|
||||
|
||||
uint8 ndims = 0u;
|
||||
(void) GetSignalNumberOfDimensions(i, ndims);
|
||||
signalInfos[i].numDimensions = ndims;
|
||||
|
||||
uint32 nelems = 1u;
|
||||
(void) GetSignalNumberOfElements(i, nelems);
|
||||
signalInfos[i].numElements = nelems;
|
||||
signalInfos[i].numCols = nelems;
|
||||
|
||||
uint32 bsz = 0u;
|
||||
(void) GetSignalByteSize(i, bsz);
|
||||
signalInfos[i].srcByteSize = bsz;
|
||||
signalInfos[i].bufferOffset = totalSrcBytes;
|
||||
totalSrcBytes += bsz;
|
||||
}
|
||||
|
||||
/* --- Pass 2: read custom per-signal fields from signalsDatabase ---
|
||||
* Note: DataSourceI::AddSignals() leaves signalsDatabase positioned at the
|
||||
* "Signals" node. We must reset to root before navigating. */
|
||||
if (ok) {
|
||||
(void) signalsDatabase.MoveToRoot();
|
||||
bool moved = signalsDatabase.MoveRelative("Signals");
|
||||
if (!moved) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not navigate to Signals in signalsDatabase.");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32 i = 0u; i < numSigs && ok; i++) {
|
||||
bool moved = signalsDatabase.MoveRelative(signalInfos[i].name.Buffer());
|
||||
if (!moved) {
|
||||
/* Signal added by framework with no user-configured custom fields */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Unit */
|
||||
StreamString unit = "";
|
||||
(void) signalsDatabase.Read("Unit", unit);
|
||||
signalInfos[i].unit = unit;
|
||||
|
||||
/* Range */
|
||||
(void) signalsDatabase.Read("RangeMin", signalInfos[i].rangeMin);
|
||||
(void) signalsDatabase.Read("RangeMax", signalInfos[i].rangeMax);
|
||||
|
||||
/* QuantizedType */
|
||||
StreamString quantStr = "";
|
||||
if (signalsDatabase.Read("QuantizedType", quantStr)) {
|
||||
if (quantStr == "uint8") {
|
||||
signalInfos[i].quantType = UDPStreamerQuantUint8;
|
||||
}
|
||||
else if (quantStr == "int8") {
|
||||
signalInfos[i].quantType = UDPStreamerQuantInt8;
|
||||
}
|
||||
else if (quantStr == "uint16") {
|
||||
signalInfos[i].quantType = UDPStreamerQuantUint16;
|
||||
}
|
||||
else if (quantStr == "int16") {
|
||||
signalInfos[i].quantType = UDPStreamerQuantInt16;
|
||||
}
|
||||
else if (quantStr == "none") {
|
||||
signalInfos[i].quantType = UDPStreamerQuantNone;
|
||||
}
|
||||
else {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: unknown QuantizedType '%s'. "
|
||||
"Allowed: none|uint8|int8|uint16|int16.",
|
||||
signalInfos[i].name.Buffer(), quantStr.Buffer());
|
||||
ok = false;
|
||||
}
|
||||
if (ok && (signalInfos[i].quantType != UDPStreamerQuantNone)) {
|
||||
TypeDescriptor td = signalInfos[i].type;
|
||||
bool isFloat = ((td == Float32Bit) || (td == Float64Bit));
|
||||
if (!isFloat) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: QuantizedType only supported for "
|
||||
"float32/float64 signals.",
|
||||
signalInfos[i].name.Buffer());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TimeMode */
|
||||
if (ok) {
|
||||
StreamString timeModeStr;
|
||||
(void) signalsDatabase.Read("TimeMode", timeModeStr);
|
||||
if (timeModeStr.Size() == 0u) {
|
||||
timeModeStr = "PacketTime";
|
||||
}
|
||||
if (timeModeStr == "PacketTime") {
|
||||
signalInfos[i].timeMode = UDPStreamerTimePacket;
|
||||
}
|
||||
else if (timeModeStr == "FullArray") {
|
||||
signalInfos[i].timeMode = UDPStreamerTimeFullArray;
|
||||
}
|
||||
else if (timeModeStr == "FirstSample") {
|
||||
signalInfos[i].timeMode = UDPStreamerTimeFirstSample;
|
||||
}
|
||||
else if (timeModeStr == "LastSample") {
|
||||
signalInfos[i].timeMode = UDPStreamerTimeLastSample;
|
||||
}
|
||||
else {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: unknown TimeMode '%s'. "
|
||||
"Allowed: PacketTime|FullArray|FirstSample|LastSample.",
|
||||
signalInfos[i].name.Buffer(), timeModeStr.Buffer());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* TimeSignal (required when TimeMode != PacketTime) */
|
||||
if (ok && (signalInfos[i].timeMode != UDPStreamerTimePacket)) {
|
||||
StreamString tsName = "";
|
||||
if (!signalsDatabase.Read("TimeSignal", tsName)) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: TimeSignal must be specified when "
|
||||
"TimeMode != PacketTime.",
|
||||
signalInfos[i].name.Buffer());
|
||||
ok = false;
|
||||
}
|
||||
else {
|
||||
timeSignalNames[i] = tsName;
|
||||
/* Index resolved in pass 3 */
|
||||
signalInfos[i].timeSignalIdx = UDPS_NO_TIME_SIGNAL;
|
||||
}
|
||||
}
|
||||
|
||||
/* SamplingRate */
|
||||
if (ok) {
|
||||
(void) signalsDatabase.Read("SamplingRate", signalInfos[i].samplingRate);
|
||||
bool needsRate = (signalInfos[i].timeMode == UDPStreamerTimeFirstSample ||
|
||||
signalInfos[i].timeMode == UDPStreamerTimeLastSample);
|
||||
if (needsRate && (signalInfos[i].samplingRate <= 0.0)) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: SamplingRate > 0 is required for "
|
||||
"FirstSample/LastSample TimeMode.",
|
||||
signalInfos[i].name.Buffer());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
(void) signalsDatabase.MoveToAncestor(1u);
|
||||
}
|
||||
|
||||
if (ok || true) { /* always attempt to restore navigation */
|
||||
(void) signalsDatabase.MoveToAncestor(1u);
|
||||
}
|
||||
|
||||
/* --- Pass 3: resolve TimeSignal names to signal indices --- */
|
||||
for (uint32 i = 0u; i < numSigs && ok; i++) {
|
||||
if (signalInfos[i].timeMode == UDPStreamerTimePacket) {
|
||||
continue; /* no time signal needed */
|
||||
}
|
||||
bool found = false;
|
||||
for (uint32 j = 0u; j < numSigs; j++) {
|
||||
if (signalInfos[j].name == timeSignalNames[i]) {
|
||||
signalInfos[i].timeSignalIdx = j;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: TimeSignal '%s' not found among declared signals.",
|
||||
signalInfos[i].name.Buffer(),
|
||||
timeSignalNames[i].Buffer());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
delete[] timeSignalNames;
|
||||
timeSignalNames = NULL_PTR(StreamString *);
|
||||
|
||||
/* --- Pass 4: validate time-signal dimensions and compute wire sizes --- */
|
||||
for (uint32 i = 0u; i < numSigs && ok; i++) {
|
||||
/* Compute wire byte size per element */
|
||||
uint32 elemWireBytes = 0u;
|
||||
switch (signalInfos[i].quantType) {
|
||||
case UDPStreamerQuantUint8:
|
||||
case UDPStreamerQuantInt8:
|
||||
elemWireBytes = 1u;
|
||||
break;
|
||||
case UDPStreamerQuantUint16:
|
||||
case UDPStreamerQuantInt16:
|
||||
elemWireBytes = 2u;
|
||||
break;
|
||||
default:
|
||||
/* Raw copy: element size = total / numElements */
|
||||
if (signalInfos[i].numElements > 0u) {
|
||||
elemWireBytes = signalInfos[i].srcByteSize / signalInfos[i].numElements;
|
||||
}
|
||||
break;
|
||||
}
|
||||
signalInfos[i].wireByteSize = elemWireBytes * signalInfos[i].numElements;
|
||||
totalWireBytes += signalInfos[i].wireByteSize;
|
||||
|
||||
/* Validate time signal dimensions */
|
||||
uint32 tsIdx = signalInfos[i].timeSignalIdx;
|
||||
if (tsIdx != UDPS_NO_TIME_SIGNAL) {
|
||||
uint32 tsElems = signalInfos[tsIdx].numElements;
|
||||
if (signalInfos[i].timeMode == UDPStreamerTimeFullArray) {
|
||||
if (tsElems != signalInfos[i].numElements) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: FullArray TimeMode requires TimeSignal "
|
||||
"%s to have the same NumberOfElements (%u vs %u).",
|
||||
signalInfos[i].name.Buffer(),
|
||||
signalInfos[tsIdx].name.Buffer(),
|
||||
tsElems, signalInfos[i].numElements);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
else if ((signalInfos[i].timeMode == UDPStreamerTimeFirstSample) ||
|
||||
(signalInfos[i].timeMode == UDPStreamerTimeLastSample)) {
|
||||
if (tsElems != 1u) {
|
||||
REPORT_ERROR(ErrorManagement::ParametersError,
|
||||
"Signal %s: FirstSample/LastSample TimeMode requires "
|
||||
"a scalar TimeSignal (found %u elements).",
|
||||
signalInfos[i].name.Buffer(), tsElems);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool UDPStreamer::AllocateMemory() {
|
||||
bool ok = MemoryDataSourceI::AllocateMemory();
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* stateMemorySize is populated by MemoryDataSourceI::AllocateMemory() */
|
||||
if (totalSrcBytes == 0u) {
|
||||
totalSrcBytes = stateMemorySize;
|
||||
}
|
||||
|
||||
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||
|
||||
/* readyBuffer: copy of signal memory shared with background thread */
|
||||
readyBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalSrcBytes));
|
||||
if (readyBuffer == NULL_PTR(uint8 *)) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate readyBuffer.");
|
||||
return false;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Set(readyBuffer, 0, totalSrcBytes);
|
||||
|
||||
/* scratchBuffer: background-thread-private copy for serialization */
|
||||
scratchBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalSrcBytes));
|
||||
if (scratchBuffer == NULL_PTR(uint8 *)) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate scratchBuffer.");
|
||||
return false;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Set(scratchBuffer, 0, totalSrcBytes);
|
||||
|
||||
/* wireBuffer: serialized/quantized payload for transmission */
|
||||
wireBuffer = reinterpret_cast<uint8 *>(heap->Malloc(totalWireBytes));
|
||||
if (wireBuffer == NULL_PTR(uint8 *)) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError, "Could not allocate wireBuffer.");
|
||||
return false;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Set(wireBuffer, 0, totalWireBytes);
|
||||
|
||||
/* Update buffer offsets to match actual MemoryDataSourceI layout */
|
||||
for (uint32 i = 0u; i < numSigs; i++) {
|
||||
void *addr = NULL_PTR(void *);
|
||||
if (GetSignalMemoryBuffer(i, 0u, addr)) {
|
||||
signalInfos[i].bufferOffset =
|
||||
static_cast<uint32>(reinterpret_cast<uint8 *>(addr) - memory);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const char8 *UDPStreamer::GetBrokerName(StructuredDataI &data,
|
||||
const SignalDirection direction) {
|
||||
const char8 *brokerName = "";
|
||||
if (direction == OutputSignals) {
|
||||
brokerName = "MemoryMapSynchronisedOutputBroker";
|
||||
}
|
||||
return brokerName;
|
||||
}
|
||||
|
||||
bool UDPStreamer::PrepareNextState(const char8 *const currentStateName,
|
||||
const char8 *const nextStateName) {
|
||||
bool ok = true;
|
||||
|
||||
/* Open server socket (idempotent: skip if already valid) */
|
||||
if (!serverSocket.IsValid()) {
|
||||
ok = serverSocket.Open();
|
||||
if (!ok) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not open server UDP socket.");
|
||||
}
|
||||
if (ok) {
|
||||
ok = serverSocket.Listen(port);
|
||||
if (!ok) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not bind server socket to port %u.",
|
||||
static_cast<uint32>(port));
|
||||
}
|
||||
}
|
||||
/* Socket stays blocking; Read() uses the timeout via select() internally. */
|
||||
}
|
||||
|
||||
/* Start the background thread (idempotent) */
|
||||
if (ok && (executor.GetStatus() == EmbeddedThreadI::OffState)) {
|
||||
executor.SetName(GetName());
|
||||
executor.SetCPUMask(ProcessorType(cpuMask));
|
||||
executor.SetStackSize(stackSize);
|
||||
ErrorManagement::ErrorType startErr = executor.Start();
|
||||
ok = (startErr == ErrorManagement::NoError);
|
||||
if (!ok) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not start background thread.");
|
||||
}
|
||||
}
|
||||
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool UDPStreamer::Synchronise() {
|
||||
/* Capture timestamp as early as possible */
|
||||
uint64 ts = HighResolutionTimer::Counter();
|
||||
|
||||
/* RT-safe copy of signal memory → readyBuffer */
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
(void) MemoryOperationsHelper::Copy(readyBuffer, memory, totalSrcBytes);
|
||||
syncTimestamp = ts;
|
||||
bufMutex.FastUnLock();
|
||||
|
||||
/* Wake the background sender thread */
|
||||
(void) dataSem.Post();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ErrorManagement::ErrorType UDPStreamer::Execute(ExecutionInfo &info) {
|
||||
ErrorManagement::ErrorType ret = ErrorManagement::NoError;
|
||||
|
||||
if (info.GetStage() == ExecutionInfo::StartupStage) {
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"UDPStreamer background thread started (port %u).",
|
||||
static_cast<uint32>(port));
|
||||
}
|
||||
|
||||
if (info.GetStage() == ExecutionInfo::MainStage) {
|
||||
/* --- Poll server socket for incoming client commands ---
|
||||
* Use select() directly so we get a silent timeout with no log spam.
|
||||
* MARTe2's Read(buf, size, timeout) calls recvfrom() and logs an error
|
||||
* on every timeout (EAGAIN from SO_RCVTIMEO). */
|
||||
uint8 cmdBuf[256u];
|
||||
Handle sockFd = serverSocket.GetReadHandle();
|
||||
fd_set rfds;
|
||||
FD_ZERO(&rfds);
|
||||
FD_SET(static_cast<int>(sockFd), &rfds);
|
||||
struct timeval tv;
|
||||
tv.tv_sec = 0;
|
||||
tv.tv_usec = static_cast<long>(UDPS_RECV_TIMEOUT_MS) * 1000L;
|
||||
int nReady = select(static_cast<int>(sockFd) + 1, &rfds, NULL_PTR(fd_set *), NULL_PTR(fd_set *), &tv);
|
||||
if (nReady > 0) {
|
||||
uint32 recvSize = static_cast<uint32>(sizeof(cmdBuf));
|
||||
bool received = serverSocket.Read(reinterpret_cast<char8 *>(cmdBuf), recvSize);
|
||||
if (received && (recvSize >= static_cast<uint32>(sizeof(UDPSPacketHeader)))) {
|
||||
HandleClientCommand(cmdBuf, recvSize);
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Wait for RT thread to post new data --- */
|
||||
/* ResetWait atomically lowers the barrier then waits, preventing missed posts */
|
||||
ErrorManagement::ErrorType waitErr =
|
||||
dataSem.ResetWait(TimeoutType(UDPS_DATA_WAIT_MS));
|
||||
bool dataReady = (waitErr == ErrorManagement::NoError);
|
||||
|
||||
if (dataReady && clientConnected) {
|
||||
/* Copy readyBuffer → scratchBuffer under brief spinlock */
|
||||
uint64 ts = 0u;
|
||||
bufMutex.FastLock(TTInfiniteWait);
|
||||
(void) MemoryOperationsHelper::Copy(scratchBuffer, readyBuffer, totalSrcBytes);
|
||||
ts = syncTimestamp;
|
||||
bufMutex.FastUnLock();
|
||||
|
||||
/* Serialize signal data into wireBuffer */
|
||||
QuantizeAndSerialize(scratchBuffer, ts);
|
||||
|
||||
/* Send (fragmented if needed) */
|
||||
packetCounter++;
|
||||
if (!SendFragmented(UDPS_TYPE_DATA, packetCounter, wireBuffer, totalWireBytes)) {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Failed to send DATA packet (counter=%u).", packetCounter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info.GetStage() == ExecutionInfo::TerminationStage) {
|
||||
if (clientConnected) {
|
||||
(void) clientSocket.Close();
|
||||
clientConnected = false;
|
||||
}
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"UDPStreamer background thread terminated.");
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void UDPStreamer::HandleClientCommand(const uint8 *buf, uint32 size) {
|
||||
if (size < static_cast<uint32>(sizeof(UDPSPacketHeader))) {
|
||||
return;
|
||||
}
|
||||
const UDPSPacketHeader *hdr = reinterpret_cast<const UDPSPacketHeader *>(buf);
|
||||
if (hdr->magic != UDPS_MAGIC) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hdr->type == UDPS_TYPE_CONNECT) {
|
||||
InternetHost src = serverSocket.GetSource();
|
||||
|
||||
/* Disconnect any previous client */
|
||||
if (clientConnected) {
|
||||
if (clientSocket.IsValid()) {
|
||||
(void) clientSocket.Close();
|
||||
}
|
||||
clientConnected = false;
|
||||
}
|
||||
|
||||
/* Open a new client socket and connect to the requesting address */
|
||||
bool sockOk = clientSocket.Open();
|
||||
if (sockOk) {
|
||||
sockOk = clientSocket.Connect(src.GetAddress().Buffer(), src.GetPort());
|
||||
}
|
||||
if (sockOk) {
|
||||
clientConnected = true;
|
||||
REPORT_ERROR(ErrorManagement::Information,
|
||||
"Client connected from %s:%u.",
|
||||
src.GetAddress().Buffer(),
|
||||
static_cast<uint32>(src.GetPort()));
|
||||
|
||||
/* Send CONFIG packet */
|
||||
uint32 configBufSize = 4u + (numSigs * UDPS_SIGNAL_DESC_SIZE) + 32u;
|
||||
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||
uint8 *cfgBuf = reinterpret_cast<uint8 *>(heap->Malloc(configBufSize));
|
||||
if (cfgBuf != NULL_PTR(uint8 *)) {
|
||||
uint32 cfgPayloadSize = 0u;
|
||||
if (BuildConfigPayload(cfgBuf, configBufSize, cfgPayloadSize)) {
|
||||
(void) SendFragmented(UDPS_TYPE_CONFIG, 0u, cfgBuf, cfgPayloadSize);
|
||||
}
|
||||
else {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Could not build CONFIG payload.");
|
||||
}
|
||||
heap->Free(reinterpret_cast<void *&>(cfgBuf));
|
||||
}
|
||||
}
|
||||
else {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Could not connect to client %s:%u.",
|
||||
src.GetAddress().Buffer(),
|
||||
static_cast<uint32>(src.GetPort()));
|
||||
}
|
||||
}
|
||||
else if (hdr->type == UDPS_TYPE_DISCONNECT) {
|
||||
REPORT_ERROR(ErrorManagement::Information, "Client sent DISCONNECT.");
|
||||
if (clientConnected) {
|
||||
if (clientSocket.IsValid()) {
|
||||
(void) clientSocket.Close();
|
||||
}
|
||||
clientConnected = false;
|
||||
}
|
||||
}
|
||||
else if (hdr->type == UDPS_TYPE_ACK) {
|
||||
/* Optional: track acknowledged counters for loss detection */
|
||||
if (size >= static_cast<uint32>(sizeof(UDPSPacketHeader)) + 4u) {
|
||||
uint32 ackedCounter = 0u;
|
||||
const uint8 *pl = buf + sizeof(UDPSPacketHeader);
|
||||
(void) MemoryOperationsHelper::Copy(&ackedCounter, pl, 4u);
|
||||
REPORT_ERROR(ErrorManagement::Debug,
|
||||
"ACK received for packet counter %u.", ackedCounter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UDPStreamer::BuildConfigPayload(uint8 *buf,
|
||||
uint32 bufSize,
|
||||
uint32 &payloadSize) {
|
||||
payloadSize = 0u;
|
||||
|
||||
/* 4 bytes: number of signals */
|
||||
if ((payloadSize + 4u) > bufSize) {
|
||||
return false;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Copy(buf + payloadSize, &numSigs, 4u);
|
||||
payloadSize += 4u;
|
||||
|
||||
for (uint32 i = 0u; i < numSigs; i++) {
|
||||
if ((payloadSize + UDPS_SIGNAL_DESC_SIZE) > bufSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8 *p = buf + payloadSize;
|
||||
|
||||
/* Name: 64 bytes, zero-padded */
|
||||
(void) MemoryOperationsHelper::Set(p, 0, UDPS_MAX_SIGNAL_NAME);
|
||||
uint32 nameLen = static_cast<uint32>(signalInfos[i].name.Size());
|
||||
if (nameLen >= UDPS_MAX_SIGNAL_NAME) {
|
||||
nameLen = UDPS_MAX_SIGNAL_NAME - 1u;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Copy(p, signalInfos[i].name.Buffer(), nameLen);
|
||||
p += UDPS_MAX_SIGNAL_NAME;
|
||||
|
||||
/* Type code: 1 byte */
|
||||
*p = TypeDescriptorToCode(signalInfos[i].type);
|
||||
p += 1u;
|
||||
|
||||
/* Quant type: 1 byte */
|
||||
*p = static_cast<uint8>(signalInfos[i].quantType);
|
||||
p += 1u;
|
||||
|
||||
/* numDimensions: 1 byte */
|
||||
*p = signalInfos[i].numDimensions;
|
||||
p += 1u;
|
||||
|
||||
/* numRows: 4 bytes */
|
||||
(void) MemoryOperationsHelper::Copy(p, &signalInfos[i].numRows, 4u);
|
||||
p += 4u;
|
||||
|
||||
/* numCols: 4 bytes */
|
||||
(void) MemoryOperationsHelper::Copy(p, &signalInfos[i].numCols, 4u);
|
||||
p += 4u;
|
||||
|
||||
/* rangeMin: 8 bytes (float64) */
|
||||
(void) MemoryOperationsHelper::Copy(p, &signalInfos[i].rangeMin, 8u);
|
||||
p += 8u;
|
||||
|
||||
/* rangeMax: 8 bytes (float64) */
|
||||
(void) MemoryOperationsHelper::Copy(p, &signalInfos[i].rangeMax, 8u);
|
||||
p += 8u;
|
||||
|
||||
/* timeMode: 1 byte */
|
||||
*p = static_cast<uint8>(signalInfos[i].timeMode);
|
||||
p += 1u;
|
||||
|
||||
/* samplingRate: 8 bytes (float64) */
|
||||
(void) MemoryOperationsHelper::Copy(p, &signalInfos[i].samplingRate, 8u);
|
||||
p += 8u;
|
||||
|
||||
/* timeSignalIdx: 4 bytes */
|
||||
(void) MemoryOperationsHelper::Copy(p, &signalInfos[i].timeSignalIdx, 4u);
|
||||
p += 4u;
|
||||
|
||||
/* Unit: 32 bytes, zero-padded */
|
||||
(void) MemoryOperationsHelper::Set(p, 0, UDPS_MAX_UNIT_LEN);
|
||||
uint32 unitLen = static_cast<uint32>(signalInfos[i].unit.Size());
|
||||
if (unitLen >= UDPS_MAX_UNIT_LEN) {
|
||||
unitLen = UDPS_MAX_UNIT_LEN - 1u;
|
||||
}
|
||||
(void) MemoryOperationsHelper::Copy(p, signalInfos[i].unit.Buffer(), unitLen);
|
||||
p += UDPS_MAX_UNIT_LEN;
|
||||
|
||||
payloadSize += UDPS_SIGNAL_DESC_SIZE;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UDPStreamer::QuantizeAndSerialize(const uint8 *srcBuf, uint64 timestamp) {
|
||||
uint8 *dst = wireBuffer;
|
||||
|
||||
/* 8-byte packet timestamp */
|
||||
(void) MemoryOperationsHelper::Copy(dst, ×tamp, UDPS_TIMESTAMP_BYTES);
|
||||
dst += UDPS_TIMESTAMP_BYTES;
|
||||
|
||||
for (uint32 i = 0u; i < numSigs; i++) {
|
||||
const uint8 *src = srcBuf + signalInfos[i].bufferOffset;
|
||||
|
||||
if (signalInfos[i].quantType == UDPStreamerQuantNone) {
|
||||
/* Raw copy */
|
||||
(void) MemoryOperationsHelper::Copy(dst, src, signalInfos[i].srcByteSize);
|
||||
dst += signalInfos[i].srcByteSize;
|
||||
}
|
||||
else {
|
||||
float64 rMin = signalInfos[i].rangeMin;
|
||||
float64 rRange = signalInfos[i].rangeMax - rMin;
|
||||
if (rRange == 0.0) {
|
||||
rRange = 1.0; /* guard against divide-by-zero */
|
||||
}
|
||||
bool isSrcFloat32 = (signalInfos[i].type == Float32Bit);
|
||||
uint32 nelems = signalInfos[i].numElements;
|
||||
const uint8 *s = src;
|
||||
|
||||
for (uint32 e = 0u; e < nelems; e++) {
|
||||
float64 rawVal = 0.0;
|
||||
if (isSrcFloat32) {
|
||||
float32 f32 = 0.0f;
|
||||
(void) MemoryOperationsHelper::Copy(&f32, s, 4u);
|
||||
rawVal = static_cast<float64>(f32);
|
||||
s += 4u;
|
||||
}
|
||||
else {
|
||||
(void) MemoryOperationsHelper::Copy(&rawVal, s, 8u);
|
||||
s += 8u;
|
||||
}
|
||||
|
||||
/* Normalize and clamp to [0.0, 1.0] */
|
||||
float64 norm = (rawVal - rMin) / rRange;
|
||||
if (norm < 0.0) { norm = 0.0; }
|
||||
if (norm > 1.0) { norm = 1.0; }
|
||||
|
||||
switch (signalInfos[i].quantType) {
|
||||
case UDPStreamerQuantUint8: {
|
||||
uint8 q = static_cast<uint8>(norm * 255.0);
|
||||
*dst = q;
|
||||
dst += 1u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantInt8: {
|
||||
int8 q = static_cast<int8>((norm * 254.0) - 127.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 1u);
|
||||
dst += 1u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantUint16: {
|
||||
uint16 q = static_cast<uint16>(norm * 65535.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 2u);
|
||||
dst += 2u;
|
||||
break;
|
||||
}
|
||||
case UDPStreamerQuantInt16: {
|
||||
int16 q = static_cast<int16>((norm * 65534.0) - 32767.0);
|
||||
(void) MemoryOperationsHelper::Copy(dst, &q, 2u);
|
||||
dst += 2u;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UDPStreamer::SendFragmented(uint8 type,
|
||||
uint32 counter,
|
||||
const uint8 *payload,
|
||||
uint32 payloadSize) {
|
||||
uint32 headerSize = static_cast<uint32>(sizeof(UDPSPacketHeader));
|
||||
uint32 maxChunk = maxPayloadSize - headerSize;
|
||||
uint32 totalFrags = (payloadSize == 0u) ? 1u :
|
||||
((payloadSize + maxChunk - 1u) / maxChunk);
|
||||
|
||||
uint32 sendBufSize = headerSize + maxChunk;
|
||||
HeapI *heap = GlobalObjectsDatabase::Instance()->GetStandardHeap();
|
||||
uint8 *sendBuf = reinterpret_cast<uint8 *>(heap->Malloc(sendBufSize));
|
||||
if (sendBuf == NULL_PTR(uint8 *)) {
|
||||
REPORT_ERROR(ErrorManagement::FatalError,
|
||||
"Could not allocate send buffer (%u bytes).", sendBufSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
uint32 offs = 0u;
|
||||
|
||||
for (uint32 f = 0u; (f < totalFrags) && ok; f++) {
|
||||
uint32 chunkSize = payloadSize - offs;
|
||||
if (chunkSize > maxChunk) {
|
||||
chunkSize = maxChunk;
|
||||
}
|
||||
|
||||
UDPSPacketHeader *hdr = reinterpret_cast<UDPSPacketHeader *>(sendBuf);
|
||||
hdr->magic = UDPS_MAGIC;
|
||||
hdr->type = type;
|
||||
hdr->counter = counter;
|
||||
hdr->fragmentIdx = static_cast<uint16>(f);
|
||||
hdr->totalFragments = static_cast<uint16>(totalFrags);
|
||||
hdr->payloadBytes = chunkSize;
|
||||
|
||||
if (chunkSize > 0u) {
|
||||
(void) MemoryOperationsHelper::Copy(sendBuf + headerSize,
|
||||
payload + offs,
|
||||
chunkSize);
|
||||
}
|
||||
|
||||
uint32 sendSize = headerSize + chunkSize;
|
||||
ok = clientSocket.Write(reinterpret_cast<const char8 *>(sendBuf), sendSize);
|
||||
if (!ok) {
|
||||
REPORT_ERROR(ErrorManagement::Warning,
|
||||
"Fragment %u/%u send failed.", f + 1u, totalFrags);
|
||||
}
|
||||
offs += chunkSize;
|
||||
}
|
||||
|
||||
heap->Free(reinterpret_cast<void *&>(sendBuf));
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint8 UDPStreamer::TypeDescriptorToCode(TypeDescriptor td) {
|
||||
uint8 code = UDPS_TYPECODE_UNKNOWN;
|
||||
if (td == UnsignedInteger8Bit) { code = UDPS_TYPECODE_UINT8; }
|
||||
else if (td == SignedInteger8Bit) { code = UDPS_TYPECODE_INT8; }
|
||||
else if (td == UnsignedInteger16Bit) { code = UDPS_TYPECODE_UINT16; }
|
||||
else if (td == SignedInteger16Bit) { code = UDPS_TYPECODE_INT16; }
|
||||
else if (td == UnsignedInteger32Bit) { code = UDPS_TYPECODE_UINT32; }
|
||||
else if (td == SignedInteger32Bit) { code = UDPS_TYPECODE_INT32; }
|
||||
else if (td == UnsignedInteger64Bit) { code = UDPS_TYPECODE_UINT64; }
|
||||
else if (td == SignedInteger64Bit) { code = UDPS_TYPECODE_INT64; }
|
||||
else if (td == Float32Bit) { code = UDPS_TYPECODE_FLOAT32; }
|
||||
else if (td == Float64Bit) { code = UDPS_TYPECODE_FLOAT64; }
|
||||
return code;
|
||||
}
|
||||
|
||||
uint16 UDPStreamer::GetPort() const {
|
||||
return port;
|
||||
}
|
||||
|
||||
uint32 UDPStreamer::GetMaxPayloadSize() const {
|
||||
return maxPayloadSize;
|
||||
}
|
||||
|
||||
bool UDPStreamer::IsClientConnected() const {
|
||||
return clientConnected;
|
||||
}
|
||||
|
||||
CLASS_REGISTER(UDPStreamer, "1.0")
|
||||
|
||||
} /* namespace MARTe */
|
||||
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* @file UDPStreamer.h
|
||||
* @brief Header file for class UDPStreamer
|
||||
* @date 13/05/2026
|
||||
* @author Martino Ferrari
|
||||
*
|
||||
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||||
* the Development of Fusion Energy ('Fusion for Energy').
|
||||
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||||
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||||
* You may not use this work except in compliance with the Licence.
|
||||
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||||
*
|
||||
* @warning Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the Licence is distributed on an "AS IS"
|
||||
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the Licence permissions and limitations under the Licence.
|
||||
*
|
||||
* @details This header file contains the declaration of the class UDPStreamer
|
||||
* with all of its public, protected and private members. It may also include
|
||||
* definitions for inline methods which need to be visible to the compiler.
|
||||
*/
|
||||
|
||||
#ifndef UDPSTREAMER_H_
|
||||
#define UDPSTREAMER_H_
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Standard header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Project header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include "BasicUDPSocket.h"
|
||||
#include "CompilerTypes.h"
|
||||
#include "EmbeddedServiceMethodBinderI.h"
|
||||
#include "EventSem.h"
|
||||
#include "FastPollingMutexSem.h"
|
||||
#include "MemoryDataSourceI.h"
|
||||
#include "SingleThreadService.h"
|
||||
#include "StreamString.h"
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Class declaration */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
namespace MARTe {
|
||||
|
||||
/**
|
||||
* @brief Quantization types for float signals.
|
||||
*/
|
||||
typedef enum {
|
||||
UDPStreamerQuantNone = 0u, /**< No quantization; raw wire format */
|
||||
UDPStreamerQuantUint8 = 1u, /**< Quantize to uint8 [0, 255] */
|
||||
UDPStreamerQuantInt8 = 2u, /**< Quantize to int8 [-127, 127] */
|
||||
UDPStreamerQuantUint16 = 3u, /**< Quantize to uint16 [0, 65535] */
|
||||
UDPStreamerQuantInt16 = 4u /**< Quantize to int16 [-32767, 32767] */
|
||||
} UDPStreamerQuantType;
|
||||
|
||||
/**
|
||||
* @brief Time reference modes for signals.
|
||||
*/
|
||||
typedef enum {
|
||||
UDPStreamerTimePacket = 0u, /**< Use the packet-level timestamp (HRT at Synchronise time) */
|
||||
UDPStreamerTimeFullArray = 1u, /**< Time signal has same NumberOfElements; one timestamp per element */
|
||||
UDPStreamerTimeFirstSample = 2u, /**< Time signal is scalar = timestamp of first element */
|
||||
UDPStreamerTimeLastSample = 3u /**< Time signal is scalar = timestamp of last element */
|
||||
} UDPStreamerTimeMode;
|
||||
|
||||
/**
|
||||
* @brief Per-signal metadata used at runtime for serialization.
|
||||
*/
|
||||
struct UDPStreamerSignalInfo {
|
||||
StreamString name; /**< Signal name */
|
||||
TypeDescriptor type; /**< MARTe2 type descriptor */
|
||||
UDPStreamerQuantType quantType; /**< Quantization type (none = raw copy) */
|
||||
uint8 numDimensions; /**< Number of dimensions (0=scalar, 1=array, 2=matrix) */
|
||||
uint32 numElements; /**< Total number of elements (rows * cols) */
|
||||
uint32 numRows; /**< Number of rows */
|
||||
uint32 numCols; /**< Number of columns */
|
||||
float64 rangeMin; /**< Min of physical range (for quantization) */
|
||||
float64 rangeMax; /**< Max of physical range (for quantization) */
|
||||
UDPStreamerTimeMode timeMode; /**< How time is encoded for this signal */
|
||||
float64 samplingRate; /**< Hz, used for First/LastSample modes */
|
||||
uint32 timeSignalIdx; /**< Index of the time-reference signal; 0xFFFFFFFFu = PacketTime */
|
||||
StreamString unit; /**< Physical unit string */
|
||||
uint32 srcByteSize; /**< Bytes in MARTe2 memory */
|
||||
uint32 wireByteSize; /**< Bytes on the wire (may differ when quantized) */
|
||||
uint32 bufferOffset; /**< Byte offset in the flat MemoryDataSourceI memory buffer */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Magic number for UDPStreamer packets: 'UDPS' in little-endian.
|
||||
*/
|
||||
static const uint32 UDPS_MAGIC = 0x53504455u;
|
||||
|
||||
/**
|
||||
* @brief Packet type codes.
|
||||
*/
|
||||
static const uint8 UDPS_TYPE_DATA = 0u; /**< Server → Client: signal data */
|
||||
static const uint8 UDPS_TYPE_CONFIG = 1u; /**< Server → Client: signal configuration */
|
||||
static const uint8 UDPS_TYPE_ACK = 2u; /**< Client → Server: acknowledge counter */
|
||||
static const uint8 UDPS_TYPE_CONNECT = 3u; /**< Client → Server: connect request */
|
||||
static const uint8 UDPS_TYPE_DISCONNECT = 4u; /**< Client → Server: disconnect */
|
||||
|
||||
/**
|
||||
* @brief Wire packet header (17 bytes, packed).
|
||||
*/
|
||||
struct UDPSPacketHeader {
|
||||
uint32 magic; /**< Must equal UDPS_MAGIC */
|
||||
uint8 type; /**< One of UDPS_TYPE_* */
|
||||
uint32 counter; /**< Sequence counter (per data update, not per fragment) */
|
||||
uint16 fragmentIdx; /**< 0-based index of this fragment */
|
||||
uint16 totalFragments; /**< Total fragments for this update (1 = no fragmentation) */
|
||||
uint32 payloadBytes; /**< Bytes of payload immediately following this header */
|
||||
} __attribute__((packed));
|
||||
|
||||
/**
|
||||
* @brief Signal type codes transmitted in the CONFIG packet.
|
||||
* These are simplified type codes independent of MARTe2 internals.
|
||||
*/
|
||||
static const uint8 UDPS_TYPECODE_UINT8 = 0u;
|
||||
static const uint8 UDPS_TYPECODE_INT8 = 1u;
|
||||
static const uint8 UDPS_TYPECODE_UINT16 = 2u;
|
||||
static const uint8 UDPS_TYPECODE_INT16 = 3u;
|
||||
static const uint8 UDPS_TYPECODE_UINT32 = 4u;
|
||||
static const uint8 UDPS_TYPECODE_INT32 = 5u;
|
||||
static const uint8 UDPS_TYPECODE_UINT64 = 6u;
|
||||
static const uint8 UDPS_TYPECODE_INT64 = 7u;
|
||||
static const uint8 UDPS_TYPECODE_FLOAT32 = 8u;
|
||||
static const uint8 UDPS_TYPECODE_FLOAT64 = 9u;
|
||||
static const uint8 UDPS_TYPECODE_UNKNOWN = 255u;
|
||||
|
||||
/**
|
||||
* @brief A DataSource that streams MARTe2 signals to a single UDP client.
|
||||
*
|
||||
* @details This output DataSource accepts signals from GAMs and forwards them
|
||||
* asynchronously to a connected UDP client. A dedicated background thread handles
|
||||
* all network I/O so that the real-time thread is only blocked for a fast spinlock
|
||||
* and a memcpy during Synchronise().
|
||||
*
|
||||
* Protocol:
|
||||
* - Client sends CONNECT → server replies with CONFIG describing all signals.
|
||||
* - Server sends DATA packets (fragmented if needed) on every RT cycle.
|
||||
* - Client sends ACK packets (optional, for monitoring loss).
|
||||
* - Client sends DISCONNECT to terminate the session.
|
||||
*
|
||||
* Signal configuration syntax:
|
||||
* <pre>
|
||||
* +UDPStreamer1 = {
|
||||
* Class = UDPStreamer
|
||||
* Port = 44500 // Optional (default 44500)
|
||||
* MaxPayloadSize = 1400 // Optional (default 1400); max payload bytes per UDP datagram
|
||||
* CPUMask = 0x2 // Optional, affinity for background thread
|
||||
* StackSize = 1048576 // Optional, stack size for background thread
|
||||
* Signals = {
|
||||
* Time = {
|
||||
* Type = uint64
|
||||
* }
|
||||
* Pressure = {
|
||||
* Type = float32
|
||||
* NumberOfDimensions = 1
|
||||
* NumberOfElements = 100
|
||||
* Unit = "Pa" // Optional
|
||||
* RangeMin = 0.0 // Optional (required for quantization)
|
||||
* RangeMax = 1000000.0 // Optional (required for quantization)
|
||||
* QuantizedType = uint16 // Optional: none|uint8|int8|uint16|int16
|
||||
* TimeMode = LastSample // Optional: PacketTime|FullArray|FirstSample|LastSample
|
||||
* TimeSignal = Time // Required when TimeMode != PacketTime
|
||||
* SamplingRate = 10000.0 // Required when TimeMode = FirstSample or LastSample
|
||||
* }
|
||||
* Temperature = {
|
||||
* Type = float64
|
||||
* Unit = "K"
|
||||
* TimeMode = PacketTime
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* Notes:
|
||||
* - QuantizedType is only valid for float32/float64 signals.
|
||||
* - TimeMode = PacketTime uses the HRT counter captured in Synchronise().
|
||||
* - TimeMode = FullArray requires TimeSignal to have the same NumberOfElements.
|
||||
* - TimeMode = FirstSample/LastSample requires a scalar TimeSignal and SamplingRate.
|
||||
*/
|
||||
class UDPStreamer : public MemoryDataSourceI, public EmbeddedServiceMethodBinderI {
|
||||
public:
|
||||
CLASS_REGISTER_DECLARATION()
|
||||
|
||||
/**
|
||||
* @brief Constructor. Initialises all members to safe defaults.
|
||||
*/
|
||||
UDPStreamer();
|
||||
|
||||
/**
|
||||
* @brief Destructor. Stops the background thread and frees allocated memory.
|
||||
*/
|
||||
virtual ~UDPStreamer();
|
||||
|
||||
/**
|
||||
* @brief Parses top-level configuration parameters (Port, MaxPayloadSize, CPUMask, StackSize).
|
||||
* @return true if all mandatory parameters are valid.
|
||||
*/
|
||||
virtual bool Initialise(StructuredDataI &data);
|
||||
|
||||
/**
|
||||
* @brief Validates signal types and builds the per-signal metadata array.
|
||||
* @details Reads per-signal custom fields (Unit, RangeMin, RangeMax, QuantizedType,
|
||||
* TimeMode, TimeSignal, SamplingRate) from signalsDatabase and populates signalInfos[].
|
||||
* @return true if all signal configurations are valid.
|
||||
*/
|
||||
virtual bool SetConfiguredDatabase(StructuredDataI &data);
|
||||
|
||||
/**
|
||||
* @brief Allocates signal memory (via parent) plus readyBuffer, scratchBuffer, wireBuffer.
|
||||
* @return true if all memory allocations succeed.
|
||||
*/
|
||||
virtual bool AllocateMemory();
|
||||
|
||||
/**
|
||||
* @brief Returns the broker name for the given signal direction.
|
||||
* @return "MemoryMapSynchronisedOutputBroker" for OutputSignals; "" otherwise.
|
||||
*/
|
||||
virtual const char8 *GetBrokerName(StructuredDataI &data,
|
||||
const SignalDirection direction);
|
||||
|
||||
/**
|
||||
* @brief Called by the RT thread after all GAMs have written output signals.
|
||||
* @details Copies signal memory to readyBuffer under a fast spinlock, then posts
|
||||
* the dataSem to wake the background thread. Returns immediately.
|
||||
* @return true always.
|
||||
*/
|
||||
virtual bool Synchronise();
|
||||
|
||||
/**
|
||||
* @brief Opens the server socket and starts the background thread.
|
||||
* @return true if the socket and thread start successfully.
|
||||
*/
|
||||
virtual bool PrepareNextState(const char8 *const currentStateName,
|
||||
const char8 *const nextStateName);
|
||||
|
||||
/**
|
||||
* @brief Background thread entry point.
|
||||
* @details Handles client CONNECT/DISCONNECT/ACK and sends DATA packets.
|
||||
*/
|
||||
virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info);
|
||||
|
||||
/**
|
||||
* @brief Returns the listening port number.
|
||||
*/
|
||||
uint16 GetPort() const;
|
||||
|
||||
/**
|
||||
* @brief Returns the maximum payload size per UDP datagram.
|
||||
*/
|
||||
uint32 GetMaxPayloadSize() const;
|
||||
|
||||
/**
|
||||
* @brief Returns true if a client is currently connected.
|
||||
*/
|
||||
bool IsClientConnected() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief Serializes the CONFIG payload into buf and sets payloadSize.
|
||||
* @return true if serialization succeeds.
|
||||
*/
|
||||
bool BuildConfigPayload(uint8 *buf, uint32 bufSize, uint32 &payloadSize);
|
||||
|
||||
/**
|
||||
* @brief Quantizes and serializes all signals from srcBuf into wireBuffer.
|
||||
* @param[in] srcBuf Flat source buffer (signal data in MARTe2 format).
|
||||
* @param[in] timestamp HRT counter to embed as packet timestamp.
|
||||
*/
|
||||
void QuantizeAndSerialize(const uint8 *srcBuf, uint64 timestamp);
|
||||
|
||||
/**
|
||||
* @brief Sends payload as one or more fragmented UDP datagrams to the connected client.
|
||||
* @return true if all datagrams were sent without error.
|
||||
*/
|
||||
bool SendFragmented(uint8 type, uint32 counter, const uint8 *payload, uint32 payloadSize);
|
||||
|
||||
/**
|
||||
* @brief Handles a single received command packet from a client.
|
||||
*/
|
||||
void HandleClientCommand(const uint8 *buf, uint32 size);
|
||||
|
||||
/**
|
||||
* @brief Maps a MARTe2 TypeDescriptor to the UDPS_TYPECODE_* constants.
|
||||
*/
|
||||
static uint8 TypeDescriptorToCode(TypeDescriptor td);
|
||||
|
||||
/* Configuration parameters */
|
||||
uint16 port; /**< UDP server port */
|
||||
uint32 maxPayloadSize; /**< Max payload bytes per UDP packet (excluding header) */
|
||||
uint32 cpuMask; /**< Background thread CPU affinity */
|
||||
uint32 stackSize; /**< Background thread stack size */
|
||||
|
||||
/* Signal metadata */
|
||||
uint32 numSigs; /**< Number of signals */
|
||||
UDPStreamerSignalInfo *signalInfos; /**< Per-signal metadata array */
|
||||
|
||||
/* Double-buffering for RT safety */
|
||||
uint8 *readyBuffer; /**< Protected copy of signal memory for background thread */
|
||||
uint8 *scratchBuffer; /**< Local copy used during serialization (avoids holding lock) */
|
||||
uint32 totalSrcBytes; /**< Total bytes of signal data (= stateMemorySize) */
|
||||
FastPollingMutexSem bufMutex; /**< Protects readyBuffer against concurrent access */
|
||||
EventSem dataSem; /**< Wakes background thread when new data is ready */
|
||||
volatile uint64 syncTimestamp; /**< HRT counter captured in Synchronise() */
|
||||
|
||||
/* Wire serialization buffer */
|
||||
uint8 *wireBuffer; /**< Pre-allocated buffer for the serialized DATA payload */
|
||||
uint32 totalWireBytes; /**< Total bytes of all signals after quantization + 8-byte timestamp prefix */
|
||||
|
||||
/* Networking */
|
||||
BasicUDPSocket serverSocket; /**< Bound to port; receives client commands */
|
||||
BasicUDPSocket clientSocket; /**< Configured to send to the connected client */
|
||||
volatile bool clientConnected; /**< True when a client has successfully connected */
|
||||
|
||||
/* Thread management */
|
||||
SingleThreadService executor; /**< Background thread service */
|
||||
|
||||
/* Packet sequencing */
|
||||
uint32 packetCounter; /**< Incremented for each DATA update sent */
|
||||
};
|
||||
|
||||
} /* namespace MARTe */
|
||||
|
||||
#endif /* UDPSTREAMER_H_ */
|
||||
@@ -0,0 +1 @@
|
||||
include Makefile.inc
|
||||
@@ -0,0 +1,38 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
OBJSX=
|
||||
|
||||
SPB = UDPStreamer.x
|
||||
|
||||
PACKAGE=Components
|
||||
ROOT_DIR=../../..
|
||||
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
|
||||
|
||||
all: $(OBJS) $(SUBPROJ)
|
||||
echo $(OBJS)
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
|
||||
@@ -0,0 +1 @@
|
||||
include Makefile.inc
|
||||
@@ -0,0 +1,57 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
OBJSX = UDPStreamerTest.x UDPStreamerGTest.x
|
||||
|
||||
PACKAGE=Components/DataSources
|
||||
ROOT_DIR=../../../..
|
||||
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
|
||||
|
||||
INCLUDES += -I.
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4StateMachine
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams
|
||||
INCLUDES += -I$(MARTe2_DIR)/Lib/gtest-1.7.0/include
|
||||
INCLUDES += -I../../../../Source/Components/DataSources/UDPStreamer
|
||||
|
||||
all: $(OBJS) \
|
||||
$(BUILD_DIR)/UDPStreamerTest$(LIBEXT)
|
||||
echo $(OBJS)
|
||||
|
||||
include depends.$(TARGET)
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* @file UDPStreamerGTest.cpp
|
||||
* @brief Source file for class UDPStreamerGTest
|
||||
* @date 13/05/2026
|
||||
* @author Martino Ferrari
|
||||
*
|
||||
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||||
* the Development of Fusion Energy ('Fusion for Energy').
|
||||
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||||
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||||
* You may not use this work except in compliance with the Licence.
|
||||
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||||
*
|
||||
* @warning Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the Licence is distributed on an "AS IS"
|
||||
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the Licence permissions and limitations under the Licence.
|
||||
*
|
||||
* @details This source file contains the GTest wrapper for all UDPStreamer tests.
|
||||
*/
|
||||
|
||||
#define DLL_API
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Standard header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include "gtest/gtest.h"
|
||||
#include <limits.h>
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Project header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
#include "UDPStreamerTest.h"
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Method definitions */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
TEST(UDPStreamerGTest, TestConstructor) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestConstructor());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestInitialise_Valid) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestInitialise_Valid());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestInitialise_DefaultPort) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestInitialise_DefaultPort());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestInitialise_InvalidMaxPayloadSize) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestInitialise_InvalidMaxPayloadSize());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestInitialise_ZeroStackSize) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestInitialise_ZeroStackSize());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestGetBrokerName_Output) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestGetBrokerName_Output());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestGetBrokerName_Input) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestGetBrokerName_Input());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestAllocateMemory) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestAllocateMemory());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_BasicSignals) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_BasicSignals());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_Quantized) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_Quantized());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_InvalidQuantOnInteger) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_InvalidQuantOnInteger());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_UnknownQuantType) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_UnknownQuantType());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_TimeModePacket) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_TimeModePacket());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_TimeModeFullArray) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_TimeModeFullArray());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_TimeModeFirstSample) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_TimeModeFirstSample());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_MissingSamplingRate) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_MissingSamplingRate());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_InvalidTimeSignal) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_InvalidTimeSignal());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSetConfiguredDatabase_FullArrayMismatch) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSetConfiguredDatabase_FullArrayMismatch());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestPrepareNextState) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestPrepareNextState());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestSynchronise_NoClient) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestSynchronise_NoClient());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestExecute_ConnectDataDisconnect) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestExecute_ConnectDataDisconnect());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestExecute_Fragmentation) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestExecute_Fragmentation());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestQuantization_Uint16Extremes) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestQuantization_Uint16Extremes());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestQuantization_Uint8Clamping) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestQuantization_Uint8Clamping());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestGetPort) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestGetPort());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestGetMaxPayloadSize) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestGetMaxPayloadSize());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestIsClientConnected_InitiallyFalse) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestIsClientConnected_InitiallyFalse());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestHighFrequency_AllocateMemory) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestHighFrequency_AllocateMemory());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestHighFrequency_Fragmentation) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestHighFrequency_Fragmentation());
|
||||
}
|
||||
|
||||
TEST(UDPStreamerGTest, TestHighFrequency_DataIntegrity) {
|
||||
UDPStreamerTest test;
|
||||
ASSERT_TRUE(test.TestHighFrequency_DataIntegrity());
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @file UDPStreamerTest.h
|
||||
* @brief Header file for class UDPStreamerTest
|
||||
* @date 13/05/2026
|
||||
* @author Martino Ferrari
|
||||
*
|
||||
* @copyright Copyright 2015 F4E | European Joint Undertaking for ITER and
|
||||
* the Development of Fusion Energy ('Fusion for Energy').
|
||||
* Licensed under the EUPL, Version 1.1 or - as soon they will be approved
|
||||
* by the European Commission - subsequent versions of the EUPL (the "Licence")
|
||||
* You may not use this work except in compliance with the Licence.
|
||||
* You may obtain a copy of the Licence at: http://ec.europa.eu/idabc/eupl
|
||||
*
|
||||
* @warning Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the Licence is distributed on an "AS IS"
|
||||
* basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
||||
* or implied. See the Licence permissions and limitations under the Licence.
|
||||
*
|
||||
* @details This header file contains the declaration of the class UDPStreamerTest
|
||||
* with all of its public, protected and private members. It may also include
|
||||
* definitions for inline methods which need to be visible to the compiler.
|
||||
*/
|
||||
|
||||
#ifndef UDPSTREAMERTEST_H_
|
||||
#define UDPSTREAMERTEST_H_
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Standard header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Project header includes */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/*---------------------------------------------------------------------------*/
|
||||
/* Class declaration */
|
||||
/*---------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* @brief Tests the UDPStreamer DataSource public methods.
|
||||
*/
|
||||
class UDPStreamerTest {
|
||||
public:
|
||||
|
||||
/**
|
||||
* @brief Tests the default constructor.
|
||||
*/
|
||||
bool TestConstructor();
|
||||
|
||||
/**
|
||||
* @brief Tests Initialise with a fully valid configuration.
|
||||
*/
|
||||
bool TestInitialise_Valid();
|
||||
|
||||
/**
|
||||
* @brief Tests that a missing Port uses the default (44500).
|
||||
*/
|
||||
bool TestInitialise_DefaultPort();
|
||||
|
||||
/**
|
||||
* @brief Tests that MaxPayloadSize below the minimum is rejected.
|
||||
*/
|
||||
bool TestInitialise_InvalidMaxPayloadSize();
|
||||
|
||||
/**
|
||||
* @brief Tests that StackSize = 0 is rejected.
|
||||
*/
|
||||
bool TestInitialise_ZeroStackSize();
|
||||
|
||||
/**
|
||||
* @brief Tests GetBrokerName returns the correct broker for OutputSignals.
|
||||
*/
|
||||
bool TestGetBrokerName_Output();
|
||||
|
||||
/**
|
||||
* @brief Tests GetBrokerName returns empty string for InputSignals.
|
||||
*/
|
||||
bool TestGetBrokerName_Input();
|
||||
|
||||
/**
|
||||
* @brief Tests AllocateMemory allocates readyBuffer, scratchBuffer, wireBuffer.
|
||||
*/
|
||||
bool TestAllocateMemory();
|
||||
|
||||
/**
|
||||
* @brief Tests SetConfiguredDatabase with basic signal types.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_BasicSignals();
|
||||
|
||||
/**
|
||||
* @brief Tests SetConfiguredDatabase with a quantized float signal.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_Quantized();
|
||||
|
||||
/**
|
||||
* @brief Tests that quantization on a non-float signal is rejected.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_InvalidQuantOnInteger();
|
||||
|
||||
/**
|
||||
* @brief Tests that an unknown QuantizedType is rejected.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_UnknownQuantType();
|
||||
|
||||
/**
|
||||
* @brief Tests TimeMode = PacketTime (default).
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_TimeModePacket();
|
||||
|
||||
/**
|
||||
* @brief Tests TimeMode = FullArray with a matching time signal.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_TimeModeFullArray();
|
||||
|
||||
/**
|
||||
* @brief Tests TimeMode = FirstSample with a scalar time signal.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_TimeModeFirstSample();
|
||||
|
||||
/**
|
||||
* @brief Tests that FirstSample without SamplingRate is rejected.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_MissingSamplingRate();
|
||||
|
||||
/**
|
||||
* @brief Tests that TimeSignal pointing to a non-existent signal is rejected.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_InvalidTimeSignal();
|
||||
|
||||
/**
|
||||
* @brief Tests that FullArray TimeMode with mismatched element counts is rejected.
|
||||
*/
|
||||
bool TestSetConfiguredDatabase_FullArrayMismatch();
|
||||
|
||||
/**
|
||||
* @brief Tests PrepareNextState starts the thread and opens the socket.
|
||||
*/
|
||||
bool TestPrepareNextState();
|
||||
|
||||
/**
|
||||
* @brief Tests Synchronise when no client is connected (must return true, no crash).
|
||||
*/
|
||||
bool TestSynchronise_NoClient();
|
||||
|
||||
/**
|
||||
* @brief Tests the full CONNECT → CONFIG → DATA → DISCONNECT flow via loopback UDP.
|
||||
*/
|
||||
bool TestExecute_ConnectDataDisconnect();
|
||||
|
||||
/**
|
||||
* @brief Tests that DATA packets are fragmented correctly for large payloads.
|
||||
*/
|
||||
bool TestExecute_Fragmentation();
|
||||
|
||||
/**
|
||||
* @brief Tests uint16 quantization: verifies that 0 maps to 0 and max maps to 65535.
|
||||
*/
|
||||
bool TestQuantization_Uint16Extremes();
|
||||
|
||||
/**
|
||||
* @brief Tests uint8 quantization: verifies clamping of out-of-range values.
|
||||
*/
|
||||
bool TestQuantization_Uint8Clamping();
|
||||
|
||||
/**
|
||||
* @brief Tests the GetPort() accessor.
|
||||
*/
|
||||
bool TestGetPort();
|
||||
|
||||
/**
|
||||
* @brief Tests the GetMaxPayloadSize() accessor.
|
||||
*/
|
||||
bool TestGetMaxPayloadSize();
|
||||
|
||||
/**
|
||||
* @brief Tests that IsClientConnected() returns false before any CONNECT.
|
||||
*/
|
||||
bool TestIsClientConnected_InitiallyFalse();
|
||||
|
||||
/**
|
||||
* @brief Tests AllocateMemory with 4 high-frequency int16[1000] packed channels (1 MSps).
|
||||
*/
|
||||
bool TestHighFrequency_AllocateMemory();
|
||||
|
||||
/**
|
||||
* @brief Tests that 4 int16[1000] channels produce exactly 6 fragments at MaxPayloadSize=1400.
|
||||
* Payload: 8 (HRT) + 8 (T0/uint64) + 4×2000 (int16[1000]) = 8016 bytes;
|
||||
* chunk = 1383 bytes → ceil(8016/1383) = 6 fragments.
|
||||
*/
|
||||
bool TestHighFrequency_Fragmentation();
|
||||
|
||||
/**
|
||||
* @brief Tests full CONNECT→DATA→DISCONNECT cycle with high-frequency packed signals.
|
||||
* Reassembles all fragments and verifies the total payload size.
|
||||
*/
|
||||
bool TestHighFrequency_DataIntegrity();
|
||||
};
|
||||
|
||||
#endif /* UDPSTREAMERTEST_H_ */
|
||||
@@ -0,0 +1,6 @@
|
||||
/depends.linux
|
||||
/dependsRaw.linux
|
||||
/depends.cov
|
||||
/dependsRaw.cov
|
||||
/cov/
|
||||
/createLibrary
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Main_TestAll.cpp
|
||||
*
|
||||
* Created on: 31/10/2016
|
||||
* Author: Andre Neto
|
||||
*/
|
||||
|
||||
#include "ErrorManagement.h"
|
||||
#include "Object.h"
|
||||
#include "StreamString.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include <limits.h>
|
||||
|
||||
void MainGTestComponentsErrorProcessFunction(const MARTe::ErrorManagement::ErrorInformation& errorInfo,
|
||||
const char* const errorDescription)
|
||||
{
|
||||
MARTe::StreamString errorCodeStr;
|
||||
MARTe::ErrorManagement::ErrorCodeToStream(errorInfo.header.errorType, errorCodeStr);
|
||||
printf("[%s - %s:%d]: %s\n", errorCodeStr.Buffer(), errorInfo.fileName, errorInfo.header.lineNumber, errorDescription);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
|
||||
|
||||
SetErrorProcessFunction(&MainGTestComponentsErrorProcessFunction);
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
include Makefile.inc
|
||||
|
||||
LIBRARIES += -L$(GTEST_DIR) -lgtest -lgtest_main -lpthread
|
||||
|
||||
LIBRARIES += -L$(MARTe2_LIB_DIR) -lMARTe2
|
||||
LIBRARIES += -ldl
|
||||
|
||||
|
||||
#Look for all the statically linked files
|
||||
LIBRARIES_STATIC+=$(shell find $(ROOT_DIR)/Build/$(TARGET) -name "*.a")
|
||||
@@ -0,0 +1,54 @@
|
||||
#############################################################
|
||||
#
|
||||
# Copyright 2015 F4E | European Joint Undertaking for ITER
|
||||
# and the Development of Fusion Energy ('Fusion for Energy')
|
||||
#
|
||||
# Licensed under the EUPL, Version 1.1 or - as soon they
|
||||
# will be approved by the European Commission - subsequent
|
||||
# versions of the EUPL (the "Licence");
|
||||
# You may not use this work except in compliance with the
|
||||
# Licence.
|
||||
# You may obtain a copy of the Licence at:
|
||||
#
|
||||
# http://ec.europa.eu/idabc/eupl
|
||||
#
|
||||
# Unless required by applicable law or agreed to in
|
||||
# writing, software distributed under the Licence is
|
||||
# distributed on an "AS IS" basis,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
|
||||
# express or implied.
|
||||
# See the Licence for the specific language governing
|
||||
# permissions and limitations under the Licence.
|
||||
#
|
||||
#############################################################
|
||||
|
||||
PACKAGE=
|
||||
ROOT_DIR=../..
|
||||
MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET)
|
||||
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability
|
||||
INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams
|
||||
INCLUDES += -I$(MARTe2_DIR)/Lib/gtest-1.7.0/include
|
||||
INCLUDES += -I../../Source/Components/DataSources/UDPStreamer
|
||||
INCLUDES += -I../Components/DataSources/UDPStreamer
|
||||
|
||||
|
||||
|
||||
all: $(BUILD_DIR)/MainGTest$(EXEEXT)
|
||||
|
||||
include depends.$(TARGET)
|
||||
|
||||
include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET)
|
||||
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* Test MARTe2 application for UDPStreamer DataSource.
|
||||
*
|
||||
* Generates scalar and high-frequency packed signals and streams them via UDPStreamer.
|
||||
* Connect with the WebUI client (Client/WebUI) to visualise the signals.
|
||||
*
|
||||
* Signals produced (scalar, 10 kHz):
|
||||
* Counter – uint32 cycle counter from LinuxTimer
|
||||
* Time – uint32 time in microseconds from LinuxTimer
|
||||
* Sine1 – float32, 1 Hz sine, amplitude 10, quantised to uint16 on wire
|
||||
* Sine2 – float32, 0.3 Hz sine, amplitude 5, raw float32 on wire
|
||||
*
|
||||
* Signals produced (packed temporal arrays, 10 kHz × 1000 samples = 10 MSps):
|
||||
* Ch1 – float32[1000], 1 kHz sine, amplitude 1.0
|
||||
* Ch2 – float32[1000], 1 kHz sine, amplitude 0.5, phase π/2
|
||||
* Both channels use TimeMode=FirstSample with Time as the anchor and
|
||||
* SamplingRate=10000000 so the WebUI reconstructs the per-sample timestamps.
|
||||
*/
|
||||
$TestApp = {
|
||||
Class = RealTimeApplication
|
||||
+Functions = {
|
||||
Class = ReferenceContainer
|
||||
|
||||
// ── Copy Counter + Time from LinuxTimer into the inter-GAM DDB ──────
|
||||
+TimerGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Counter = {
|
||||
DataSource = Timer
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
Frequency = 1000
|
||||
DataSource = Timer
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Counter = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1 Hz sinusoidal signal ───────────────────────────────────────────
|
||||
+SineGAM1 = {
|
||||
Class = WaveformSin
|
||||
Amplitude = 10.0
|
||||
Frequency = 1.0
|
||||
Phase = 0.0
|
||||
Offset = 0.0
|
||||
InputSignals = {
|
||||
Time = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Sine1 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 0.3 Hz sinusoidal signal (phase-shifted) ─────────────────────────
|
||||
+SineGAM2 = {
|
||||
Class = WaveformSin
|
||||
Amplitude = 5.0
|
||||
Frequency = 0.3
|
||||
Phase = 1.0472
|
||||
Offset = 0.0
|
||||
InputSignals = {
|
||||
Time = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Sine2 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1 kHz sine burst – channel 1 (1000 samples/packet at 10 MSps) ──────
|
||||
+SineGAM3 = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0
|
||||
Amplitude = 1.0
|
||||
Phase = 0.0
|
||||
Offset = 0.0
|
||||
SamplingRate = 1000000.0
|
||||
OutputSignals = {
|
||||
Ch1 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1 kHz sine burst – channel 2 (phase-shifted by π/2) ──────────────
|
||||
+SineGAM4 = {
|
||||
Class = SineArrayGAM
|
||||
Frequency = 1000.0
|
||||
Amplitude = 0.5
|
||||
Phase = 1.5708
|
||||
Offset = 0.0
|
||||
SamplingRate = 1000000.0
|
||||
OutputSignals = {
|
||||
Ch2 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Route signals into UDPStreamer ────────────────────────────────────
|
||||
+StreamerGAM = {
|
||||
Class = IOGAM
|
||||
InputSignals = {
|
||||
Counter = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
DataSource = DDB
|
||||
Type = uint32
|
||||
}
|
||||
Sine1 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
}
|
||||
Sine2 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
}
|
||||
Ch1 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
Ch2 = {
|
||||
DataSource = DDB
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
}
|
||||
OutputSignals = {
|
||||
Counter = {
|
||||
DataSource = Streamer
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
DataSource = Streamer
|
||||
Type = uint32
|
||||
}
|
||||
Sine1 = {
|
||||
DataSource = Streamer
|
||||
Type = float32
|
||||
}
|
||||
Sine2 = {
|
||||
DataSource = Streamer
|
||||
Type = float32
|
||||
}
|
||||
Ch1 = {
|
||||
DataSource = Streamer
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
Ch2 = {
|
||||
DataSource = Streamer
|
||||
Type = float32
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+Data = {
|
||||
Class = ReferenceContainer
|
||||
DefaultDataSource = DDB
|
||||
|
||||
// ── Inter-GAM data buffer ────────────────────────────────────────────
|
||||
+DDB = {
|
||||
Class = GAMDataSource
|
||||
}
|
||||
|
||||
// ── Real-time clock / trigger source ─────────────────────────────────
|
||||
+Timer = {
|
||||
Class = LinuxTimer
|
||||
SleepNature = "Default"
|
||||
Signals = {
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
Type = uint32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── UDP Streamer DataSource ──────────────────────────────────────────
|
||||
+Streamer = {
|
||||
Class = UDPStreamer
|
||||
Port = 44500
|
||||
MaxPayloadSize = 1400
|
||||
Signals = {
|
||||
Counter = {
|
||||
Type = uint32
|
||||
}
|
||||
Time = {
|
||||
Type = uint32
|
||||
Unit = "us"
|
||||
}
|
||||
Sine1 = {
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
RangeMin = -10.0
|
||||
RangeMax = 10.0
|
||||
QuantizedType = "uint16"
|
||||
}
|
||||
Sine2 = {
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
RangeMin = -5.0
|
||||
RangeMax = 5.0
|
||||
}
|
||||
Ch1 = {
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
TimeMode = FirstSample
|
||||
TimeSignal = Time
|
||||
SamplingRate = 1000000.0
|
||||
}
|
||||
Ch2 = {
|
||||
Type = float32
|
||||
Unit = "V"
|
||||
NumberOfDimensions = 1
|
||||
NumberOfElements = 1000
|
||||
TimeMode = FirstSample
|
||||
TimeSignal = Time
|
||||
SamplingRate = 1000000.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Timing statistics ────────────────────────────────────────────────
|
||||
+Timings = {
|
||||
Class = TimingDataSource
|
||||
}
|
||||
}
|
||||
|
||||
+States = {
|
||||
Class = ReferenceContainer
|
||||
+Running = {
|
||||
Class = RealTimeState
|
||||
+Threads = {
|
||||
Class = ReferenceContainer
|
||||
+Thread1 = {
|
||||
Class = RealTimeThread
|
||||
CPUs = 0x1
|
||||
Functions = {TimerGAM SineGAM1 SineGAM2 SineGAM3 SineGAM4 StreamerGAM}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+Scheduler = {
|
||||
Class = GAMScheduler
|
||||
TimingDataSource = Timings
|
||||
}
|
||||
}
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
# run.sh – Build UDPStreamer and launch the test MARTe2 application.
|
||||
#
|
||||
# Usage:
|
||||
# ./run.sh # run MARTe2 app only
|
||||
# ./run.sh --webui # also start the WebUI Go client in the background
|
||||
# ./run.sh --help # show this message
|
||||
#
|
||||
# Prerequisites:
|
||||
# - marte_env.sh must be present in the repo root (sets MARTe2_DIR, etc.)
|
||||
# - MARTe2 and MARTe2-components must already be built
|
||||
#
|
||||
# The script resolves all paths relative to the repo root so it can be
|
||||
# called from any working directory.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
# ── Parse arguments ──────────────────────────────────────────────────────────
|
||||
START_WEBUI=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--webui) START_WEBUI=1 ;;
|
||||
--help|-h)
|
||||
sed -n '2,12p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Load MARTe2 environment ──────────────────────────────────────────────────
|
||||
ENV_SCRIPT="${REPO_ROOT}/marte_env.sh"
|
||||
if [ ! -f "${ENV_SCRIPT}" ]; then
|
||||
echo "ERROR: ${ENV_SCRIPT} not found." >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
source "${ENV_SCRIPT}"
|
||||
|
||||
if [ -z "${MARTe2_DIR}" ]; then
|
||||
echo "ERROR: MARTe2_DIR is not set after sourcing marte_env.sh." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Build UDPStreamer ─────────────────────────────────────────────────────────
|
||||
TARGET=x86-linux
|
||||
UDPSTREAMER_SRC="${REPO_ROOT}/Source/Components/DataSources/UDPStreamer"
|
||||
UDPSTREAMER_LIB="${REPO_ROOT}/Build/${TARGET}/Components/DataSources/UDPStreamer"
|
||||
|
||||
echo "==> Building UDPStreamer (TARGET=${TARGET})..."
|
||||
make -C "${UDPSTREAMER_SRC}" \
|
||||
-f Makefile.gcc \
|
||||
TARGET="${TARGET}" \
|
||||
2>&1 | tail -5
|
||||
echo "==> Build done."
|
||||
|
||||
# ── Build WebUI binary (if requested and not already built) ──────────────────
|
||||
WEBUI_DIR="${REPO_ROOT}/Client/WebUI"
|
||||
WEBUI_BIN="${WEBUI_DIR}/udpstreamer-webui"
|
||||
if [ "${START_WEBUI}" -eq 1 ] && [ ! -x "${WEBUI_BIN}" ]; then
|
||||
echo "==> Building WebUI..."
|
||||
(cd "${WEBUI_DIR}" && go build -o udpstreamer-webui ./...)
|
||||
echo "==> WebUI build done."
|
||||
fi
|
||||
|
||||
# ── Set LD_LIBRARY_PATH ───────────────────────────────────────────────────────
|
||||
COMP="${MARTe2_Components_DIR}/Build/${TARGET}/Components"
|
||||
|
||||
export LD_LIBRARY_PATH="\
|
||||
${UDPSTREAMER_LIB}:\
|
||||
${MARTe2_DIR}/Build/${TARGET}/Core:\
|
||||
${COMP}/DataSources/LinuxTimer:\
|
||||
${COMP}/DataSources/LoggerDataSource:\
|
||||
${COMP}/GAMs/IOGAM:\
|
||||
${COMP}/GAMs/WaveformGAM:\
|
||||
${LD_LIBRARY_PATH}"
|
||||
|
||||
echo "==> LD_LIBRARY_PATH=${LD_LIBRARY_PATH}"
|
||||
|
||||
# ── Create class-name symlinks for MARTe2 auto-loader ────────────────────────
|
||||
# MARTe2 looks for <ClassName>.so when a class is not yet registered.
|
||||
# Some components bundle multiple classes into one .so (e.g. WaveformGAM.so
|
||||
# contains WaveformSin, WaveformChirp, WaveformPointsDef). We need symlinks
|
||||
# so dlopen("<ClassName>.so") succeeds.
|
||||
WAVEFORM_DIR="${COMP}/GAMs/WaveformGAM"
|
||||
for cls in WaveformSin WaveformChirp WaveformPointsDef; do
|
||||
target="${WAVEFORM_DIR}/${cls}.so"
|
||||
if [ ! -e "${target}" ]; then
|
||||
ln -sf "${WAVEFORM_DIR}/WaveformGAM.so" "${target}"
|
||||
fi
|
||||
done
|
||||
|
||||
# SineArrayGAM is bundled inside UDPStreamer.so; create a symlink so MARTe2
|
||||
# can dlopen("SineArrayGAM.so") before UDPStreamer has been registered.
|
||||
SINE_LINK="${UDPSTREAMER_LIB}/SineArrayGAM.so"
|
||||
if [ ! -e "${SINE_LINK}" ]; then
|
||||
ln -sf "${UDPSTREAMER_LIB}/UDPStreamer.so" "${SINE_LINK}"
|
||||
fi
|
||||
|
||||
# ── Optionally start WebUI ────────────────────────────────────────────────────
|
||||
if [ "${START_WEBUI}" -eq 1 ]; then
|
||||
echo "==> Starting WebUI on http://localhost:8080 (streamer at 127.0.0.1:44500)..."
|
||||
"${WEBUI_BIN}" \
|
||||
--streamer 127.0.0.1:44500 \
|
||||
--listen :8080 \
|
||||
--clientport 44900 &
|
||||
WEBUI_PID=$!
|
||||
echo "==> WebUI PID ${WEBUI_PID}"
|
||||
fi
|
||||
|
||||
# ── Launch MARTe2 application ─────────────────────────────────────────────────
|
||||
MARTE_APP="${MARTe2_DIR}/Build/${TARGET}/App/MARTeApp.ex"
|
||||
CFG="${SCRIPT_DIR}/TestApp.cfg"
|
||||
|
||||
if [ ! -x "${MARTE_APP}" ]; then
|
||||
echo "ERROR: MARTeApp.ex not found at ${MARTE_APP}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Starting MARTe2 application (state=Running, 100 Hz)..."
|
||||
echo "==> Press Ctrl+C to stop."
|
||||
echo ""
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "==> Stopping..."
|
||||
if [ "${START_WEBUI}" -eq 1 ] && kill -0 "${WEBUI_PID}" 2>/dev/null; then
|
||||
kill "${WEBUI_PID}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
"${MARTE_APP}" \
|
||||
-l RealTimeLoader \
|
||||
-f "${CFG}" \
|
||||
-s Running
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
BDIR=Build/x86-linux
|
||||
|
||||
export MARTE_ENV="MARTe"
|
||||
|
||||
MARTE_ENV_PATH="${HOME}/workspace"
|
||||
export MARTE_ENV_PATH="$MARTE_ENV_PATH"
|
||||
export EPICS_BASE=/opt/epics/
|
||||
|
||||
export MARTe2_DIR="$MARTE_ENV_PATH/MARTe2"
|
||||
export MARTe2_LIB_DIR="$MARTe2_DIR/$BDIR/Core"
|
||||
export MARTe2_Components_DIR="$MARTE_ENV_PATH/MARTe2-components"
|
||||
|
||||
export LCOV_FLAGS="--ignore-errors mismatch,mismatch"
|
||||
|
||||
|
||||
export GTEST_DIR="$MARTe2_DIR/Lib/gtest-1.7.0"
|
||||
|
||||
|
||||
LIBRARIES+=("$MARTe2_LIB_DIR")
|
||||
LIBRARIES+=("$MARTe2_Components_DIR/$BDIR/Components/GAMs/IOGAM")
|
||||
LIBRARIES+=("$MARTe2_Components_DIR/$BDIR/Components/GAMs/WaveformGAM")
|
||||
LIBRARIES+=("$MARTe2_Components_DIR/$BDIR/Components/DataSources/LinuxTimer")
|
||||
LIBRARIES+=("$MARTe2_Components_DIR/$BDIR/Components/DataSources/LoggerDataSource")
|
||||
LIBRARIES+=("/opt/epics/lib/linux-x86_64")
|
||||
|
||||
export CODAC_ROOT="${MARTE_ENV_PATH}/codac"
|
||||
|
||||
export _OLD_LIBRARY_PATH="$LD_LIBRARY_PATH"
|
||||
_NEW_LIB_PATH=""
|
||||
for _lib in "${LIBRARIES[@]}"; do
|
||||
if [ -z "$_NEW_LIB_PATH" ]; then
|
||||
_NEW_LIB_PATH="$_lib"
|
||||
else
|
||||
_NEW_LIB_PATH="$_NEW_LIB_PATH:$_lib"
|
||||
fi
|
||||
done
|
||||
export LD_LIBRARY_PATH="$_NEW_LIB_PATH"
|
||||
Reference in New Issue
Block a user